@stocksharp/diagram 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE +30 -0
  2. package/NOTICE +21 -0
  3. package/README.md +346 -0
  4. package/dist/esm/canvas-renderer.js +3226 -0
  5. package/dist/esm/canvas-renderer.js.map +1 -0
  6. package/dist/esm/core/action-registry.js +35 -0
  7. package/dist/esm/core/action-registry.js.map +1 -0
  8. package/dist/esm/core/document.js +350 -0
  9. package/dist/esm/core/document.js.map +1 -0
  10. package/dist/esm/core/history.js +136 -0
  11. package/dist/esm/core/history.js.map +1 -0
  12. package/dist/esm/core/model.js +2 -0
  13. package/dist/esm/core/model.js.map +1 -0
  14. package/dist/esm/core/state.js +73 -0
  15. package/dist/esm/core/state.js.map +1 -0
  16. package/dist/esm/core/view-state.js +70 -0
  17. package/dist/esm/core/view-state.js.map +1 -0
  18. package/dist/esm/diagram/api.js +2 -0
  19. package/dist/esm/diagram/api.js.map +1 -0
  20. package/dist/esm/diagram/catalog.js +45 -0
  21. package/dist/esm/diagram/catalog.js.map +1 -0
  22. package/dist/esm/diagram/event-emitter.js +42 -0
  23. package/dist/esm/diagram/event-emitter.js.map +1 -0
  24. package/dist/esm/diagram/palette.js +269 -0
  25. package/dist/esm/diagram/palette.js.map +1 -0
  26. package/dist/esm/diagram/stocksharp-diagram.js +784 -0
  27. package/dist/esm/diagram/stocksharp-diagram.js.map +1 -0
  28. package/dist/esm/diagram/types.js +105 -0
  29. package/dist/esm/diagram/types.js.map +1 -0
  30. package/dist/esm/embed.js +398 -0
  31. package/dist/esm/embed.js.map +1 -0
  32. package/dist/esm/i18n.js +12 -0
  33. package/dist/esm/i18n.js.map +1 -0
  34. package/dist/esm/index.js +12 -0
  35. package/dist/esm/index.js.map +1 -0
  36. package/dist/ssdiagram.js +5141 -0
  37. package/dist/ssdiagram.js.map +7 -0
  38. package/dist/types/canvas-renderer.d.ts +512 -0
  39. package/dist/types/canvas-renderer.d.ts.map +1 -0
  40. package/dist/types/core/action-registry.d.ts +18 -0
  41. package/dist/types/core/action-registry.d.ts.map +1 -0
  42. package/dist/types/core/document.d.ts +12 -0
  43. package/dist/types/core/document.d.ts.map +1 -0
  44. package/dist/types/core/history.d.ts +33 -0
  45. package/dist/types/core/history.d.ts.map +1 -0
  46. package/dist/types/core/model.d.ts +85 -0
  47. package/dist/types/core/model.d.ts.map +1 -0
  48. package/dist/types/core/state.d.ts +68 -0
  49. package/dist/types/core/state.d.ts.map +1 -0
  50. package/dist/types/core/view-state.d.ts +14 -0
  51. package/dist/types/core/view-state.d.ts.map +1 -0
  52. package/dist/types/diagram/api.d.ts +213 -0
  53. package/dist/types/diagram/api.d.ts.map +1 -0
  54. package/dist/types/diagram/catalog.d.ts +19 -0
  55. package/dist/types/diagram/catalog.d.ts.map +1 -0
  56. package/dist/types/diagram/event-emitter.d.ts +9 -0
  57. package/dist/types/diagram/event-emitter.d.ts.map +1 -0
  58. package/dist/types/diagram/palette.d.ts +63 -0
  59. package/dist/types/diagram/palette.d.ts.map +1 -0
  60. package/dist/types/diagram/stocksharp-diagram.d.ts +157 -0
  61. package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -0
  62. package/dist/types/diagram/types.d.ts +171 -0
  63. package/dist/types/diagram/types.d.ts.map +1 -0
  64. package/dist/types/embed.d.ts +35 -0
  65. package/dist/types/embed.d.ts.map +1 -0
  66. package/dist/types/i18n.d.ts +167 -0
  67. package/dist/types/i18n.d.ts.map +1 -0
  68. package/dist/types/index.d.ts +22 -0
  69. package/dist/types/index.d.ts.map +1 -0
  70. package/package.json +126 -0
  71. package/sample.png +0 -0
  72. package/src/canvas-renderer.ts +3249 -0
  73. package/src/core/action-registry.ts +46 -0
  74. package/src/core/document.ts +387 -0
  75. package/src/core/history.ts +154 -0
  76. package/src/core/model.ts +99 -0
  77. package/src/core/state.ts +148 -0
  78. package/src/core/view-state.ts +82 -0
  79. package/src/diagram/api.ts +257 -0
  80. package/src/diagram/catalog.ts +55 -0
  81. package/src/diagram/event-emitter.ts +44 -0
  82. package/src/diagram/palette.ts +312 -0
  83. package/src/diagram/stocksharp-diagram.ts +945 -0
  84. package/src/diagram/types.ts +332 -0
  85. package/src/embed.ts +508 -0
  86. package/src/i18n.ts +210 -0
  87. package/src/index.ts +171 -0
@@ -0,0 +1,46 @@
1
+ export interface DiagramAction<TId extends string, TContext> {
2
+ readonly id: TId;
3
+ canExecute(context: TContext): boolean;
4
+ execute(context: TContext): void;
5
+ }
6
+
7
+ export interface DiagramActionState<TId extends string> {
8
+ id: TId;
9
+ enabled: boolean;
10
+ }
11
+
12
+ export class DiagramActionRegistry<TId extends string, TContext> {
13
+ private readonly actions = new Map<TId, DiagramAction<TId, TContext>>();
14
+
15
+ register(action: DiagramAction<TId, TContext>): () => void {
16
+ if (this.actions.has(action.id)) {
17
+ throw new Error(`Diagram action "${action.id}" is already registered.`);
18
+ }
19
+ this.actions.set(action.id, action);
20
+ return () => {
21
+ if (this.actions.get(action.id) === action) this.actions.delete(action.id);
22
+ };
23
+ }
24
+
25
+ get(id: TId): DiagramAction<TId, TContext> | null {
26
+ return this.actions.get(id) ?? null;
27
+ }
28
+
29
+ states(context: TContext): DiagramActionState<TId>[] {
30
+ return [...this.actions.values()].map((action) => ({
31
+ id: action.id,
32
+ enabled: action.canExecute(context),
33
+ }));
34
+ }
35
+
36
+ canExecute(id: TId, context: TContext): boolean {
37
+ return this.actions.get(id)?.canExecute(context) ?? false;
38
+ }
39
+
40
+ execute(id: TId, context: TContext): boolean {
41
+ const action = this.actions.get(id);
42
+ if (action === undefined || !action.canExecute(context)) return false;
43
+ action.execute(context);
44
+ return true;
45
+ }
46
+ }
@@ -0,0 +1,387 @@
1
+ import {
2
+ DIAGRAM_DOCUMENT_VERSION,
3
+ type DiagramDocument,
4
+ type DiagramDocumentEndpoint,
5
+ type DiagramDocumentInput,
6
+ type DiagramDocumentLink,
7
+ type DiagramDocumentLinkInput,
8
+ type DiagramDocumentNode,
9
+ type DiagramDocumentNodeInput,
10
+ type DiagramDocumentPort,
11
+ type DiagramDocumentPortInput,
12
+ type DiagramParameterSchema,
13
+ type JsonObject,
14
+ type JsonValue,
15
+ } from './model.js';
16
+
17
+ export { DIAGRAM_DOCUMENT_VERSION } from './model.js';
18
+ export type {
19
+ DiagramDocument,
20
+ DiagramDocumentEndpoint,
21
+ DiagramDocumentInput,
22
+ DiagramDocumentLink,
23
+ DiagramDocumentLinkInput,
24
+ DiagramDocumentNode,
25
+ DiagramDocumentNodeInput,
26
+ DiagramDocumentPort,
27
+ DiagramDocumentPortInput,
28
+ DiagramDocumentVersion,
29
+ DiagramParameterSchema,
30
+ JsonObject,
31
+ JsonPrimitive,
32
+ JsonValue,
33
+ } from './model.js';
34
+
35
+ export class DiagramDocumentError extends Error {
36
+ constructor(message: string, readonly path: string = '$') {
37
+ super(`${path}: ${message}`);
38
+ this.name = 'DiagramDocumentError';
39
+ }
40
+ }
41
+
42
+ export function createDiagramDocument(input: DiagramDocumentInput = {}): DiagramDocument {
43
+ const nodes = (input.nodes ?? []).map((node, index) => normalizeNode(node, `$.nodes[${index}]`));
44
+ const usedLinkIds = new Set<string>();
45
+
46
+ for (let index = 0; index < (input.links ?? []).length; index++) {
47
+ const id = input.links?.[index].id;
48
+ if (id === undefined) continue;
49
+ requireIdentifier(id, `$.links[${index}].id`);
50
+ if (usedLinkIds.has(id)) {
51
+ throw new DiagramDocumentError(`duplicate link id "${id}"`, `$.links[${index}].id`);
52
+ }
53
+ usedLinkIds.add(id);
54
+ }
55
+
56
+ let sequence = 1;
57
+ const links = (input.links ?? []).map((link, index) => {
58
+ let id = link.id;
59
+ if (id === undefined) {
60
+ do id = `link_${sequence++}`;
61
+ while (usedLinkIds.has(id));
62
+ usedLinkIds.add(id);
63
+ }
64
+ return normalizeLink({ ...link, id }, `$.links[${index}]`);
65
+ });
66
+ const document: DiagramDocument = {
67
+ version: DIAGRAM_DOCUMENT_VERSION,
68
+ nodes,
69
+ links,
70
+ metadata: cloneJsonObject(input.metadata ?? {}, '$.metadata'),
71
+ };
72
+ validateDocument(document);
73
+ return document;
74
+ }
75
+
76
+ export function cloneDiagramDocument(document: DiagramDocument): DiagramDocument {
77
+ return parseDiagramDocument(document);
78
+ }
79
+
80
+ export function serializeDiagramDocument(document: DiagramDocument, space?: number): string {
81
+ return JSON.stringify(parseDiagramDocument(document), null, space);
82
+ }
83
+
84
+ export function parseDiagramDocument(source: string | unknown): DiagramDocument {
85
+ let value: unknown = source;
86
+ if (typeof source === 'string') {
87
+ try {
88
+ value = JSON.parse(source) as unknown;
89
+ } catch (error) {
90
+ const reason = error instanceof Error ? error.message : 'invalid JSON';
91
+ throw new DiagramDocumentError(reason);
92
+ }
93
+ }
94
+
95
+ const root = requireObject(value, '$');
96
+ const version = requireNumber(root.version, '$.version');
97
+ if (version !== DIAGRAM_DOCUMENT_VERSION) {
98
+ throw new DiagramDocumentError(`unsupported document version ${version}`, '$.version');
99
+ }
100
+ const document: DiagramDocument = {
101
+ version: DIAGRAM_DOCUMENT_VERSION,
102
+ nodes: requireArray(root.nodes, '$.nodes').map((node, index) => parseNode(node, `$.nodes[${index}]`)),
103
+ links: requireArray(root.links, '$.links').map((link, index) => parseLink(link, `$.links[${index}]`)),
104
+ metadata: cloneJsonObject(root.metadata, '$.metadata'),
105
+ };
106
+ validateDocument(document);
107
+ return document;
108
+ }
109
+
110
+ function normalizeNode(input: DiagramDocumentNodeInput, path: string): DiagramDocumentNode {
111
+ const id = requireIdentifier(input.id, `${path}.id`);
112
+ return {
113
+ id,
114
+ typeId: requireIdentifier(input.typeId ?? id, `${path}.typeId`),
115
+ name: requireString(input.name, `${path}.name`),
116
+ description: requireString(input.description ?? '', `${path}.description`),
117
+ groupName: requireString(input.groupName ?? 'Common', `${path}.groupName`),
118
+ x: requireFiniteNumber(input.x ?? 0, `${path}.x`),
119
+ y: requireFiniteNumber(input.y ?? 0, `${path}.y`),
120
+ color: requireString(input.color ?? '#d7d7d7', `${path}.color`),
121
+ border: requireString(input.border ?? '#8c8c8c', `${path}.border`),
122
+ icon: requireString(input.icon ?? '', `${path}.icon`),
123
+ message: requireString(input.message ?? '', `${path}.message`),
124
+ openAction: requireString(input.openAction ?? '', `${path}.openAction`),
125
+ inPorts: (input.inPorts ?? []).map((port, index) => normalizePort(port, `${path}.inPorts[${index}]`)),
126
+ outPorts: (input.outPorts ?? []).map((port, index) => normalizePort(port, `${path}.outPorts[${index}]`)),
127
+ parameters: (input.parameters ?? []).map((parameter, index) => normalizeParameter(parameter, `${path}.parameters[${index}]`)),
128
+ paramValues: cloneStringRecord(input.paramValues ?? {}, `${path}.paramValues`),
129
+ metadata: cloneJsonObject(input.metadata ?? {}, `${path}.metadata`),
130
+ };
131
+ }
132
+
133
+ function normalizePort(input: DiagramDocumentPortInput, path: string): DiagramDocumentPort {
134
+ return {
135
+ id: requireIdentifier(input.id, `${path}.id`),
136
+ name: requireString(input.name, `${path}.name`),
137
+ description: requireString(input.description ?? '', `${path}.description`),
138
+ type: requireString(input.type ?? '', `${path}.type`),
139
+ maxLinks: requireNonNegativeInteger(input.maxLinks ?? 0, `${path}.maxLinks`),
140
+ availableTypes: (input.availableTypes ?? []).map((type, index) => requireString(type, `${path}.availableTypes[${index}]`)),
141
+ isDynamic: requireBoolean(input.isDynamic ?? false, `${path}.isDynamic`),
142
+ dynamicMode: requireString(input.dynamicMode ?? '', `${path}.dynamicMode`),
143
+ isSibling: requireBoolean(input.isSibling ?? false, `${path}.isSibling`),
144
+ metadata: cloneJsonObject(input.metadata ?? {}, `${path}.metadata`),
145
+ };
146
+ }
147
+
148
+ function normalizeParameter(input: DiagramParameterSchema, path: string): DiagramParameterSchema {
149
+ return {
150
+ name: requireIdentifier(input.name, `${path}.name`),
151
+ displayName: requireString(input.displayName, `${path}.displayName`),
152
+ description: requireString(input.description, `${path}.description`),
153
+ type: requireString(input.type, `${path}.type`),
154
+ defaultValue: requireString(input.defaultValue, `${path}.defaultValue`),
155
+ options: requireArray(input.options, `${path}.options`).map((option, index) => requireString(option, `${path}.options[${index}]`)),
156
+ min: requireNullableFiniteNumber(input.min, `${path}.min`),
157
+ max: requireNullableFiniteNumber(input.max, `${path}.max`),
158
+ displayOrder: requireFiniteNumber(input.displayOrder, `${path}.displayOrder`),
159
+ category: requireString(input.category, `${path}.category`),
160
+ isBasic: requireBoolean(input.isBasic, `${path}.isBasic`),
161
+ editorType: requireString(input.editorType, `${path}.editorType`),
162
+ };
163
+ }
164
+
165
+ function normalizeLink(input: DiagramDocumentLinkInput & { id: string }, path: string): DiagramDocumentLink {
166
+ return {
167
+ id: requireIdentifier(input.id, `${path}.id`),
168
+ from: normalizeEndpoint(input.from, `${path}.from`),
169
+ to: normalizeEndpoint(input.to, `${path}.to`),
170
+ metadata: cloneJsonObject(input.metadata ?? {}, `${path}.metadata`),
171
+ };
172
+ }
173
+
174
+ function normalizeEndpoint(input: DiagramDocumentEndpoint, path: string): DiagramDocumentEndpoint {
175
+ return {
176
+ nodeId: requireIdentifier(input.nodeId, `${path}.nodeId`),
177
+ portId: requireIdentifier(input.portId, `${path}.portId`),
178
+ };
179
+ }
180
+
181
+ function parseNode(value: unknown, path: string): DiagramDocumentNode {
182
+ const node = requireObject(value, path);
183
+ return normalizeNode({
184
+ id: requireString(node.id, `${path}.id`),
185
+ typeId: requireString(node.typeId, `${path}.typeId`),
186
+ name: requireString(node.name, `${path}.name`),
187
+ description: requireString(node.description, `${path}.description`),
188
+ groupName: requireString(node.groupName, `${path}.groupName`),
189
+ x: requireNumber(node.x, `${path}.x`),
190
+ y: requireNumber(node.y, `${path}.y`),
191
+ color: requireString(node.color, `${path}.color`),
192
+ border: requireString(node.border, `${path}.border`),
193
+ icon: requireString(node.icon, `${path}.icon`),
194
+ message: requireString(node.message, `${path}.message`),
195
+ openAction: requireString(node.openAction, `${path}.openAction`),
196
+ inPorts: requireArray(node.inPorts, `${path}.inPorts`).map((port, index) => parsePort(port, `${path}.inPorts[${index}]`)),
197
+ outPorts: requireArray(node.outPorts, `${path}.outPorts`).map((port, index) => parsePort(port, `${path}.outPorts[${index}]`)),
198
+ parameters: requireArray(node.parameters, `${path}.parameters`).map((parameter, index) => parseParameter(parameter, `${path}.parameters[${index}]`)),
199
+ paramValues: cloneStringRecord(node.paramValues, `${path}.paramValues`),
200
+ metadata: cloneJsonObject(node.metadata, `${path}.metadata`),
201
+ }, path);
202
+ }
203
+
204
+ function parsePort(value: unknown, path: string): DiagramDocumentPort {
205
+ const port = requireObject(value, path);
206
+ return normalizePort({
207
+ id: requireString(port.id, `${path}.id`),
208
+ name: requireString(port.name, `${path}.name`),
209
+ description: requireString(port.description, `${path}.description`),
210
+ type: requireString(port.type, `${path}.type`),
211
+ maxLinks: requireNumber(port.maxLinks, `${path}.maxLinks`),
212
+ availableTypes: requireArray(port.availableTypes, `${path}.availableTypes`).map((type, index) => requireString(type, `${path}.availableTypes[${index}]`)),
213
+ isDynamic: requireBoolean(port.isDynamic, `${path}.isDynamic`),
214
+ dynamicMode: requireString(port.dynamicMode, `${path}.dynamicMode`),
215
+ isSibling: requireBoolean(port.isSibling, `${path}.isSibling`),
216
+ metadata: cloneJsonObject(port.metadata, `${path}.metadata`),
217
+ }, path);
218
+ }
219
+
220
+ function parseParameter(value: unknown, path: string): DiagramParameterSchema {
221
+ const parameter = requireObject(value, path);
222
+ return normalizeParameter({
223
+ name: requireString(parameter.name, `${path}.name`),
224
+ displayName: requireString(parameter.displayName, `${path}.displayName`),
225
+ description: requireString(parameter.description, `${path}.description`),
226
+ type: requireString(parameter.type, `${path}.type`),
227
+ defaultValue: requireString(parameter.defaultValue, `${path}.defaultValue`),
228
+ options: requireArray(parameter.options, `${path}.options`).map((option, index) => requireString(option, `${path}.options[${index}]`)),
229
+ min: requireNullableFiniteNumber(parameter.min, `${path}.min`),
230
+ max: requireNullableFiniteNumber(parameter.max, `${path}.max`),
231
+ displayOrder: requireNumber(parameter.displayOrder, `${path}.displayOrder`),
232
+ category: requireString(parameter.category, `${path}.category`),
233
+ isBasic: requireBoolean(parameter.isBasic, `${path}.isBasic`),
234
+ editorType: requireString(parameter.editorType, `${path}.editorType`),
235
+ }, path);
236
+ }
237
+
238
+ function parseLink(value: unknown, path: string): DiagramDocumentLink {
239
+ const link = requireObject(value, path);
240
+ return normalizeLink({
241
+ id: requireString(link.id, `${path}.id`),
242
+ from: parseEndpoint(link.from, `${path}.from`),
243
+ to: parseEndpoint(link.to, `${path}.to`),
244
+ metadata: cloneJsonObject(link.metadata, `${path}.metadata`),
245
+ }, path);
246
+ }
247
+
248
+ function parseEndpoint(value: unknown, path: string): DiagramDocumentEndpoint {
249
+ const endpoint = requireObject(value, path);
250
+ return {
251
+ nodeId: requireIdentifier(endpoint.nodeId, `${path}.nodeId`),
252
+ portId: requireIdentifier(endpoint.portId, `${path}.portId`),
253
+ };
254
+ }
255
+
256
+ function validateDocument(document: DiagramDocument): void {
257
+ const nodes = new Map<string, DiagramDocumentNode>();
258
+ for (let index = 0; index < document.nodes.length; index++) {
259
+ const node = document.nodes[index];
260
+ if (nodes.has(node.id)) throw new DiagramDocumentError(`duplicate node id "${node.id}"`, `$.nodes[${index}].id`);
261
+ validateUniquePorts(node.inPorts, `$.nodes[${index}].inPorts`);
262
+ validateUniquePorts(node.outPorts, `$.nodes[${index}].outPorts`);
263
+ nodes.set(node.id, node);
264
+ }
265
+
266
+ const linkIds = new Set<string>();
267
+ const endpoints = new Set<string>();
268
+ for (let index = 0; index < document.links.length; index++) {
269
+ const link = document.links[index];
270
+ const path = `$.links[${index}]`;
271
+ if (linkIds.has(link.id)) throw new DiagramDocumentError(`duplicate link id "${link.id}"`, `${path}.id`);
272
+ linkIds.add(link.id);
273
+ const fromNode = nodes.get(link.from.nodeId);
274
+ const toNode = nodes.get(link.to.nodeId);
275
+ if (fromNode === undefined) throw new DiagramDocumentError(`unknown source node "${link.from.nodeId}"`, `${path}.from.nodeId`);
276
+ if (toNode === undefined) throw new DiagramDocumentError(`unknown target node "${link.to.nodeId}"`, `${path}.to.nodeId`);
277
+ if (!fromNode.outPorts.some((port) => port.id === link.from.portId)) {
278
+ throw new DiagramDocumentError(`unknown output port "${link.from.portId}"`, `${path}.from.portId`);
279
+ }
280
+ if (!toNode.inPorts.some((port) => port.id === link.to.portId)) {
281
+ throw new DiagramDocumentError(`unknown input port "${link.to.portId}"`, `${path}.to.portId`);
282
+ }
283
+ const key = `${link.from.nodeId}\u0000${link.from.portId}\u0000${link.to.nodeId}\u0000${link.to.portId}`;
284
+ if (endpoints.has(key)) throw new DiagramDocumentError('duplicate link endpoints', path);
285
+ endpoints.add(key);
286
+ }
287
+ }
288
+
289
+ function validateUniquePorts(ports: readonly DiagramDocumentPort[], path: string): void {
290
+ const ids = new Set<string>();
291
+ for (let index = 0; index < ports.length; index++) {
292
+ const id = ports[index].id;
293
+ if (ids.has(id)) throw new DiagramDocumentError(`duplicate port id "${id}"`, `${path}[${index}].id`);
294
+ ids.add(id);
295
+ }
296
+ }
297
+
298
+ function cloneStringRecord(value: unknown, path: string): Record<string, string> {
299
+ const object = requireObject(value, path);
300
+ const result: Record<string, string> = {};
301
+ for (const [key, item] of Object.entries(object)) result[key] = requireString(item, `${path}.${key}`);
302
+ return result;
303
+ }
304
+
305
+ function cloneJsonObject(value: unknown, path: string, ancestors = new WeakSet<object>()): JsonObject {
306
+ const object = requireObject(value, path);
307
+ if (ancestors.has(object)) throw new DiagramDocumentError('circular JSON value', path);
308
+ ancestors.add(object);
309
+ const result: JsonObject = {};
310
+ try {
311
+ for (const [key, item] of Object.entries(object)) {
312
+ result[key] = cloneJsonValue(item, `${path}.${key}`, ancestors);
313
+ }
314
+ } finally {
315
+ ancestors.delete(object);
316
+ }
317
+ return result;
318
+ }
319
+
320
+ function cloneJsonValue(value: unknown, path: string, ancestors: WeakSet<object>): JsonValue {
321
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
322
+ if (typeof value === 'number') return requireFiniteNumber(value, path);
323
+ if (Array.isArray(value)) {
324
+ if (ancestors.has(value)) throw new DiagramDocumentError('circular JSON value', path);
325
+ ancestors.add(value);
326
+ try {
327
+ return value.map((item, index) => cloneJsonValue(item, `${path}[${index}]`, ancestors));
328
+ } finally {
329
+ ancestors.delete(value);
330
+ }
331
+ }
332
+ if (isObject(value)) return cloneJsonObject(value, path, ancestors);
333
+ throw new DiagramDocumentError('expected a JSON value', path);
334
+ }
335
+
336
+ function requireObject(value: unknown, path: string): Record<string, unknown> {
337
+ if (!isObject(value)) throw new DiagramDocumentError('expected an object', path);
338
+ return value;
339
+ }
340
+
341
+ function isObject(value: unknown): value is Record<string, unknown> {
342
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
343
+ const prototype = Object.getPrototypeOf(value) as object | null;
344
+ return prototype === Object.prototype || prototype === null;
345
+ }
346
+
347
+ function requireArray(value: unknown, path: string): unknown[] {
348
+ if (!Array.isArray(value)) throw new DiagramDocumentError('expected an array', path);
349
+ return value;
350
+ }
351
+
352
+ function requireString(value: unknown, path: string): string {
353
+ if (typeof value !== 'string') throw new DiagramDocumentError('expected a string', path);
354
+ return value;
355
+ }
356
+
357
+ function requireIdentifier(value: unknown, path: string): string {
358
+ const id = requireString(value, path);
359
+ if (id.trim().length === 0) throw new DiagramDocumentError('identifier cannot be empty', path);
360
+ return id;
361
+ }
362
+
363
+ function requireNumber(value: unknown, path: string): number {
364
+ if (typeof value !== 'number') throw new DiagramDocumentError('expected a number', path);
365
+ return value;
366
+ }
367
+
368
+ function requireFiniteNumber(value: unknown, path: string): number {
369
+ const number = requireNumber(value, path);
370
+ if (!Number.isFinite(number)) throw new DiagramDocumentError('expected a finite number', path);
371
+ return number;
372
+ }
373
+
374
+ function requireNullableFiniteNumber(value: unknown, path: string): number | null {
375
+ return value === null ? null : requireFiniteNumber(value, path);
376
+ }
377
+
378
+ function requireNonNegativeInteger(value: unknown, path: string): number {
379
+ const number = requireFiniteNumber(value, path);
380
+ if (!Number.isInteger(number) || number < 0) throw new DiagramDocumentError('expected a non-negative integer', path);
381
+ return number;
382
+ }
383
+
384
+ function requireBoolean(value: unknown, path: string): boolean {
385
+ if (typeof value !== 'boolean') throw new DiagramDocumentError('expected a boolean', path);
386
+ return value;
387
+ }
@@ -0,0 +1,154 @@
1
+ export interface DiagramCommand {
2
+ readonly label: string;
3
+ execute(): void;
4
+ undo(): void;
5
+ }
6
+
7
+ export interface DiagramHistoryState {
8
+ canUndo: boolean;
9
+ canRedo: boolean;
10
+ undoDepth: number;
11
+ redoDepth: number;
12
+ undoLabel: string | null;
13
+ redoLabel: string | null;
14
+ }
15
+
16
+ export type DiagramHistoryListener = (state: DiagramHistoryState) => void;
17
+
18
+ interface TransactionFrame {
19
+ label: string;
20
+ commands: DiagramCommand[];
21
+ }
22
+
23
+ export class DiagramCommandHistory {
24
+ private readonly undoCommands: DiagramCommand[] = [];
25
+ private readonly redoCommands: DiagramCommand[] = [];
26
+ private readonly transactions: TransactionFrame[] = [];
27
+ private replaying = false;
28
+
29
+ constructor(private readonly listener?: DiagramHistoryListener) {}
30
+
31
+ get state(): DiagramHistoryState {
32
+ return {
33
+ canUndo: this.undoCommands.length > 0,
34
+ canRedo: this.redoCommands.length > 0,
35
+ undoDepth: this.undoCommands.length,
36
+ redoDepth: this.redoCommands.length,
37
+ undoLabel: this.undoCommands[this.undoCommands.length - 1]?.label ?? null,
38
+ redoLabel: this.redoCommands[this.redoCommands.length - 1]?.label ?? null,
39
+ };
40
+ }
41
+
42
+ execute(command: DiagramCommand): void {
43
+ if (this.replaying) {
44
+ command.execute();
45
+ return;
46
+ }
47
+ command.execute();
48
+ this.recordApplied(command);
49
+ }
50
+
51
+ /** Records a gesture that already changed the document, such as pointer drag. */
52
+ recordApplied(command: DiagramCommand): void {
53
+ if (this.replaying) return;
54
+ const transaction = this.transactions[this.transactions.length - 1];
55
+ if (transaction !== undefined) {
56
+ transaction.commands.push(command);
57
+ return;
58
+ }
59
+ this.undoCommands.push(command);
60
+ this.redoCommands.length = 0;
61
+ this.notify();
62
+ }
63
+
64
+ transaction<T>(label: string, action: () => T): T {
65
+ const frame: TransactionFrame = { label, commands: [] };
66
+ this.transactions.push(frame);
67
+ try {
68
+ const result = action();
69
+ this.transactions.pop();
70
+ const command = compose(frame);
71
+ if (command !== null) this.recordApplied(command);
72
+ return result;
73
+ } catch (error) {
74
+ this.transactions.pop();
75
+ this.replay(() => {
76
+ for (const command of frame.commands.slice().reverse()) command.undo();
77
+ });
78
+ throw error;
79
+ }
80
+ }
81
+
82
+ undo(): boolean {
83
+ const command = this.undoCommands.pop();
84
+ if (command === undefined) return false;
85
+ try {
86
+ this.replay(() => command.undo());
87
+ } catch (error) {
88
+ this.undoCommands.push(command);
89
+ throw error;
90
+ }
91
+ this.redoCommands.push(command);
92
+ this.notify();
93
+ return true;
94
+ }
95
+
96
+ redo(): boolean {
97
+ const command = this.redoCommands.pop();
98
+ if (command === undefined) return false;
99
+ try {
100
+ this.replay(() => command.execute());
101
+ } catch (error) {
102
+ this.redoCommands.push(command);
103
+ throw error;
104
+ }
105
+ this.undoCommands.push(command);
106
+ this.notify();
107
+ return true;
108
+ }
109
+
110
+ clear(): void {
111
+ if (this.transactions.length > 0) {
112
+ throw new Error('Cannot clear command history inside a transaction.');
113
+ }
114
+ this.undoCommands.length = 0;
115
+ this.redoCommands.length = 0;
116
+ this.notify();
117
+ }
118
+
119
+ private replay(action: () => void): void {
120
+ const previous = this.replaying;
121
+ this.replaying = true;
122
+ try {
123
+ action();
124
+ } finally {
125
+ this.replaying = previous;
126
+ }
127
+ }
128
+
129
+ private notify(): void {
130
+ this.listener?.(this.state);
131
+ }
132
+ }
133
+
134
+ function compose(frame: TransactionFrame): DiagramCommand | null {
135
+ if (frame.commands.length === 0) return null;
136
+ if (frame.commands.length === 1) {
137
+ const command = frame.commands[0];
138
+ return frame.label === command.label ? command : {
139
+ label: frame.label,
140
+ execute: () => command.execute(),
141
+ undo: () => command.undo(),
142
+ };
143
+ }
144
+ const commands = [...frame.commands];
145
+ return {
146
+ label: frame.label,
147
+ execute: () => {
148
+ for (const command of commands) command.execute();
149
+ },
150
+ undo: () => {
151
+ for (const command of commands.slice().reverse()) command.undo();
152
+ },
153
+ };
154
+ }
@@ -0,0 +1,99 @@
1
+ export type JsonPrimitive = string | number | boolean | null;
2
+ export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
3
+
4
+ export interface JsonObject {
5
+ [key: string]: JsonValue;
6
+ }
7
+
8
+ export interface DiagramParameterSchema {
9
+ name: string;
10
+ displayName: string;
11
+ description: string;
12
+ type: string;
13
+ defaultValue: string;
14
+ options: string[];
15
+ min: number | null;
16
+ max: number | null;
17
+ displayOrder: number;
18
+ category: string;
19
+ isBasic: boolean;
20
+ editorType: string;
21
+ }
22
+
23
+ export interface DiagramDocumentPort {
24
+ id: string;
25
+ name: string;
26
+ description: string;
27
+ type: string;
28
+ maxLinks: number;
29
+ availableTypes: string[];
30
+ isDynamic: boolean;
31
+ dynamicMode: string;
32
+ isSibling: boolean;
33
+ metadata: JsonObject;
34
+ }
35
+
36
+ export interface DiagramDocumentNode {
37
+ id: string;
38
+ typeId: string;
39
+ name: string;
40
+ description: string;
41
+ groupName: string;
42
+ x: number;
43
+ y: number;
44
+ color: string;
45
+ border: string;
46
+ icon: string;
47
+ /** Persistent host text. Runtime and load errors belong to DiagramRuntimeState. */
48
+ message: string;
49
+ openAction: string;
50
+ inPorts: DiagramDocumentPort[];
51
+ outPorts: DiagramDocumentPort[];
52
+ parameters: DiagramParameterSchema[];
53
+ paramValues: Record<string, string>;
54
+ metadata: JsonObject;
55
+ }
56
+
57
+ export interface DiagramDocumentEndpoint {
58
+ nodeId: string;
59
+ portId: string;
60
+ }
61
+
62
+ export interface DiagramDocumentLink {
63
+ /** Stable identity used by selection, relinking and history. */
64
+ id: string;
65
+ from: DiagramDocumentEndpoint;
66
+ to: DiagramDocumentEndpoint;
67
+ metadata: JsonObject;
68
+ }
69
+
70
+ export const DIAGRAM_DOCUMENT_VERSION = 1 as const;
71
+ export type DiagramDocumentVersion = typeof DIAGRAM_DOCUMENT_VERSION;
72
+
73
+ export interface DiagramDocument {
74
+ version: DiagramDocumentVersion;
75
+ nodes: DiagramDocumentNode[];
76
+ links: DiagramDocumentLink[];
77
+ metadata: JsonObject;
78
+ }
79
+
80
+ export type DiagramDocumentPortInput = Pick<DiagramDocumentPort, 'id' | 'name'>
81
+ & Partial<Omit<DiagramDocumentPort, 'id' | 'name'>>;
82
+
83
+ export type DiagramDocumentNodeInput = Pick<DiagramDocumentNode, 'id' | 'name'>
84
+ & Partial<Omit<DiagramDocumentNode, 'id' | 'name' | 'inPorts' | 'outPorts'>>
85
+ & {
86
+ inPorts?: readonly DiagramDocumentPortInput[];
87
+ outPorts?: readonly DiagramDocumentPortInput[];
88
+ };
89
+
90
+ export type DiagramDocumentLinkInput = Omit<DiagramDocumentLink, 'id' | 'metadata'> & {
91
+ id?: string;
92
+ metadata?: JsonObject;
93
+ };
94
+
95
+ export interface DiagramDocumentInput {
96
+ nodes?: readonly DiagramDocumentNodeInput[];
97
+ links?: readonly DiagramDocumentLinkInput[];
98
+ metadata?: JsonObject;
99
+ }