155 lines
8.1 KiB
JavaScript
155 lines
8.1 KiB
JavaScript
// Shared native-slot identity contract. Names are schema keys, never labels/types.
|
|
const configuringNodes = new WeakMap();
|
|
const configuredPrototypes = new WeakSet();
|
|
export const nativeSlotsAreConfiguring = node => (configuringNodes.get(node) || 0) > 0;
|
|
export function installNativeSlotConfigureTransaction(nodeType, validate = () => {}) {
|
|
const prototype = nodeType.prototype;
|
|
if (configuredPrototypes.has(prototype)) return;
|
|
const original = prototype.configure;
|
|
if (typeof original !== "function") return;
|
|
configuredPrototypes.add(prototype);
|
|
prototype.configure = function(data, ...args) {
|
|
validate(data);
|
|
const depth = configuringNodes.get(this) || 0;
|
|
configuringNodes.set(this, depth + 1);
|
|
try { return original.call(this, data, ...args); }
|
|
finally { if (depth) configuringNodes.set(this, depth); else configuringNodes.delete(this); }
|
|
};
|
|
}
|
|
export function updateNativeSlot(slot, values) {
|
|
Object.assign(slot, values);
|
|
return slot;
|
|
}
|
|
|
|
export function reusableNativeSlotIndex(ports, id, name, identityKey = "bvSlotId") {
|
|
const entries = (ports || []).map((slot, index) => ({slot, index}));
|
|
const identified = entries.filter(({slot}) => slot[identityKey] === id);
|
|
const candidates = identified.length ? identified : entries.filter(({slot}) => !slot[identityKey] && slot.name === name);
|
|
return (candidates.find(({slot}) => slot.link != null || Boolean(slot.links?.length)) || candidates[0])?.index ?? -1;
|
|
}
|
|
|
|
export function resolveNativeSlot(slot, definitions, identityKey = "bvSlotId") {
|
|
const identity = slot?.[identityKey];
|
|
const matches = definitions.filter(definition => identity ? definition.id === identity : definition.name === slot?.name);
|
|
if (matches.length !== 1) throw new Error(`Missing or ambiguous slot identity: ${slot?.name}`);
|
|
if (identity && definitions.some(definition => definition.name === slot.name && definition.id !== identity)) throw new Error(`Conflicting slot identity: ${slot.name}`);
|
|
return matches[0];
|
|
}
|
|
|
|
export function validateSavedSlotIdentities(slots, definitions, identityKey = "bvSlotId") {
|
|
const seen = new Set();
|
|
for (const slot of slots || []) {
|
|
if (!slot[identityKey]) continue;
|
|
const definition = resolveNativeSlot(slot, definitions, identityKey);
|
|
if (seen.has(definition.id)) throw new Error(`Duplicate saved slot identity: ${definition.id}`);
|
|
seen.add(definition.id);
|
|
}
|
|
}
|
|
|
|
export function nativeOutputIndexMap(outputs, definitions, reserved = {pipe: 0}) {
|
|
const result = {}, identities = new Set();
|
|
if (definitions.some(item => !item.id || !Number.isInteger(item.executionIndex) || item.executionIndex < 0 || Object.values(reserved).includes(item.executionIndex))) throw new Error("Invalid execution slot schema");
|
|
if (new Set(definitions.map(item => item.id)).size !== definitions.length || new Set(definitions.map(item => item.executionIndex)).size !== definitions.length) throw new Error("Ambiguous execution slot schema");
|
|
for (const [name, index] of Object.entries(reserved)) if (outputs?.[index]?.name !== name) throw new Error(`Missing reserved output: ${name}`);
|
|
for (const [index, slot] of (outputs || []).entries()) {
|
|
if (Object.hasOwn(reserved, slot.name)) {
|
|
if (index !== reserved[slot.name] || slot.bvSlotId || (outputs || []).filter(item => item.name === slot.name).length !== 1) throw new Error(`Conflicting reserved output: ${slot.name}`);
|
|
result[index] = reserved[slot.name]; continue;
|
|
}
|
|
const definition = resolveNativeSlot(slot, definitions);
|
|
if (identities.has(definition.id)) throw new Error(`Duplicate output identity: ${definition.id}`);
|
|
identities.add(definition.id);
|
|
result[index] = definition.executionIndex;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function validateNativeOutputLinks(prompt, maps) {
|
|
for (const entry of Object.values(prompt || {})) for (const value of Object.values(entry?.inputs || {})) {
|
|
if (!Array.isArray(value) || value.length !== 2) continue;
|
|
const map = maps[String(value[0])];
|
|
if (map && (!Number.isInteger(value[1]) || !Object.hasOwn(map, value[1]))) throw new Error(`Unknown connected output index: ${value[0]}:${value[1]}`);
|
|
}
|
|
}
|
|
|
|
export function validateConnectedSlotIdentities(ports, connected, reserved, identityKey = "bvSlotId") {
|
|
duplicateNativeSlotRemovals(ports, connected, reserved);
|
|
const identities = new Set();
|
|
for (const slot of ports || []) {
|
|
if (reserved(slot) || !connected(slot)) continue;
|
|
const key = slot[identityKey] || slot.name;
|
|
if (identities.has(key)) throw new Error(`Ambiguous connected slot identity: ${slot.name}`);
|
|
identities.add(key);
|
|
}
|
|
}
|
|
|
|
/** Plan all removals first: no stale indices across duplicate groups, no lost links. */
|
|
export function duplicateNativeSlotRemovals(ports, connected, reserved = slot => slot.name === "pipe") {
|
|
const groups = new Map();
|
|
for (const [index, slot] of (ports || []).entries()) {
|
|
if (reserved(slot)) continue;
|
|
const group = groups.get(slot.name) || []; group.push({slot, index}); groups.set(slot.name, group);
|
|
}
|
|
const removals = [];
|
|
for (const group of groups.values()) {
|
|
const linked = group.filter(({slot}) => connected(slot));
|
|
if (linked.length > 1) throw new Error(`Ambiguous connected slot: ${group[0].slot.name}`);
|
|
const keep = linked[0] || group[0];
|
|
for (const item of group) if (item !== keep) removals.push(item.index);
|
|
}
|
|
return removals.sort((a, b) => b - a);
|
|
}
|
|
|
|
/** Restore saved physical order before the enclosing graph restores link indices. */
|
|
export function restoreNativeSlotOrder(node, data, createMissing, identityKey = "bvSlotId") {
|
|
const plans = [];
|
|
for (const field of ["inputs", "outputs"]) {
|
|
if (!Array.isArray(data?.[field])) continue;
|
|
const used = new Set();
|
|
const names = new Set();
|
|
// Native cloneObject copies saved array indices onto the existing backend
|
|
// array without truncating it. During configure the matching prefix is the
|
|
// authoritative saved contract; trailing factory slots may duplicate names.
|
|
const savedPrefix = nativeSlotsAreConfiguring(node) && data[field].every((saved, index) => {
|
|
const slot = node[field]?.[index];
|
|
if (slot?.name !== saved.name) return false;
|
|
return field === "inputs" ? (slot.link ?? null) === (saved.link ?? null)
|
|
: JSON.stringify(slot.links || []) === JSON.stringify(saved.links || []);
|
|
});
|
|
const ordered = data[field].map((saved, index) => {
|
|
if (names.has(saved.name)) throw new Error(`Ambiguous saved ${field} slot: ${saved.name}`);
|
|
names.add(saved.name);
|
|
const matches = savedPrefix ? [node[field][index]] : (node[field] || []).filter(slot => slot.name === saved.name);
|
|
if (matches.length > 1 || (matches.length && used.has(matches[0]))) throw new Error(`Ambiguous saved ${field} slot: ${saved.name}`);
|
|
if (matches[0]?.[identityKey] && saved[identityKey] && matches[0][identityKey] !== saved[identityKey]) throw new Error(`Conflicting saved slot identity: ${saved.name}`);
|
|
if (!matches.length) {
|
|
const create = createMissing?.(field, saved);
|
|
if (!create) throw new Error(`Missing saved ${field} slot: ${saved.name}`);
|
|
if (used.has(saved.name)) throw new Error(`Duplicate saved slot: ${saved.name}`);
|
|
used.add(saved.name);
|
|
return {create, saved};
|
|
}
|
|
used.add(matches[0]);
|
|
return {slot: matches[0], saved};
|
|
});
|
|
plans.push({field, ordered, savedPrefix});
|
|
}
|
|
for (const {field, ordered, savedPrefix} of plans) {
|
|
for (const item of ordered) {
|
|
if (item.create) item.slot = item.create();
|
|
if (item.saved[identityKey]) item.slot[identityKey] = item.saved[identityKey];
|
|
else if (savedPrefix) delete item.slot[identityKey];
|
|
}
|
|
node[field].splice(0, node[field].length, ...ordered.map(item => item.slot));
|
|
node[field].forEach((slot, index) => {
|
|
if (Object.hasOwn(slot, "slot_index")) slot.slot_index = index;
|
|
});
|
|
}
|
|
}
|
|
|
|
export function serializeNativeSlotIdentities(node, data, identityKey = "bvSlotId") {
|
|
for (const field of ["inputs", "outputs"]) for (const [index, slot] of (node[field] || []).entries()) {
|
|
if (slot[identityKey] && data[field]?.[index]) data[field][index][identityKey] = slot[identityKey];
|
|
}
|
|
}
|