@stocksharp/diagram 0.2.3 → 0.3.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 (67) hide show
  1. package/dist/esm/canvas-renderer.js +28 -22
  2. package/dist/esm/canvas-renderer.js.map +1 -1
  3. package/dist/esm/core/action-registry.js +24 -0
  4. package/dist/esm/core/action-registry.js.map +1 -1
  5. package/dist/esm/core/document.js +85 -28
  6. package/dist/esm/core/document.js.map +1 -1
  7. package/dist/esm/core/json.js +17 -0
  8. package/dist/esm/core/json.js.map +1 -0
  9. package/dist/esm/core/model.js +4 -0
  10. package/dist/esm/core/model.js.map +1 -1
  11. package/dist/esm/core/state.js +16 -2
  12. package/dist/esm/core/state.js.map +1 -1
  13. package/dist/esm/diagram/catalog.js +17 -5
  14. package/dist/esm/diagram/catalog.js.map +1 -1
  15. package/dist/esm/diagram/event-emitter.js +10 -0
  16. package/dist/esm/diagram/event-emitter.js.map +1 -1
  17. package/dist/esm/diagram/stocksharp-diagram.js +58 -56
  18. package/dist/esm/diagram/stocksharp-diagram.js.map +1 -1
  19. package/dist/esm/diagram/types.js +2 -1
  20. package/dist/esm/diagram/types.js.map +1 -1
  21. package/dist/esm/embed.js +121 -38
  22. package/dist/esm/embed.js.map +1 -1
  23. package/dist/esm/index.js +1 -1
  24. package/dist/esm/index.js.map +1 -1
  25. package/dist/ssdiagram.js +297 -145
  26. package/dist/ssdiagram.js.map +4 -4
  27. package/dist/types/canvas-renderer.d.ts +3 -5
  28. package/dist/types/canvas-renderer.d.ts.map +1 -1
  29. package/dist/types/core/action-registry.d.ts +7 -0
  30. package/dist/types/core/action-registry.d.ts.map +1 -1
  31. package/dist/types/core/document.d.ts.map +1 -1
  32. package/dist/types/core/json.d.ts +11 -0
  33. package/dist/types/core/json.d.ts.map +1 -0
  34. package/dist/types/core/model.d.ts +10 -1
  35. package/dist/types/core/model.d.ts.map +1 -1
  36. package/dist/types/core/state.d.ts +24 -1
  37. package/dist/types/core/state.d.ts.map +1 -1
  38. package/dist/types/diagram/api.d.ts +6 -6
  39. package/dist/types/diagram/api.d.ts.map +1 -1
  40. package/dist/types/diagram/catalog.d.ts +2 -1
  41. package/dist/types/diagram/catalog.d.ts.map +1 -1
  42. package/dist/types/diagram/event-emitter.d.ts +6 -1
  43. package/dist/types/diagram/event-emitter.d.ts.map +1 -1
  44. package/dist/types/diagram/palette.d.ts +1 -1
  45. package/dist/types/diagram/palette.d.ts.map +1 -1
  46. package/dist/types/diagram/stocksharp-diagram.d.ts +29 -22
  47. package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -1
  48. package/dist/types/diagram/types.d.ts +4 -4
  49. package/dist/types/diagram/types.d.ts.map +1 -1
  50. package/dist/types/embed.d.ts.map +1 -1
  51. package/dist/types/index.d.ts +3 -3
  52. package/dist/types/index.d.ts.map +1 -1
  53. package/package.json +2 -2
  54. package/src/canvas-renderer.ts +26 -24
  55. package/src/core/action-registry.ts +25 -0
  56. package/src/core/document.ts +91 -31
  57. package/src/core/json.ts +16 -0
  58. package/src/core/model.ts +14 -1
  59. package/src/core/state.ts +41 -3
  60. package/src/diagram/api.ts +6 -5
  61. package/src/diagram/catalog.ts +18 -6
  62. package/src/diagram/event-emitter.ts +12 -1
  63. package/src/diagram/palette.ts +1 -1
  64. package/src/diagram/stocksharp-diagram.ts +79 -76
  65. package/src/diagram/types.ts +6 -5
  66. package/src/embed.ts +140 -39
  67. package/src/index.ts +4 -0
package/dist/ssdiagram.js CHANGED
@@ -37,6 +37,7 @@ var SSDiagram = (() => {
37
37
  StockSharpDiagram: () => StockSharpDiagram,
38
38
  StockSharpPalette: () => StockSharpPalette,
39
39
  cloneDiagramDocument: () => cloneDiagramDocument,
40
+ cloneDiagramNodeErrors: () => cloneDiagramNodeErrors,
40
41
  cloneDiagramRuntimeState: () => cloneDiagramRuntimeState,
41
42
  createDiagramDocument: () => createDiagramDocument,
42
43
  createDiagramNodeRuntimeState: () => createDiagramNodeRuntimeState,
@@ -58,7 +59,19 @@ var SSDiagram = (() => {
58
59
  serializeDiagramViewState: () => serializeDiagramViewState
59
60
  });
60
61
 
62
+ // src/core/json.ts
63
+ function setJsonKey(target, key, value) {
64
+ if (key === "__proto__") {
65
+ Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
66
+ return;
67
+ }
68
+ target[key] = value;
69
+ }
70
+
61
71
  // src/core/model.ts
72
+ function toPortDynamicMode(value) {
73
+ return value === "manual" || value === "onConnect" ? value : "";
74
+ }
62
75
  var DIAGRAM_DOCUMENT_VERSION = 1;
63
76
 
64
77
  // src/core/document.ts
@@ -70,25 +83,29 @@ var SSDiagram = (() => {
70
83
  }
71
84
  };
72
85
  function createDiagramDocument(input = {}) {
73
- const nodes = (input.nodes ?? []).map((node, index) => normalizeNode(node, `$.nodes[${index}]`));
86
+ const rawNodes = mapElements(input.nodes, "$.nodes");
87
+ const rawLinks = mapElements(input.links, "$.links");
88
+ const nodes = rawNodes.map((node, index) => normalizeNode(node, `$.nodes[${index}]`));
74
89
  const usedLinkIds = /* @__PURE__ */ new Set();
75
- for (let index = 0; index < (input.links ?? []).length; index++) {
76
- const id = input.links?.[index].id;
77
- if (id === void 0) continue;
78
- requireIdentifier(id, `$.links[${index}].id`);
90
+ for (let index = 0; index < rawLinks.length; index++) {
91
+ const raw = rawLinks[index].id;
92
+ if (raw === void 0) continue;
93
+ const id = requireIdentifier(raw, `$.links[${index}].id`);
79
94
  if (usedLinkIds.has(id)) {
80
95
  throw new DiagramDocumentError(`duplicate link id "${id}"`, `$.links[${index}].id`);
81
96
  }
82
97
  usedLinkIds.add(id);
83
98
  }
84
99
  let sequence = 1;
85
- const links = (input.links ?? []).map((link, index) => {
100
+ const links = rawLinks.map((link, index) => {
86
101
  let id = link.id;
87
102
  if (id === void 0) {
103
+ let generated;
88
104
  do
89
- id = `link_${sequence++}`;
90
- while (usedLinkIds.has(id));
91
- usedLinkIds.add(id);
105
+ generated = `link_${sequence++}`;
106
+ while (usedLinkIds.has(generated));
107
+ usedLinkIds.add(generated);
108
+ id = generated;
92
109
  }
93
110
  return normalizeLink({ ...link, id }, `$.links[${index}]`);
94
111
  });
@@ -117,7 +134,7 @@ var SSDiagram = (() => {
117
134
  throw new DiagramDocumentError(reason);
118
135
  }
119
136
  }
120
- const root = requireObject(value, "$");
137
+ const root = requireStructure(value, "$");
121
138
  const version = requireNumber(root.version, "$.version");
122
139
  if (version !== DIAGRAM_DOCUMENT_VERSION) {
123
140
  throw new DiagramDocumentError(`unsupported document version ${version}`, "$.version");
@@ -131,7 +148,8 @@ var SSDiagram = (() => {
131
148
  validateDocument(document2);
132
149
  return document2;
133
150
  }
134
- function normalizeNode(input, path) {
151
+ function normalizeNode(value, path) {
152
+ const input = requireStructure(value, path);
135
153
  const id = requireIdentifier(input.id, `${path}.id`);
136
154
  return {
137
155
  id,
@@ -146,28 +164,30 @@ var SSDiagram = (() => {
146
164
  icon: requireString(input.icon ?? "", `${path}.icon`),
147
165
  message: requireString(input.message ?? "", `${path}.message`),
148
166
  openAction: requireString(input.openAction ?? "", `${path}.openAction`),
149
- inPorts: (input.inPorts ?? []).map((port, index) => normalizePort(port, `${path}.inPorts[${index}]`)),
150
- outPorts: (input.outPorts ?? []).map((port, index) => normalizePort(port, `${path}.outPorts[${index}]`)),
151
- parameters: (input.parameters ?? []).map((parameter, index) => normalizeParameter(parameter, `${path}.parameters[${index}]`)),
167
+ inPorts: mapElements(input.inPorts, `${path}.inPorts`).map((port, index) => normalizePort(port, `${path}.inPorts[${index}]`)),
168
+ outPorts: mapElements(input.outPorts, `${path}.outPorts`).map((port, index) => normalizePort(port, `${path}.outPorts[${index}]`)),
169
+ parameters: mapElements(input.parameters, `${path}.parameters`).map((parameter, index) => normalizeParameter(parameter, `${path}.parameters[${index}]`)),
152
170
  paramValues: cloneStringRecord(input.paramValues ?? {}, `${path}.paramValues`),
153
171
  metadata: cloneJsonObject(input.metadata ?? {}, `${path}.metadata`)
154
172
  };
155
173
  }
156
- function normalizePort(input, path) {
174
+ function normalizePort(value, path) {
175
+ const input = requireStructure(value, path);
157
176
  return {
158
177
  id: requireIdentifier(input.id, `${path}.id`),
159
178
  name: requireString(input.name, `${path}.name`),
160
179
  description: requireString(input.description ?? "", `${path}.description`),
161
180
  type: requireString(input.type ?? "", `${path}.type`),
162
181
  maxLinks: requireNonNegativeInteger(input.maxLinks ?? 0, `${path}.maxLinks`),
163
- availableTypes: (input.availableTypes ?? []).map((type, index) => requireString(type, `${path}.availableTypes[${index}]`)),
182
+ availableTypes: requireArray(input.availableTypes ?? [], `${path}.availableTypes`).map((type, index) => requireString(type, `${path}.availableTypes[${index}]`)),
164
183
  isDynamic: requireBoolean(input.isDynamic ?? false, `${path}.isDynamic`),
165
- dynamicMode: requireString(input.dynamicMode ?? "", `${path}.dynamicMode`),
184
+ dynamicMode: requirePortDynamicMode(input.dynamicMode ?? "", `${path}.dynamicMode`),
166
185
  isSibling: requireBoolean(input.isSibling ?? false, `${path}.isSibling`),
167
186
  metadata: cloneJsonObject(input.metadata ?? {}, `${path}.metadata`)
168
187
  };
169
188
  }
170
- function normalizeParameter(input, path) {
189
+ function normalizeParameter(value, path) {
190
+ const input = requireStructure(value, path);
171
191
  return {
172
192
  name: requireIdentifier(input.name, `${path}.name`),
173
193
  displayName: requireString(input.displayName, `${path}.displayName`),
@@ -183,7 +203,8 @@ var SSDiagram = (() => {
183
203
  editorType: requireString(input.editorType, `${path}.editorType`)
184
204
  };
185
205
  }
186
- function normalizeLink(input, path) {
206
+ function normalizeLink(value, path) {
207
+ const input = requireStructure(value, path);
187
208
  return {
188
209
  id: requireIdentifier(input.id, `${path}.id`),
189
210
  from: normalizeEndpoint(input.from, `${path}.from`),
@@ -191,14 +212,15 @@ var SSDiagram = (() => {
191
212
  metadata: cloneJsonObject(input.metadata ?? {}, `${path}.metadata`)
192
213
  };
193
214
  }
194
- function normalizeEndpoint(input, path) {
215
+ function normalizeEndpoint(value, path) {
216
+ const input = requireStructure(value, path);
195
217
  return {
196
218
  nodeId: requireIdentifier(input.nodeId, `${path}.nodeId`),
197
219
  portId: requireIdentifier(input.portId, `${path}.portId`)
198
220
  };
199
221
  }
200
222
  function parseNode(value, path) {
201
- const node = requireObject(value, path);
223
+ const node = requireStructure(value, path);
202
224
  return normalizeNode({
203
225
  id: requireString(node.id, `${path}.id`),
204
226
  typeId: requireString(node.typeId, `${path}.typeId`),
@@ -220,7 +242,7 @@ var SSDiagram = (() => {
220
242
  }, path);
221
243
  }
222
244
  function parsePort(value, path) {
223
- const port = requireObject(value, path);
245
+ const port = requireStructure(value, path);
224
246
  return normalizePort({
225
247
  id: requireString(port.id, `${path}.id`),
226
248
  name: requireString(port.name, `${path}.name`),
@@ -229,13 +251,13 @@ var SSDiagram = (() => {
229
251
  maxLinks: requireNumber(port.maxLinks, `${path}.maxLinks`),
230
252
  availableTypes: requireArray(port.availableTypes, `${path}.availableTypes`).map((type, index) => requireString(type, `${path}.availableTypes[${index}]`)),
231
253
  isDynamic: requireBoolean(port.isDynamic, `${path}.isDynamic`),
232
- dynamicMode: requireString(port.dynamicMode, `${path}.dynamicMode`),
254
+ dynamicMode: requirePortDynamicMode(port.dynamicMode, `${path}.dynamicMode`),
233
255
  isSibling: requireBoolean(port.isSibling, `${path}.isSibling`),
234
256
  metadata: cloneJsonObject(port.metadata, `${path}.metadata`)
235
257
  }, path);
236
258
  }
237
259
  function parseParameter(value, path) {
238
- const parameter = requireObject(value, path);
260
+ const parameter = requireStructure(value, path);
239
261
  return normalizeParameter({
240
262
  name: requireString(parameter.name, `${path}.name`),
241
263
  displayName: requireString(parameter.displayName, `${path}.displayName`),
@@ -252,7 +274,7 @@ var SSDiagram = (() => {
252
274
  }, path);
253
275
  }
254
276
  function parseLink(value, path) {
255
- const link = requireObject(value, path);
277
+ const link = requireStructure(value, path);
256
278
  return normalizeLink({
257
279
  id: requireString(link.id, `${path}.id`),
258
280
  from: parseEndpoint(link.from, `${path}.from`),
@@ -261,7 +283,7 @@ var SSDiagram = (() => {
261
283
  }, path);
262
284
  }
263
285
  function parseEndpoint(value, path) {
264
- const endpoint = requireObject(value, path);
286
+ const endpoint = requireStructure(value, path);
265
287
  return {
266
288
  nodeId: requireIdentifier(endpoint.nodeId, `${path}.nodeId`),
267
289
  portId: requireIdentifier(endpoint.portId, `${path}.portId`)
@@ -309,7 +331,7 @@ var SSDiagram = (() => {
309
331
  function cloneStringRecord(value, path) {
310
332
  const object = requireObject(value, path);
311
333
  const result = {};
312
- for (const [key, item] of Object.entries(object)) result[key] = requireString(item, `${path}.${key}`);
334
+ for (const [key, item] of Object.entries(object)) setJsonKey(result, key, requireString(item, `${path}.${key}`));
313
335
  return result;
314
336
  }
315
337
  function cloneJsonObject(value, path, ancestors = /* @__PURE__ */ new WeakSet()) {
@@ -319,7 +341,7 @@ var SSDiagram = (() => {
319
341
  const result = {};
320
342
  try {
321
343
  for (const [key, item] of Object.entries(object)) {
322
- result[key] = cloneJsonValue(item, `${path}.${key}`, ancestors);
344
+ setJsonKey(result, key, cloneJsonValue(item, `${path}.${key}`, ancestors));
323
345
  }
324
346
  } finally {
325
347
  ancestors.delete(object);
@@ -341,6 +363,12 @@ var SSDiagram = (() => {
341
363
  if (isObject(value)) return cloneJsonObject(value, path, ancestors);
342
364
  throw new DiagramDocumentError("expected a JSON value", path);
343
365
  }
366
+ function requireStructure(value, path) {
367
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
368
+ throw new DiagramDocumentError("expected an object", path);
369
+ }
370
+ return value;
371
+ }
344
372
  function requireObject(value, path) {
345
373
  if (!isObject(value)) throw new DiagramDocumentError("expected an object", path);
346
374
  return value;
@@ -350,6 +378,13 @@ var SSDiagram = (() => {
350
378
  const prototype = Object.getPrototypeOf(value);
351
379
  return prototype === Object.prototype || prototype === null;
352
380
  }
381
+ function mapElements(value, path) {
382
+ if (value === void 0 || value === null) return [];
383
+ return Array.from(
384
+ requireArray(value, path),
385
+ (element, index) => requireStructure(element, `${path}[${index}]`)
386
+ );
387
+ }
353
388
  function requireArray(value, path) {
354
389
  if (!Array.isArray(value)) throw new DiagramDocumentError("expected an array", path);
355
390
  return value;
@@ -358,6 +393,10 @@ var SSDiagram = (() => {
358
393
  if (typeof value !== "string") throw new DiagramDocumentError("expected a string", path);
359
394
  return value;
360
395
  }
396
+ function requirePortDynamicMode(value, path) {
397
+ const mode = requireString(value, path);
398
+ return toPortDynamicMode(mode);
399
+ }
361
400
  function requireIdentifier(value, path) {
362
401
  const id = requireString(value, path);
363
402
  if (id.trim().length === 0) throw new DiagramDocumentError("identifier cannot be empty", path);
@@ -399,6 +438,24 @@ var SSDiagram = (() => {
399
438
  if (this.actions.get(action.id) === action) this.actions.delete(action.id);
400
439
  };
401
440
  }
441
+ /**
442
+ * Registers one action per id from a table keyed by TId, in key order.
443
+ * Because the table is a total Record, adding a member to TId without adding
444
+ * its action stops compiling instead of producing a command that silently
445
+ * does nothing.
446
+ */
447
+ registerAll(actions) {
448
+ const ids = Object.keys(actions);
449
+ const disposers = ids.map((id) => {
450
+ const action = actions[id];
451
+ return this.register({
452
+ id,
453
+ canExecute: (context) => action.canExecute(context),
454
+ execute: (context) => action.execute(context)
455
+ });
456
+ });
457
+ return () => disposers.forEach((dispose) => dispose());
458
+ }
402
459
  get(id) {
403
460
  return this.actions.get(id) ?? null;
404
461
  }
@@ -554,7 +611,7 @@ var SSDiagram = (() => {
554
611
  globalError: state.globalError === null ? null : { ...state.globalError },
555
612
  nodes: Object.fromEntries(Object.entries(state.nodes).map(([nodeId, node]) => [nodeId, {
556
613
  active: node.active,
557
- error: node.error === null ? null : { ...node.error },
614
+ errors: cloneDiagramNodeErrors(node.errors),
558
615
  ports: {
559
616
  in: Object.fromEntries(Object.entries(node.ports.in).map(([portId, port]) => [portId, { ...port }])),
560
617
  out: Object.fromEntries(Object.entries(node.ports.out).map(([portId, port]) => [portId, { ...port }]))
@@ -611,10 +668,17 @@ var SSDiagram = (() => {
611
668
  function createDiagramNodeRuntimeState() {
612
669
  return {
613
670
  active: false,
614
- error: null,
671
+ errors: {},
615
672
  ports: { in: {}, out: {} }
616
673
  };
617
674
  }
675
+ function cloneDiagramNodeErrors(errors) {
676
+ const result = {};
677
+ if (errors === void 0 || errors === null) return result;
678
+ if (errors.runtime !== void 0) result.runtime = { ...errors.runtime };
679
+ if (errors.load !== void 0) result.load = { ...errors.load };
680
+ return result;
681
+ }
618
682
 
619
683
  // src/core/view-state.ts
620
684
  var DIAGRAM_VIEW_STATE_VERSION = 1;
@@ -699,7 +763,7 @@ var SSDiagram = (() => {
699
763
  function copyJsonObject(value) {
700
764
  if (value === void 0) return {};
701
765
  const result = {};
702
- for (const [key, item] of Object.entries(value)) result[key] = copyJsonValue(item);
766
+ for (const [key, item] of Object.entries(value)) setJsonKey(result, key, copyJsonValue(item));
703
767
  return result;
704
768
  }
705
769
  function copyParameters(value) {
@@ -730,7 +794,7 @@ var SSDiagram = (() => {
730
794
  this.maxLinks = typeof init.maxLinks === "number" ? init.maxLinks : 0;
731
795
  this.availableTypes = [...init.availableTypes ?? []];
732
796
  this.isDynamic = init.isDynamic ?? false;
733
- this.dynamicMode = init.dynamicMode ?? "";
797
+ this.dynamicMode = toPortDynamicMode(init.dynamicMode);
734
798
  this.isSibling = init.isSibling ?? false;
735
799
  this.metadata = copyJsonObject(init.metadata);
736
800
  }
@@ -897,8 +961,6 @@ var SSDiagram = (() => {
897
961
  this.dragStart = [];
898
962
  // group-drag origin
899
963
  this.dragAnchor = { wx: 0, wy: 0 };
900
- this.dragDX = 0;
901
- this.dragDY = 0;
902
964
  this.panning = false;
903
965
  this.panX = 0;
904
966
  this.panY = 0;
@@ -1482,7 +1544,7 @@ var SSDiagram = (() => {
1482
1544
  setNodeParamValue(nodeId, name, value) {
1483
1545
  return this.updateNodeState(nodeId, "set node parameter", (node) => {
1484
1546
  if (value === void 0) delete node.paramValues[name];
1485
- else node.paramValues[name] = value;
1547
+ else setJsonKey(node.paramValues, name, value);
1486
1548
  });
1487
1549
  }
1488
1550
  setShowNodeMessages(show) {
@@ -1565,7 +1627,7 @@ var SSDiagram = (() => {
1565
1627
  for (const node of this.nodes) {
1566
1628
  if (node.loadError.length === 0) continue;
1567
1629
  const state = createDiagramNodeRuntimeState();
1568
- state.error = { kind: "load", message: node.loadError, pulse: ++this.runtimePulse };
1630
+ state.errors.load = { kind: "load", message: node.loadError, pulse: ++this.runtimePulse };
1569
1631
  this.runtimeState.nodes[node.id] = state;
1570
1632
  }
1571
1633
  if (Object.keys(this.runtimeState.nodes).length > 0) this.emitRuntimeStateChanged();
@@ -1765,15 +1827,16 @@ var SSDiagram = (() => {
1765
1827
  this.runtimePulse = Math.max(
1766
1828
  this.runtimePulse,
1767
1829
  this.runtimeState.globalError?.pulse ?? 0,
1768
- ...Object.values(this.runtimeState.nodes).map((node) => node.error?.pulse ?? 0)
1830
+ ...Object.values(this.runtimeState.nodes).flatMap((node) => [node.errors.runtime?.pulse ?? 0, node.errors.load?.pulse ?? 0])
1769
1831
  );
1770
1832
  for (const node of this.nodes) {
1771
- const error = this.runtimeState.nodes[node.id]?.error ?? null;
1772
- const previousError = previous.nodes[node.id]?.error ?? null;
1773
- node.runtimeError = error?.kind === "runtime" ? error.message : "";
1774
- node.loadError = error?.kind === "load" ? error.message : "";
1775
- if (error?.kind === "runtime") {
1776
- if (previousError?.kind !== "runtime" || previousError.pulse !== error.pulse) {
1833
+ const errors = this.runtimeState.nodes[node.id]?.errors;
1834
+ const previousRuntime = previous.nodes[node.id]?.errors.runtime;
1835
+ const runtimeError = errors?.runtime;
1836
+ node.runtimeError = runtimeError?.message ?? "";
1837
+ node.loadError = errors?.load?.message ?? "";
1838
+ if (runtimeError !== void 0) {
1839
+ if (previousRuntime === void 0 || previousRuntime.pulse !== runtimeError.pulse) {
1777
1840
  node.errorFlashStart = performance.now();
1778
1841
  }
1779
1842
  } else {
@@ -1822,7 +1885,9 @@ var SSDiagram = (() => {
1822
1885
  const state = this.getRuntimeState();
1823
1886
  const nodeState = state.nodes[id] ?? createDiagramNodeRuntimeState();
1824
1887
  state.nodes[id] = nodeState;
1825
- nodeState.error = message.length === 0 ? null : { kind, message, pulse: ++this.runtimePulse };
1888
+ if (message.length === 0) delete nodeState.errors[kind];
1889
+ else if (kind === "load") nodeState.errors.load = { kind, message, pulse: ++this.runtimePulse };
1890
+ else nodeState.errors.runtime = { kind, message, pulse: ++this.runtimePulse };
1826
1891
  this.runtimeState = state;
1827
1892
  this.emitRuntimeStateChanged();
1828
1893
  return true;
@@ -1837,8 +1902,9 @@ var SSDiagram = (() => {
1837
1902
  if (kind === void 0 || kind === "load") node.loadError = "";
1838
1903
  const state = this.getRuntimeState();
1839
1904
  const nodeState = state.nodes[id];
1840
- if (nodeState !== void 0 && (kind === void 0 || nodeState.error?.kind === kind)) {
1841
- nodeState.error = node.runtimeError.length > 0 ? { kind: "runtime", message: node.runtimeError, pulse: ++this.runtimePulse } : node.loadError.length > 0 ? { kind: "load", message: node.loadError, pulse: ++this.runtimePulse } : null;
1905
+ if (nodeState !== void 0) {
1906
+ if (kind === void 0 || kind === "runtime") delete nodeState.errors.runtime;
1907
+ if (kind === void 0 || kind === "load") delete nodeState.errors.load;
1842
1908
  }
1843
1909
  this.runtimeState = state;
1844
1910
  this.emitRuntimeStateChanged();
@@ -3634,6 +3700,13 @@ ${runtime.error}`;
3634
3700
  }
3635
3701
  }
3636
3702
  }
3703
+ /**
3704
+ * Whether anyone is listening. Lets a subject skip building a payload that
3705
+ * is expensive to produce and that nothing would read.
3706
+ */
3707
+ hasHandlers(event) {
3708
+ return (this.handlers.get(event)?.size ?? 0) > 0;
3709
+ }
3637
3710
  clearEventHandlers() {
3638
3711
  this.handlers.clear();
3639
3712
  }
@@ -3655,7 +3728,7 @@ ${runtime.error}`;
3655
3728
  this.maxLinks = typeof init.maxLinks === "number" ? init.maxLinks : 0;
3656
3729
  this.availableTypes = init.availableTypes ?? [];
3657
3730
  this.isDynamic = init.isDynamic ?? false;
3658
- this.dynamicMode = init.dynamicMode ?? "";
3731
+ this.dynamicMode = toPortDynamicMode(init.dynamicMode);
3659
3732
  this.isSibling = init.isSibling ?? false;
3660
3733
  }
3661
3734
  clone() {
@@ -3757,7 +3830,6 @@ ${runtime.error}`;
3757
3830
  this.fullscreen = false;
3758
3831
  this.fullscreenButtonVisible = true;
3759
3832
  this.fullscreenLabels = { enter: "Enter fullscreen", exit: "Exit fullscreen" };
3760
- this.linkValidator = null;
3761
3833
  this.contextActions = new DiagramActionRegistry();
3762
3834
  this.div = options.div;
3763
3835
  this.catalog = options.catalog;
@@ -3781,7 +3853,6 @@ ${runtime.error}`;
3781
3853
  this.updateZoomLabel();
3782
3854
  }
3783
3855
  setLinkValidator(validator) {
3784
- this.linkValidator = validator;
3785
3856
  this.canvas.setLinkValidator(validator === null ? null : ({ fromNode, fromPort, toNode, toPort }) => validator({
3786
3857
  fromNode: this.fromCanvasNode(fromNode),
3787
3858
  fromPort: this.fromCanvasPort(fromPort),
@@ -3855,21 +3926,25 @@ ${runtime.error}`;
3855
3926
  removeLink(link) {
3856
3927
  this.canvas.removeLink(this.toCanvasLink(link));
3857
3928
  }
3929
+ /** False when nothing changed: no such node or port, or the edit was a no-op. */
3858
3930
  addPort(nodeId, direction, port) {
3859
- this.canvas.addPort(nodeId, direction, this.toCanvasPort(port));
3931
+ return this.canvas.addPort(nodeId, direction, this.toCanvasPort(port));
3860
3932
  }
3933
+ /** False when nothing changed: no such node or port, or the edit was a no-op. */
3861
3934
  removePort(nodeId, direction, portId) {
3862
- this.canvas.removePort(nodeId, direction, portId);
3935
+ return this.canvas.removePort(nodeId, direction, portId);
3863
3936
  }
3937
+ /** False when nothing changed: no such node or port, or the edit was a no-op. */
3864
3938
  updatePortType(nodeId, direction, portId, type) {
3865
- this.canvas.updatePortType(nodeId, direction, portId, type);
3939
+ return this.canvas.updatePortType(nodeId, direction, portId, type);
3866
3940
  }
3867
3941
  updatePort(nodeId, direction, portId, patch) {
3868
3942
  return this.canvas.updatePort(nodeId, direction, portId, patch);
3869
3943
  }
3944
+ /** False when nothing changed: no such node or port, or the edit was a no-op. */
3870
3945
  setNodePorts(nodeId, inPorts, outPorts) {
3871
3946
  const current = this.canvas.findNode(nodeId);
3872
- if (current === void 0) return;
3947
+ if (current === void 0) return false;
3873
3948
  const convert = (port) => ({
3874
3949
  id: port.key,
3875
3950
  name: port.name,
@@ -3888,13 +3963,15 @@ ${runtime.error}`;
3888
3963
  for (const sibling of current.outPorts.filter((port) => port.isSibling)) {
3889
3964
  if (!nextOut.some((port) => port.id === sibling.id)) nextOut.push(sibling.toInit());
3890
3965
  }
3891
- this.canvas.setNodePorts(nodeId, nextIn, nextOut);
3966
+ return this.canvas.setNodePorts(nodeId, nextIn, nextOut);
3892
3967
  }
3968
+ /** False when nothing changed: no such node or port, or the edit was a no-op. */
3893
3969
  updateNode(nodeId, patch) {
3894
- this.canvas.updateNode(nodeId, patch);
3970
+ return this.canvas.updateNode(nodeId, patch);
3895
3971
  }
3972
+ /** False when nothing changed: no such node or port, or the edit was a no-op. */
3896
3973
  setNodeMessage(nodeId, message) {
3897
- this.canvas.updateNode(nodeId, { message });
3974
+ return this.canvas.updateNode(nodeId, { message });
3898
3975
  }
3899
3976
  setNodeError(nodeId, message, options = {}) {
3900
3977
  return this.canvas.setNodeError(nodeId, message, options);
@@ -3920,11 +3997,13 @@ ${runtime.error}`;
3920
3997
  setGlobalError(message, kind = "invalid") {
3921
3998
  this.canvas.setGlobalError(message, kind);
3922
3999
  }
4000
+ /** False when nothing changed: no such node or port, or the edit was a no-op. */
3923
4001
  setNodeParamValue(nodeId, paramName, value) {
3924
- this.canvas.setNodeParamValue(nodeId, paramName, value);
4002
+ return this.canvas.setNodeParamValue(nodeId, paramName, value);
3925
4003
  }
4004
+ /** False when nothing changed: no such node or port, or the edit was a no-op. */
3926
4005
  setNodeName(nodeId, value) {
3927
- this.canvas.updateNode(nodeId, { name: value });
4006
+ return this.canvas.updateNode(nodeId, { name: value });
3928
4007
  }
3929
4008
  /** Groups host-driven document edits into one undo/redo operation. */
3930
4009
  transaction(label, action) {
@@ -4347,50 +4426,43 @@ ${runtime.error}`;
4347
4426
  }
4348
4427
  registerContextActions() {
4349
4428
  const permissions = () => this.canvas.getInteractionPermissions();
4350
- this.contextActions.register({
4351
- id: "undo",
4352
- canExecute: () => this.canUndo(),
4353
- execute: () => this.undo()
4354
- });
4355
- this.contextActions.register({
4356
- id: "redo",
4357
- canExecute: () => this.canRedo(),
4358
- execute: () => this.redo()
4359
- });
4360
- this.contextActions.register({
4361
- id: "cut",
4362
- canExecute: ({ nodes }) => nodes.length > 0 && permissions().copy && permissions().deleteSelection,
4363
- execute: () => this.cutSelection()
4364
- });
4365
- this.contextActions.register({
4366
- id: "copy",
4367
- canExecute: ({ nodes }) => nodes.length > 0 && permissions().copy,
4368
- execute: () => this.copySelection()
4369
- });
4370
- this.contextActions.register({
4371
- id: "paste",
4372
- canExecute: () => this.canvas.hasClipboard() && permissions().paste,
4373
- execute: () => this.pasteSelection()
4374
- });
4375
- this.contextActions.register({
4376
- id: "open",
4377
- canExecute: ({ nodes }) => nodes.length === 1 && nodes[0].openAction.length > 0,
4378
- execute: ({ nodes }) => this.emit("nodeOpen", { nodes })
4379
- });
4380
- this.contextActions.register({
4381
- id: "delete",
4382
- canExecute: ({ selection }) => permissions().deleteSelection && (selection.nodeIds.length > 0 || selection.linkIds.length > 0),
4383
- execute: () => this.canvas.deleteSelection()
4384
- });
4385
- this.contextActions.register({
4386
- id: "properties",
4387
- canExecute: ({ nodes }) => nodes.length > 0,
4388
- execute: ({ nodes }) => this.emit("nodeProperties", { nodes })
4389
- });
4390
- this.contextActions.register({
4391
- id: "help",
4392
- canExecute: ({ nodes }) => this.helpEnabled && nodes.length > 0,
4393
- execute: ({ nodes }) => this.emit("nodeHelp", { nodes })
4429
+ this.contextActions.registerAll({
4430
+ undo: {
4431
+ canExecute: () => this.canUndo(),
4432
+ execute: () => this.undo()
4433
+ },
4434
+ redo: {
4435
+ canExecute: () => this.canRedo(),
4436
+ execute: () => this.redo()
4437
+ },
4438
+ cut: {
4439
+ canExecute: ({ nodes }) => nodes.length > 0 && permissions().copy && permissions().deleteSelection,
4440
+ execute: () => this.cutSelection()
4441
+ },
4442
+ copy: {
4443
+ canExecute: ({ nodes }) => nodes.length > 0 && permissions().copy,
4444
+ execute: () => this.copySelection()
4445
+ },
4446
+ paste: {
4447
+ canExecute: () => this.canvas.hasClipboard() && permissions().paste,
4448
+ execute: () => this.pasteSelection()
4449
+ },
4450
+ open: {
4451
+ canExecute: ({ nodes }) => nodes.length === 1 && nodes[0].openAction.length > 0,
4452
+ execute: ({ nodes }) => this.emit("nodeOpen", { nodes })
4453
+ },
4454
+ delete: {
4455
+ canExecute: ({ selection }) => permissions().deleteSelection && (selection.nodeIds.length > 0 || selection.linkIds.length > 0),
4456
+ execute: () => this.canvas.deleteSelection()
4457
+ },
4458
+ properties: {
4459
+ canExecute: ({ nodes }) => nodes.length > 0,
4460
+ execute: ({ nodes }) => this.emit("nodeProperties", { nodes })
4461
+ },
4462
+ help: {
4463
+ canExecute: ({ nodes }) => this.helpEnabled && nodes.length > 0,
4464
+ execute: ({ nodes }) => this.emit("nodeHelp", { nodes })
4465
+ }
4394
4466
  });
4395
4467
  }
4396
4468
  contextActionContext() {
@@ -4558,19 +4630,26 @@ ${runtime.error}`;
4558
4630
  // imported node ("Element type is missing from the palette"). Node.id
4559
4631
  // keeps its original casing so save/load round-trips unchanged.
4560
4632
  addNodeType(node) {
4561
- const n = node instanceof Node ? node : new Node(node);
4633
+ const n = (node instanceof Node ? node : new Node(node)).clone();
4562
4634
  this.nodeTypes.set(n.id.toLowerCase(), n);
4563
- this.emit("nodeTypesChanged", this.getNodeTypes());
4635
+ this.emitNodeTypesChanged();
4564
4636
  }
4565
4637
  removeNodeType(id) {
4566
4638
  this.nodeTypes.delete(id.toLowerCase());
4567
- this.emit("nodeTypesChanged", this.getNodeTypes());
4639
+ this.emitNodeTypesChanged();
4568
4640
  }
4569
4641
  getNodeType(id) {
4570
- return this.nodeTypes.get(id.toLowerCase()) ?? null;
4642
+ return this.nodeTypes.get(id.toLowerCase())?.clone() ?? null;
4571
4643
  }
4572
4644
  getNodeTypes() {
4573
- return Array.from(this.nodeTypes.values());
4645
+ return Array.from(this.nodeTypes.values(), (node) => node.clone());
4646
+ }
4647
+ // Node definitions are mutable, so the payload has to be a copy of each one.
4648
+ // Building a catalog is a loop of addNodeType calls, which would make that
4649
+ // quadratic; skipping it while nobody listens keeps catalog construction --
4650
+ // where no subscriber exists yet -- as cheap as it was.
4651
+ emitNodeTypesChanged() {
4652
+ if (this.hasHandlers("nodeTypesChanged")) this.emit("nodeTypesChanged", this.getNodeTypes());
4574
4653
  }
4575
4654
  };
4576
4655
 
@@ -4883,60 +4962,127 @@ ${runtime.error}`;
4883
4962
  });
4884
4963
  return { nodes, nodeErrors };
4885
4964
  }
4965
+ function isRecord(value) {
4966
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4967
+ }
4968
+ function asRecord(value) {
4969
+ return isRecord(value) ? value : void 0;
4970
+ }
4971
+ function asArray(value) {
4972
+ return Array.isArray(value) ? value : [];
4973
+ }
4974
+ function asString(value) {
4975
+ return typeof value === "string" ? value : "";
4976
+ }
4977
+ function asFiniteNumber(value) {
4978
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
4979
+ }
4886
4980
  function nodeName(node) {
4887
- const settings = node?.Settings;
4888
- const parameters = settings?.Parameters;
4889
- const nameParam = parameters?.Name;
4890
- const nameVal = nameParam?.Value;
4891
- if (typeof nameVal === "string" && nameVal.length > 0)
4892
- return nameVal;
4893
- if (typeof node?.Figure === "string" && node.Figure.length > 0)
4894
- return node.Figure;
4895
- return typeof node?.Key === "string" ? node.Key : "";
4981
+ const named = asRecord(asRecord(asRecord(node.Settings)?.Parameters)?.Name)?.Value;
4982
+ if (typeof named === "string" && named.length > 0)
4983
+ return named;
4984
+ const figure = asString(node.Figure);
4985
+ return figure.length > 0 ? figure : asString(node.Key);
4896
4986
  }
4897
4987
  function parseRawScheme(raw) {
4898
4988
  const nodes = [];
4899
4989
  const links = [];
4900
- const root = raw;
4901
- const content = root?.Content;
4902
- const value = content?.Value;
4903
- const scheme = value?.Scheme;
4904
- const model = scheme?.Model;
4905
- if (!model)
4990
+ const model = asRecord(asRecord(asRecord(asRecord(raw)?.Content)?.Value)?.Scheme)?.Model;
4991
+ const fields = asRecord(model);
4992
+ if (fields === void 0)
4906
4993
  return { nodes, links };
4907
- for (const raw2 of model.Nodes ?? []) {
4908
- const key = raw2?.Key;
4994
+ for (const entry of asArray(fields.Nodes)) {
4995
+ const node = asRecord(entry);
4996
+ if (node === void 0)
4997
+ continue;
4998
+ const key = node.Key;
4909
4999
  if (typeof key !== "string" || key.length === 0)
4910
5000
  continue;
4911
5001
  nodes.push({
4912
5002
  id: key,
4913
- typeId: typeof raw2.TypeId === "string" ? raw2.TypeId : "",
4914
- name: nodeName(raw2),
4915
- x: typeof raw2.X === "number" ? raw2.X : 0,
4916
- y: typeof raw2.Y === "number" ? raw2.Y : 0
5003
+ typeId: asString(node.TypeId),
5004
+ name: nodeName(node),
5005
+ x: asFiniteNumber(node.X),
5006
+ y: asFiniteNumber(node.Y)
4917
5007
  });
4918
5008
  }
4919
- for (const raw2 of model.Links ?? []) {
4920
- const from = raw2?.From;
4921
- const to = raw2?.To;
5009
+ for (const entry of asArray(fields.Links)) {
5010
+ const link = asRecord(entry);
5011
+ if (link === void 0)
5012
+ continue;
5013
+ const from = link.From;
5014
+ const to = link.To;
4922
5015
  if (typeof from !== "string" || typeof to !== "string")
4923
5016
  continue;
4924
5017
  links.push({
4925
5018
  from,
4926
- fromPort: typeof raw2.FromPort === "string" ? raw2.FromPort : "",
5019
+ fromPort: asString(link.FromPort),
4927
5020
  to,
4928
- toPort: typeof raw2.ToPort === "string" ? raw2.ToPort : ""
5021
+ toPort: asString(link.ToPort)
4929
5022
  });
4930
5023
  }
4931
5024
  return { nodes, links };
4932
5025
  }
5026
+ function parsePalettePorts(value) {
5027
+ const ports = [];
5028
+ for (const entry of asArray(value)) {
5029
+ const port = asRecord(entry);
5030
+ if (port === void 0)
5031
+ continue;
5032
+ const key = asString(port.key);
5033
+ if (key.length === 0)
5034
+ continue;
5035
+ ports.push({
5036
+ key,
5037
+ name: asString(port.name),
5038
+ type: asString(port.type),
5039
+ maxLinks: asFiniteNumber(port.maxLinks),
5040
+ availableTypes: asArray(port.availableTypes).filter((item) => typeof item === "string"),
5041
+ isDynamic: port.isDynamic === true,
5042
+ dynamicMode: toPortDynamicMode(port.dynamicMode)
5043
+ });
5044
+ }
5045
+ return ports;
5046
+ }
5047
+ function parsePalette(value) {
5048
+ const socketTypes = [];
5049
+ const elements = [];
5050
+ const root = asRecord(value);
5051
+ if (root === void 0)
5052
+ return { socketTypes, elements };
5053
+ for (const entry of asArray(root.socketTypes)) {
5054
+ const socket = asRecord(entry);
5055
+ const name = asString(socket?.name);
5056
+ if (name.length === 0)
5057
+ continue;
5058
+ socketTypes.push({ name, color: asString(socket?.color) });
5059
+ }
5060
+ for (const entry of asArray(root.elements)) {
5061
+ const element = asRecord(entry);
5062
+ const typeId = asString(element?.typeId);
5063
+ if (element === void 0 || typeId.length === 0)
5064
+ continue;
5065
+ elements.push({
5066
+ typeId,
5067
+ name: asString(element.name),
5068
+ groupName: asString(element.groupName),
5069
+ icon: asString(element.icon),
5070
+ inPorts: parsePalettePorts(element.inPorts),
5071
+ outPorts: parsePalettePorts(element.outPorts)
5072
+ });
5073
+ }
5074
+ return { socketTypes, elements };
5075
+ }
4933
5076
  async function renderScheme(div, paletteUrl, scheme, options = {}) {
4934
5077
  return renderSchemeAtRevision(div, paletteUrl, scheme, beginRender(div), options);
4935
5078
  }
4936
5079
  async function renderSchemeAtRevision(div, paletteUrl, scheme, revision, options) {
4937
5080
  if (renderRevisions.get(div) !== revision)
4938
5081
  return null;
4939
- const palette = await fetch(paletteUrl).then((r) => r.json());
5082
+ const response = await fetch(paletteUrl);
5083
+ if (!response.ok)
5084
+ throw new Error(`Palette request to ${paletteUrl} failed with status ${response.status}.`);
5085
+ const palette = parsePalette(await response.json());
4940
5086
  if (renderRevisions.get(div) !== revision)
4941
5087
  return null;
4942
5088
  const catalog = buildCatalog(palette);
@@ -5100,6 +5246,14 @@ ${runtime.error}`;
5100
5246
  disconnectedHostObserver.disconnect();
5101
5247
  disconnectedHostObserver = null;
5102
5248
  }
5249
+ function errorTexts(div) {
5250
+ const parts = (div.dataset.diagramErrors ?? "").split("|");
5251
+ return {
5252
+ load: parts[0] || "Diagram source could not be loaded.",
5253
+ empty: parts[1] || "Diagram is empty or malformed.",
5254
+ draw: parts[2] || "Diagram could not be rendered."
5255
+ };
5256
+ }
5103
5257
  function note(div, message, revision) {
5104
5258
  if (renderRevisions.get(div) !== revision) return;
5105
5259
  disposeActiveRender(div);
@@ -5108,53 +5262,51 @@ ${runtime.error}`;
5108
5262
  }
5109
5263
  async function renderFromSource(div, paletteUrl, srcUrl, options = {}) {
5110
5264
  const revision = beginRender(div);
5111
- const errors = (div.dataset.diagramErrors ?? "").split("|");
5112
- const [errLoad = "Diagram source could not be loaded.", errEmpty = "Diagram is empty or malformed.", errDraw = "Diagram could not be rendered."] = errors;
5265
+ const errors = errorTexts(div);
5113
5266
  let raw;
5114
5267
  try {
5115
5268
  const resp = await fetch(srcUrl);
5116
5269
  if (!resp.ok) {
5117
- note(div, errLoad, revision);
5270
+ note(div, errors.load, revision);
5118
5271
  return null;
5119
5272
  }
5120
5273
  const text = (await resp.text()).replace(/^/, "");
5121
5274
  raw = JSON.parse(text);
5122
5275
  } catch {
5123
- note(div, errLoad, revision);
5276
+ note(div, errors.load, revision);
5124
5277
  return null;
5125
5278
  }
5126
5279
  const scheme = parseRawScheme(raw);
5127
5280
  if (scheme.nodes.length === 0) {
5128
- note(div, errEmpty, revision);
5281
+ note(div, errors.empty, revision);
5129
5282
  return null;
5130
5283
  }
5131
5284
  try {
5132
5285
  return await renderSchemeAtRevision(div, paletteUrl, scheme, revision, options);
5133
5286
  } catch {
5134
- note(div, errDraw, revision);
5287
+ note(div, errors.draw, revision);
5135
5288
  return null;
5136
5289
  }
5137
5290
  }
5138
5291
  async function renderFromInline(div, paletteUrl, json, options = {}) {
5139
5292
  const revision = beginRender(div);
5140
- const errors = (div.dataset.diagramErrors ?? "").split("|");
5141
- const [, errEmpty = "Diagram is empty or malformed.", errDraw = "Diagram could not be rendered."] = errors;
5293
+ const errors = errorTexts(div);
5142
5294
  let raw;
5143
5295
  try {
5144
5296
  raw = JSON.parse(json);
5145
5297
  } catch {
5146
- note(div, errEmpty, revision);
5298
+ note(div, errors.empty, revision);
5147
5299
  return null;
5148
5300
  }
5149
5301
  const scheme = parseRawScheme(raw);
5150
5302
  if (scheme.nodes.length === 0) {
5151
- note(div, errEmpty, revision);
5303
+ note(div, errors.empty, revision);
5152
5304
  return null;
5153
5305
  }
5154
5306
  try {
5155
5307
  return await renderSchemeAtRevision(div, paletteUrl, scheme, revision, options);
5156
5308
  } catch {
5157
- note(div, errDraw, revision);
5309
+ note(div, errors.draw, revision);
5158
5310
  return null;
5159
5311
  }
5160
5312
  }