diff --git a/src/LGraph.ts b/src/LGraph.ts index e2633e981..60c866efb 100644 --- a/src/LGraph.ts +++ b/src/LGraph.ts @@ -14,6 +14,9 @@ import type { SerialisableGraph, SerialisableReroute, } from "./types/serialisation" +import type { UUID } from "@/utils/uuid" + +import { createUuidv4, zeroUuid } from "@/utils/uuid" import { LGraphCanvas } from "./LGraphCanvas" import { LGraphGroup } from "./LGraphGroup" @@ -66,6 +69,9 @@ export class LGraph implements LinkNetwork, Serialisable { static STATUS_STOPPED = 1 static STATUS_RUNNING = 2 + id: UUID = zeroUuid + revision: number = 0 + _version: number = -1 /** The backing store for links. Keys are wrapped in String() */ _links: Map = new Map() @@ -1513,6 +1519,8 @@ export class LGraph implements LinkNetwork, Serialisable { extra.reroutes = reroutes?.length ? reroutes : undefined return { + id: this.id, + revision: this.revision, last_node_id: state.lastNodeId, last_link_id: state.lastLinkId, nodes, @@ -1534,7 +1542,7 @@ export class LGraph implements LinkNetwork, Serialisable { * It is intended for use with {@link structuredClone} or {@link JSON.stringify}. */ asSerialisable(options?: { sortNodes: boolean }): SerialisableGraph & Required> { - const { config, state, extra } = this + const { id, revision, config, state, extra } = this const nodeList = !LiteGraph.use_uuids && options?.sortNodes // @ts-expect-error If LiteGraph.use_uuids is false, ids are numbers. @@ -1549,6 +1557,8 @@ export class LGraph implements LinkNetwork, Serialisable { const reroutes = this.reroutes.size ? [...this.reroutes.values()].map(x => x.asSerialisable()) : undefined const data: ReturnType = { + id, + revision, version: LGraph.serialisedSchemaVersion, config, state, @@ -1578,6 +1588,10 @@ export class LGraph implements LinkNetwork, Serialisable { if (!data) return if (!keep_old) this.clear() + // Create a new graph ID if none is provided + if (data.id) this.id = data.id + else if (this.id === zeroUuid) this.id = createUuidv4() + let reroutes: SerialisableReroute[] | undefined // TODO: Determine whether this should this fall back to 0.4. @@ -1665,7 +1679,7 @@ export class LGraph implements LinkNetwork, Serialisable { // copy all stored fields for (const i in data) { // links must be accepted - if (["nodes", "groups", "links", "state", "reroutes", "floatingLinks"].includes(i)) { + if (["nodes", "groups", "links", "state", "reroutes", "floatingLinks", "id"].includes(i)) { continue } // @ts-expect-error #574 Legacy property assignment diff --git a/src/LiteGraphGlobal.ts b/src/LiteGraphGlobal.ts index b6db26c13..77ba08102 100644 --- a/src/LiteGraphGlobal.ts +++ b/src/LiteGraphGlobal.ts @@ -19,6 +19,7 @@ import { RenderShape, TitleMode, } from "./types/globalEnums" +import { createUuidv4 } from "./utils/uuid" /** * The Global Scope. It contains all the registered node classes. @@ -549,14 +550,8 @@ export class LiteGraphGlobal { return target } - /* - * https://gist.github.com/jed/982883?permalink_comment_id=852670#gistcomment-852670 - */ - uuidv4(): string { - // @ts-expect-error - return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replaceAll(/[018]/g, a => - (a ^ ((Math.random() * 16) >> (a / 4))).toString(16)) - } + /** @see {@link createUuidv4} @inheritdoc */ + uuidv4 = createUuidv4 /** * Returns if the types of two slots are compatible (taking into account wildcards, etc) diff --git a/src/types/serialisation.ts b/src/types/serialisation.ts index 404525971..2f250ec93 100644 --- a/src/types/serialisation.ts +++ b/src/types/serialisation.ts @@ -15,6 +15,7 @@ import type { LinkId, SerialisedLLinkArray } from "../LLink" import type { FloatingRerouteSlot, RerouteId } from "../Reroute" import type { TWidgetValue } from "../types/widgets" import type { RenderShape } from "./globalEnums" +import type { UUID } from "@/utils/uuid" /** * An object that implements custom pre-serialization logic via {@link Serialisable.asSerialisable}. @@ -29,6 +30,9 @@ export interface Serialisable { } export interface SerialisableGraph { + /** Unique graph ID. Automatically generated if not provided. */ + id: UUID + revision: number /** Schema version. @remarks Version bump should add to const union, which is used to narrow type during deserialise. */ version: 0 | 1 config: LGraphConfig @@ -80,6 +84,8 @@ export interface ISerialisedNode { * Maintained for backwards compat */ export interface ISerialisedGraph { + id: UUID + revision: number last_node_id: NodeId last_link_id: number nodes: ISerialisedNode[] diff --git a/src/utils/uuid.ts b/src/utils/uuid.ts new file mode 100644 index 000000000..157813e43 --- /dev/null +++ b/src/utils/uuid.ts @@ -0,0 +1,29 @@ +export type UUID = `${string}-${string}-${string}-${string}-${string}` + +/** Special-case zero-UUID, consisting entirely of zeros. Used as a default value. */ +export const zeroUuid = "00000000-0000-0000-0000-000000000000" + +/** Pre-allocated storage for uuid random values. */ +const randomStorage = new Uint32Array(31) + +/** + * Creates a UUIDv4 string. + * @returns A new UUIDv4 string + * @remarks + * Original implementation from https://gist.github.com/jed/982883?permalink_comment_id=852670#gistcomment-852670 + * + * Prefers the {@link crypto.randomUUID} method if available, falling back to + * {@link crypto.getRandomValues}, then finally the legacy {@link Math.random} method. + */ +export function createUuidv4(): UUID { + if (typeof crypto?.randomUUID === "function") return crypto.randomUUID() + // Assertion: `replaceAll` returns `string`; UUID format must be asserted below + if (typeof crypto?.getRandomValues === "function") { + const random = crypto.getRandomValues(randomStorage) + let i = 0 + return "10000000-1000-4000-8000-100000000000".replaceAll(/[018]/g, a => + (Number(a) ^ ((random[i++] * 3.725_290_298_461_914e-9) >> (Number(a) * 0.25))).toString(16)) as UUID + } + return "10000000-1000-4000-8000-100000000000".replaceAll(/[018]/g, a => + (Number(a) ^ ((Math.random() * 16) >> (Number(a) * 0.25))).toString(16)) as UUID +} diff --git a/test/__snapshots__/ConfigureGraph.test.ts.snap b/test/__snapshots__/ConfigureGraph.test.ts.snap index b6c0bc2cf..91d5ecf47 100644 --- a/test/__snapshots__/ConfigureGraph.test.ts.snap +++ b/test/__snapshots__/ConfigureGraph.test.ts.snap @@ -240,6 +240,7 @@ LGraph { "fixedtime": 0, "fixedtime_lapse": 0.01, "globaltime": 0, + "id": "ca9da7d8-fddd-4707-ad32-67be9be13140", "inputs": {}, "iteration": 0, "last_update_time": 0, @@ -249,6 +250,7 @@ LGraph { "nodes_executedAction": [], "nodes_executing": [], "outputs": {}, + "revision": 0, "runningtime": 0, "starttime": 0, "state": { @@ -285,6 +287,7 @@ LGraph { "fixedtime": 0, "fixedtime_lapse": 0.01, "globaltime": 0, + "id": "d175890f-716a-4ece-ba33-1d17a513b7be", "inputs": {}, "iteration": 0, "last_update_time": 0, @@ -294,6 +297,7 @@ LGraph { "nodes_executedAction": [], "nodes_executing": [], "outputs": {}, + "revision": 0, "runningtime": 0, "starttime": 0, "state": { diff --git a/test/__snapshots__/LGraph.test.ts.snap b/test/__snapshots__/LGraph.test.ts.snap index 84aea52ad..567bed734 100644 --- a/test/__snapshots__/LGraph.test.ts.snap +++ b/test/__snapshots__/LGraph.test.ts.snap @@ -246,6 +246,7 @@ LGraph { "fixedtime": 0, "fixedtime_lapse": 0.01, "globaltime": 0, + "id": "b4e984f1-b421-4d24-b8b4-ff895793af13", "inputs": {}, "iteration": 0, "last_update_time": 0, @@ -255,6 +256,7 @@ LGraph { "nodes_executedAction": [], "nodes_executing": [], "outputs": {}, + "revision": 0, "runningtime": 0, "starttime": 0, "state": { diff --git a/test/__snapshots__/LGraph_constructor.test.ts.snap b/test/__snapshots__/LGraph_constructor.test.ts.snap index 80151bf91..d71d797f6 100644 --- a/test/__snapshots__/LGraph_constructor.test.ts.snap +++ b/test/__snapshots__/LGraph_constructor.test.ts.snap @@ -240,6 +240,7 @@ LGraph { "fixedtime": 0, "fixedtime_lapse": 0.01, "globaltime": 0, + "id": "ca9da7d8-fddd-4707-ad32-67be9be13140", "inputs": {}, "iteration": 0, "last_update_time": 0, @@ -249,6 +250,7 @@ LGraph { "nodes_executedAction": [], "nodes_executing": [], "outputs": {}, + "revision": 0, "runningtime": 0, "starttime": 0, "state": { @@ -285,6 +287,7 @@ LGraph { "fixedtime": 0, "fixedtime_lapse": 0.01, "globaltime": 0, + "id": "d175890f-716a-4ece-ba33-1d17a513b7be", "inputs": {}, "iteration": 0, "last_update_time": 0, @@ -294,6 +297,7 @@ LGraph { "nodes_executedAction": [], "nodes_executing": [], "outputs": {}, + "revision": 0, "runningtime": 0, "starttime": 0, "state": { diff --git a/test/__snapshots__/litegraph.test.ts.snap b/test/__snapshots__/litegraph.test.ts.snap index 5e3fe4727..d79354ad6 100644 --- a/test/__snapshots__/litegraph.test.ts.snap +++ b/test/__snapshots__/litegraph.test.ts.snap @@ -179,5 +179,6 @@ LiteGraphGlobal { "throw_errors": true, "use_legacy_node_error_indicator": false, "use_uuids": false, + "uuidv4": [Function], } `; diff --git a/test/assets/testGraphs.ts b/test/assets/testGraphs.ts index d7601aad7..a4e57671c 100644 --- a/test/assets/testGraphs.ts +++ b/test/assets/testGraphs.ts @@ -1,6 +1,8 @@ import type { ISerialisedGraph, SerialisableGraph } from "@/litegraph" export const oldSchemaGraph: ISerialisedGraph = { + id: "b4e984f1-b421-4d24-b8b4-ff895793af13", + revision: 0, version: 0.4, config: {}, last_node_id: 0, @@ -23,6 +25,8 @@ export const oldSchemaGraph: ISerialisedGraph = { } export const minimalSerialisableGraph: SerialisableGraph = { + id: "d175890f-716a-4ece-ba33-1d17a513b7be", + revision: 0, version: 1, config: {}, state: { @@ -37,6 +41,8 @@ export const minimalSerialisableGraph: SerialisableGraph = { } export const basicSerialisableGraph: SerialisableGraph = { + id: "ca9da7d8-fddd-4707-ad32-67be9be13140", + revision: 0, version: 1, config: {}, state: {