From e632f5c69b6e9ab9101a175ab383e3dda393dde9 Mon Sep 17 00:00:00 2001 From: filtered <176114999+webfiltered@users.noreply.github.com> Date: Mon, 10 Mar 2025 23:20:34 +1100 Subject: [PATCH] Fix all unknown slot props are serialised (#732) Ensures only specified properties are cloned for serialisation. --- src/LGraphNode.ts | 6 +++--- src/NodeSlot.ts | 43 +++++++++++++++++++++++++------------- src/interfaces.ts | 7 +++++++ src/types/serialisation.ts | 8 +++---- test/NodeSlot.test.ts | 12 +++++------ 5 files changed, 49 insertions(+), 27 deletions(-) diff --git a/src/LGraphNode.ts b/src/LGraphNode.ts index 4557de60d1..1b273d4091 100644 --- a/src/LGraphNode.ts +++ b/src/LGraphNode.ts @@ -32,7 +32,7 @@ import { LGraphCanvas } from "./LGraphCanvas" import { type LGraphNodeConstructor, LiteGraph } from "./litegraph" import { LLink } from "./LLink" import { createBounds, isInRect, isInRectangle, isPointInRect, snapPoint } from "./measure" -import { ConnectionColorContext, isINodeInputSlot, isWidgetInputSlot, NodeInputSlot, NodeOutputSlot, serializeSlot, toNodeSlotClass } from "./NodeSlot" +import { ConnectionColorContext, inputAsSerialisable, isINodeInputSlot, isWidgetInputSlot, NodeInputSlot, NodeOutputSlot, outputAsSerialisable, toNodeSlotClass } from "./NodeSlot" import { LGraphEventMode, NodeSlotType, @@ -703,8 +703,8 @@ export class LGraphNode implements Positionable, IPinnable, IColorable { if (this.constructor === LGraphNode && this.last_serialization) return this.last_serialization - if (this.inputs) o.inputs = this.inputs.map(input => serializeSlot(input)) - if (this.outputs) o.outputs = this.outputs.map(output => serializeSlot(output)) + if (this.inputs) o.inputs = this.inputs.map(input => inputAsSerialisable(input)) + if (this.outputs) o.outputs = this.outputs.map(output => outputAsSerialisable(output)) if (this.title && this.title != this.constructor.title) o.title = this.title diff --git a/src/NodeSlot.ts b/src/NodeSlot.ts index d69a893922..71f61322cd 100644 --- a/src/NodeSlot.ts +++ b/src/NodeSlot.ts @@ -1,13 +1,12 @@ -import type { CanvasColour, Dictionary, INodeInputSlot, INodeOutputSlot, INodeSlot, ISlotType, IWidgetInputSlot, Point } from "./interfaces" +import type { CanvasColour, Dictionary, INodeInputSlot, INodeOutputSlot, INodeSlot, ISlotType, IWidgetInputSlot, Point, SharedIntersection } from "./interfaces" import type { LinkId } from "./LLink" import type { IWidget } from "./types/widgets" import { LabelPosition, SlotShape, SlotType } from "./draw" import { LiteGraph } from "./litegraph" import { LinkDirection, RenderShape } from "./types/globalEnums" -import { ISerialisedNodeOutputSlot } from "./types/serialisation" -import { ISerialisedNodeInputSlot } from "./types/serialisation" -import { omitBy } from "./utils/object" +import { ISerialisableNodeOutput } from "./types/serialisation" +import { ISerialisableNodeInput } from "./types/serialisation" export interface ConnectionColorContext { default_connection_color: { @@ -31,16 +30,32 @@ interface IDrawOptions { highlight?: boolean } -export function serializeSlot(slot: INodeInputSlot): ISerialisedNodeInputSlot -export function serializeSlot(slot: INodeOutputSlot): ISerialisedNodeOutputSlot -export function serializeSlot(slot: INodeInputSlot | INodeOutputSlot): ISerialisedNodeInputSlot | ISerialisedNodeOutputSlot { - return omitBy({ - ...slot, - _layoutElement: undefined, - _data: undefined, - pos: isWidgetInputSlot(slot) ? undefined : slot.pos, - widget: isWidgetInputSlot(slot) ? { name: slot.widget.name } : undefined, - }, value => value === undefined) as ISerialisedNodeInputSlot | ISerialisedNodeOutputSlot +type CommonIoSlotProps = SharedIntersection + +export function shallowCloneCommonProps(slot: CommonIoSlotProps): CommonIoSlotProps { + const { color_off, color_on, dir, label, localized_name, locked, name, nameLocked, removable, shape, type } = slot + return { color_off, color_on, dir, label, localized_name, locked, name, nameLocked, removable, shape, type } +} + +export function inputAsSerialisable(slot: INodeInputSlot): ISerialisableNodeInput { + const widgetInputProps = slot.widget + ? { widget: { name: slot.widget.name } } + : { pos: slot.pos } + + return { + ...shallowCloneCommonProps(slot), + ...widgetInputProps, + link: slot.link, + } +} + +export function outputAsSerialisable(slot: INodeOutputSlot): ISerialisableNodeOutput { + return { + ...shallowCloneCommonProps(slot), + pos: slot.pos, + slot_index: slot.slot_index, + links: slot.links, + } } export function toNodeSlotClass(slot: INodeSlot): NodeSlot { diff --git a/src/interfaces.ts b/src/interfaces.ts index 1c34304b24..b5c6eeae88 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -22,6 +22,13 @@ export type WhenNullish = T & {} | (T extends null ? Result : T exten /** A type with each of the {@link Properties} made optional. */ export type OptionalProps = Omit & { [K in Properties]?: T[K] } +/** Bitwise AND intersection of two types; returns a new, non-union type that includes only properties that exist on both types. */ +export type SharedIntersection = { + [P in keyof T1 as P extends keyof T2 ? P : never]: T1[P] +} & { + [P in keyof T2 as P extends keyof T1 ? P : never]: T2[P] +} + export type CanvasColour = string | CanvasGradient | CanvasPattern /** An object containing a set of child objects */ diff --git a/src/types/serialisation.ts b/src/types/serialisation.ts index f70aa03704..9b0a4227cd 100644 --- a/src/types/serialisation.ts +++ b/src/types/serialisation.ts @@ -40,10 +40,10 @@ export interface SerialisableGraph { extra?: Record } -export type ISerialisedNodeInputSlot = Omit & { +export type ISerialisableNodeInput = Omit & { widget?: { name: string } } -export type ISerialisedNodeOutputSlot = Omit +export type ISerialisableNodeOutput = Omit /** Serialised LGraphNode */ export interface ISerialisedNode { @@ -55,8 +55,8 @@ export interface ISerialisedNode { flags?: INodeFlags order?: number mode?: number - outputs?: ISerialisedNodeOutputSlot[] - inputs?: ISerialisedNodeInputSlot[] + outputs?: ISerialisableNodeOutput[] + inputs?: ISerialisableNodeInput[] properties?: Dictionary shape?: RenderShape boxcolor?: string diff --git a/test/NodeSlot.test.ts b/test/NodeSlot.test.ts index 45637f0bbd..b8333d6cae 100644 --- a/test/NodeSlot.test.ts +++ b/test/NodeSlot.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from "vitest" import { INodeInputSlot, INodeOutputSlot } from "@/interfaces" -import { serializeSlot } from "@/NodeSlot" +import { inputAsSerialisable, outputAsSerialisable } from "@/NodeSlot" describe("NodeSlot", () => { - describe("serializeSlot", () => { + describe("inputAsSerialisable", () => { it("removes _data from serialized slot", () => { const slot: INodeOutputSlot = { _data: "test data", @@ -12,7 +12,7 @@ describe("NodeSlot", () => { type: "STRING", links: [], } - const serialized = serializeSlot(slot) + const serialized = outputAsSerialisable(slot) expect(serialized).not.toHaveProperty("_data") }) @@ -32,7 +32,7 @@ describe("NodeSlot", () => { }, } - const serialized = serializeSlot(widgetInputSlot) + const serialized = inputAsSerialisable(widgetInputSlot) expect(serialized).not.toHaveProperty("pos") }) @@ -43,7 +43,7 @@ describe("NodeSlot", () => { pos: [10, 20], link: null, } - const serialized = serializeSlot(normalSlot) + const serialized = inputAsSerialisable(normalSlot) expect(serialized).toHaveProperty("pos") }) @@ -62,7 +62,7 @@ describe("NodeSlot", () => { }, } - const serialized = serializeSlot(widgetInputSlot) + const serialized = inputAsSerialisable(widgetInputSlot) expect(serialized.widget).toEqual({ name: "test-widget" }) expect(serialized.widget).not.toHaveProperty("type") expect(serialized.widget).not.toHaveProperty("value")