Add graph ID creation / serialisation (#790)

- Updates UUIDv4 generator
- Adds a unique id & revision support to graphs
- `revision` to be incremented downstream (e.g. on save)
- `id` automatically assigned if not provided
This commit is contained in:
filtered
2025-03-17 05:41:08 +11:00
committed by GitHub
parent 3447ea1981
commit dbc605e4da
9 changed files with 71 additions and 10 deletions

View File

@@ -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<SerialisableGraph> {
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<LinkId, LLink> = new Map()
@@ -1513,6 +1519,8 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
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<SerialisableGraph> {
* It is intended for use with {@link structuredClone} or {@link JSON.stringify}.
*/
asSerialisable(options?: { sortNodes: boolean }): SerialisableGraph & Required<Pick<SerialisableGraph, "nodes" | "groups" | "extra">> {
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<SerialisableGraph> {
const reroutes = this.reroutes.size ? [...this.reroutes.values()].map(x => x.asSerialisable()) : undefined
const data: ReturnType<typeof this.asSerialisable> = {
id,
revision,
version: LGraph.serialisedSchemaVersion,
config,
state,
@@ -1578,6 +1588,10 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
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<SerialisableGraph> {
// 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

View File

@@ -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)

View File

@@ -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<SerialisableObject> {
}
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[]

29
src/utils/uuid.ts Normal file
View File

@@ -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
}

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -179,5 +179,6 @@ LiteGraphGlobal {
"throw_errors": true,
"use_legacy_node_error_indicator": false,
"use_uuids": false,
"uuidv4": [Function],
}
`;

View File

@@ -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: {