@stocksharp/diagram 0.2.3 → 0.4.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 (68) hide show
  1. package/dist/esm/canvas-renderer.js +79 -28
  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 +134 -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 +8 -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 +380 -150
  26. package/dist/ssdiagram.js.map +4 -4
  27. package/dist/types/canvas-renderer.d.ts +11 -6
  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 +1 -1
  32. package/dist/types/core/document.d.ts.map +1 -1
  33. package/dist/types/core/json.d.ts +11 -0
  34. package/dist/types/core/json.d.ts.map +1 -0
  35. package/dist/types/core/model.d.ts +45 -2
  36. package/dist/types/core/model.d.ts.map +1 -1
  37. package/dist/types/core/state.d.ts +24 -1
  38. package/dist/types/core/state.d.ts.map +1 -1
  39. package/dist/types/diagram/api.d.ts +6 -6
  40. package/dist/types/diagram/api.d.ts.map +1 -1
  41. package/dist/types/diagram/catalog.d.ts +2 -1
  42. package/dist/types/diagram/catalog.d.ts.map +1 -1
  43. package/dist/types/diagram/event-emitter.d.ts +6 -1
  44. package/dist/types/diagram/event-emitter.d.ts.map +1 -1
  45. package/dist/types/diagram/palette.d.ts +1 -1
  46. package/dist/types/diagram/palette.d.ts.map +1 -1
  47. package/dist/types/diagram/stocksharp-diagram.d.ts +29 -22
  48. package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -1
  49. package/dist/types/diagram/types.d.ts +4 -4
  50. package/dist/types/diagram/types.d.ts.map +1 -1
  51. package/dist/types/embed.d.ts.map +1 -1
  52. package/dist/types/index.d.ts +3 -3
  53. package/dist/types/index.d.ts.map +1 -1
  54. package/package.json +2 -2
  55. package/src/canvas-renderer.ts +84 -29
  56. package/src/core/action-registry.ts +25 -0
  57. package/src/core/document.ts +147 -31
  58. package/src/core/json.ts +16 -0
  59. package/src/core/model.ts +55 -2
  60. package/src/core/state.ts +41 -3
  61. package/src/diagram/api.ts +6 -5
  62. package/src/diagram/catalog.ts +18 -6
  63. package/src/diagram/event-emitter.ts +12 -1
  64. package/src/diagram/palette.ts +1 -1
  65. package/src/diagram/stocksharp-diagram.ts +79 -76
  66. package/src/diagram/types.ts +6 -5
  67. package/src/embed.ts +140 -39
  68. package/src/index.ts +4 -0
@@ -6,7 +6,7 @@ import {
6
6
  parseDiagramViewState,
7
7
  serializeDiagramViewState,
8
8
  } from '../core/view-state.js';
9
- import type { DiagramDocument } from '../core/model.js';
9
+ import type { DiagramDocument, PortDynamicMode } from '../core/model.js';
10
10
  import { DiagramActionRegistry } from '../core/action-registry.js';
11
11
  import type {
12
12
  DiagramGlobalErrorKind,
@@ -14,6 +14,7 @@ import type {
14
14
  DiagramPortRuntimeState,
15
15
  DiagramRuntimeState,
16
16
  DiagramSelection,
17
+ DiagramSnapshot,
17
18
  DiagramViewState,
18
19
  } from '../core/state.js';
19
20
  import {
@@ -77,7 +78,6 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
77
78
  private fullscreen = false;
78
79
  private fullscreenButtonVisible = true;
79
80
  private fullscreenLabels: DiagramFullscreenLabels = { enter: 'Enter fullscreen', exit: 'Exit fullscreen' };
80
- private linkValidator: LinkValidator | null = null;
81
81
  private readonly contextActions = new DiagramActionRegistry<ContextCommand, ContextActionContext>();
82
82
 
83
83
  constructor(options: DiagramOptions) {
@@ -105,7 +105,6 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
105
105
  }
106
106
 
107
107
  setLinkValidator(validator: LinkValidator | null): void {
108
- this.linkValidator = validator;
109
108
  this.canvas.setLinkValidator(validator === null ? null : ({ fromNode, fromPort, toNode, toPort }) => validator({
110
109
  fromNode: this.fromCanvasNode(fromNode),
111
110
  fromPort: this.fromCanvasPort(fromPort),
@@ -178,7 +177,7 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
178
177
  this.canvas.setGridSnap(enabled, size);
179
178
  }
180
179
 
181
- getGridSnap(): DiagramGridSettings {
180
+ getGridSnap(): DiagramSnapshot<DiagramGridSettings> {
182
181
  return this.canvas.getGridSnap();
183
182
  }
184
183
 
@@ -202,29 +201,33 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
202
201
  this.canvas.removeLink(this.toCanvasLink(link));
203
202
  }
204
203
 
205
- addPort(nodeId: string, direction: PortDirection, port: Port): void {
206
- this.canvas.addPort(nodeId, direction, this.toCanvasPort(port));
204
+ /** False when nothing changed: no such node or port, or the edit was a no-op. */
205
+ addPort(nodeId: string, direction: PortDirection, port: Port): boolean {
206
+ return this.canvas.addPort(nodeId, direction, this.toCanvasPort(port));
207
207
  }
208
208
 
209
- removePort(nodeId: string, direction: PortDirection, portId: string): void {
210
- this.canvas.removePort(nodeId, direction, portId);
209
+ /** False when nothing changed: no such node or port, or the edit was a no-op. */
210
+ removePort(nodeId: string, direction: PortDirection, portId: string): boolean {
211
+ return this.canvas.removePort(nodeId, direction, portId);
211
212
  }
212
213
 
213
- updatePortType(nodeId: string, direction: PortDirection, portId: string, type: string): void {
214
- this.canvas.updatePortType(nodeId, direction, portId, type);
214
+ /** False when nothing changed: no such node or port, or the edit was a no-op. */
215
+ updatePortType(nodeId: string, direction: PortDirection, portId: string, type: string): boolean {
216
+ return this.canvas.updatePortType(nodeId, direction, portId, type);
215
217
  }
216
218
 
217
219
  updatePort(nodeId: string, direction: PortDirection, portId: string, patch: PortUpdate): boolean {
218
220
  return this.canvas.updatePort(nodeId, direction, portId, patch);
219
221
  }
220
222
 
223
+ /** False when nothing changed: no such node or port, or the edit was a no-op. */
221
224
  setNodePorts(
222
225
  nodeId: string,
223
- inPorts: ReadonlyArray<{ key: string; name: string; description: string; type: string; maxLinks: number; availableTypes?: string[]; isDynamic?: boolean; dynamicMode?: string }>,
224
- outPorts: ReadonlyArray<{ key: string; name: string; description: string; type: string; maxLinks: number; availableTypes?: string[]; isDynamic?: boolean; dynamicMode?: string }>,
225
- ): void {
226
+ inPorts: ReadonlyArray<{ key: string; name: string; description: string; type: string; maxLinks: number; availableTypes?: string[]; isDynamic?: boolean; dynamicMode?: PortDynamicMode }>,
227
+ outPorts: ReadonlyArray<{ key: string; name: string; description: string; type: string; maxLinks: number; availableTypes?: string[]; isDynamic?: boolean; dynamicMode?: PortDynamicMode }>,
228
+ ): boolean {
226
229
  const current = this.canvas.findNode(nodeId);
227
- if (current === undefined) return;
230
+ if (current === undefined) return false;
228
231
  const convert = (port: typeof inPorts[number]): CanvasPortInit => ({
229
232
  id: port.key,
230
233
  name: port.name,
@@ -243,15 +246,17 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
243
246
  for (const sibling of current.outPorts.filter((port) => port.isSibling)) {
244
247
  if (!nextOut.some((port) => port.id === sibling.id)) nextOut.push(sibling.toInit());
245
248
  }
246
- this.canvas.setNodePorts(nodeId, nextIn, nextOut);
249
+ return this.canvas.setNodePorts(nodeId, nextIn, nextOut);
247
250
  }
248
251
 
249
- updateNode(nodeId: string, patch: { name?: string; description?: string; color?: string; border?: string }): void {
250
- this.canvas.updateNode(nodeId, patch);
252
+ /** False when nothing changed: no such node or port, or the edit was a no-op. */
253
+ updateNode(nodeId: string, patch: { name?: string; description?: string; color?: string; border?: string }): boolean {
254
+ return this.canvas.updateNode(nodeId, patch);
251
255
  }
252
256
 
253
- setNodeMessage(nodeId: string, message: string): void {
254
- this.canvas.updateNode(nodeId, { message });
257
+ /** False when nothing changed: no such node or port, or the edit was a no-op. */
258
+ setNodeMessage(nodeId: string, message: string): boolean {
259
+ return this.canvas.updateNode(nodeId, { message });
255
260
  }
256
261
 
257
262
  setNodeError(nodeId: string, message: string, options: NodeErrorOptions = {}): boolean {
@@ -262,11 +267,11 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
262
267
  return this.canvas.clearNodeError(nodeId, kind);
263
268
  }
264
269
 
265
- getRuntimeState(): DiagramRuntimeState {
270
+ getRuntimeState(): DiagramSnapshot<DiagramRuntimeState> {
266
271
  return this.canvas.getRuntimeState();
267
272
  }
268
273
 
269
- setRuntimeState(state: DiagramRuntimeState): void {
274
+ setRuntimeState(state: DiagramSnapshot<DiagramRuntimeState>): void {
270
275
  this.canvas.setRuntimeState(state);
271
276
  }
272
277
 
@@ -291,12 +296,14 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
291
296
  this.canvas.setGlobalError(message, kind);
292
297
  }
293
298
 
294
- setNodeParamValue(nodeId: string, paramName: string, value: string | undefined): void {
295
- this.canvas.setNodeParamValue(nodeId, paramName, value);
299
+ /** False when nothing changed: no such node or port, or the edit was a no-op. */
300
+ setNodeParamValue(nodeId: string, paramName: string, value: string | undefined): boolean {
301
+ return this.canvas.setNodeParamValue(nodeId, paramName, value);
296
302
  }
297
303
 
298
- setNodeName(nodeId: string, value: string): void {
299
- this.canvas.updateNode(nodeId, { name: value });
304
+ /** False when nothing changed: no such node or port, or the edit was a no-op. */
305
+ setNodeName(nodeId: string, value: string): boolean {
306
+ return this.canvas.updateNode(nodeId, { name: value });
300
307
  }
301
308
 
302
309
  /** Groups host-driven document edits into one undo/redo operation. */
@@ -312,7 +319,7 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
312
319
  this.canvas.setReadOnly(readonly);
313
320
  }
314
321
 
315
- getInteractionPermissions(): DiagramInteractionPermissions {
322
+ getInteractionPermissions(): DiagramSnapshot<DiagramInteractionPermissions> {
316
323
  return this.canvas.getInteractionPermissions();
317
324
  }
318
325
 
@@ -341,11 +348,11 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
341
348
  this.canvas.setZoom(scale);
342
349
  }
343
350
 
344
- getViewState(): DiagramViewState {
351
+ getViewState(): DiagramSnapshot<DiagramViewState> {
345
352
  return this.canvas.getViewState();
346
353
  }
347
354
 
348
- setViewState(state: DiagramViewState): void {
355
+ setViewState(state: DiagramSnapshot<DiagramViewState>): void {
349
356
  this.canvas.setViewState(state);
350
357
  this.overviewContainer?.classList.toggle('hidden', !state.overviewVisible);
351
358
  }
@@ -365,7 +372,7 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
365
372
  return this.canvas.takeScreenshot(options);
366
373
  }
367
374
 
368
- getSelection(): DiagramSelection {
375
+ getSelection(): DiagramSnapshot<DiagramSelection> {
369
376
  return this.canvas.getSelection();
370
377
  }
371
378
 
@@ -400,12 +407,12 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
400
407
  }
401
408
 
402
409
  /** Text of the fullscreen button, for a host that renders in another language. */
403
- setFullscreenLabels(labels: DiagramFullscreenLabels): void {
410
+ setFullscreenLabels(labels: DiagramSnapshot<DiagramFullscreenLabels>): void {
404
411
  this.fullscreenLabels = labels;
405
412
  this.updateFullscreenButton();
406
413
  }
407
414
 
408
- getFullscreenLabels(): DiagramFullscreenLabels {
415
+ getFullscreenLabels(): DiagramSnapshot<DiagramFullscreenLabels> {
409
416
  return { ...this.fullscreenLabels };
410
417
  }
411
418
 
@@ -760,51 +767,47 @@ export class StockSharpDiagram extends EventEmitter<DiagramEvents> {
760
767
 
761
768
  private registerContextActions(): void {
762
769
  const permissions = () => this.canvas.getInteractionPermissions();
763
- this.contextActions.register({
764
- id: 'undo',
765
- canExecute: () => this.canUndo(),
766
- execute: () => this.undo(),
767
- });
768
- this.contextActions.register({
769
- id: 'redo',
770
- canExecute: () => this.canRedo(),
771
- execute: () => this.redo(),
772
- });
773
- this.contextActions.register({
774
- id: 'cut',
775
- canExecute: ({ nodes }) => nodes.length > 0 && permissions().copy && permissions().deleteSelection,
776
- execute: () => this.cutSelection(),
777
- });
778
- this.contextActions.register({
779
- id: 'copy',
780
- canExecute: ({ nodes }) => nodes.length > 0 && permissions().copy,
781
- execute: () => this.copySelection(),
782
- });
783
- this.contextActions.register({
784
- id: 'paste',
785
- canExecute: () => this.canvas.hasClipboard() && permissions().paste,
786
- execute: () => this.pasteSelection(),
787
- });
788
- this.contextActions.register({
789
- id: 'open',
790
- canExecute: ({ nodes }) => nodes.length === 1 && nodes[0].openAction.length > 0,
791
- execute: ({ nodes }) => this.emit('nodeOpen', { nodes }),
792
- });
793
- this.contextActions.register({
794
- id: 'delete',
795
- canExecute: ({ selection }) => permissions().deleteSelection
796
- && (selection.nodeIds.length > 0 || selection.linkIds.length > 0),
797
- execute: () => this.canvas.deleteSelection(),
798
- });
799
- this.contextActions.register({
800
- id: 'properties',
801
- canExecute: ({ nodes }) => nodes.length > 0,
802
- execute: ({ nodes }) => this.emit('nodeProperties', { nodes }),
803
- });
804
- this.contextActions.register({
805
- id: 'help',
806
- canExecute: ({ nodes }) => this.helpEnabled && nodes.length > 0,
807
- execute: ({ nodes }) => this.emit('nodeHelp', { nodes }),
770
+ // Keyed by ContextCommand, so a command added to the union without an
771
+ // entry here fails to compile rather than quietly missing from the menu.
772
+ // Key order is menu order.
773
+ this.contextActions.registerAll({
774
+ undo: {
775
+ canExecute: () => this.canUndo(),
776
+ execute: () => this.undo(),
777
+ },
778
+ redo: {
779
+ canExecute: () => this.canRedo(),
780
+ execute: () => this.redo(),
781
+ },
782
+ cut: {
783
+ canExecute: ({ nodes }) => nodes.length > 0 && permissions().copy && permissions().deleteSelection,
784
+ execute: () => this.cutSelection(),
785
+ },
786
+ copy: {
787
+ canExecute: ({ nodes }) => nodes.length > 0 && permissions().copy,
788
+ execute: () => this.copySelection(),
789
+ },
790
+ paste: {
791
+ canExecute: () => this.canvas.hasClipboard() && permissions().paste,
792
+ execute: () => this.pasteSelection(),
793
+ },
794
+ open: {
795
+ canExecute: ({ nodes }) => nodes.length === 1 && nodes[0].openAction.length > 0,
796
+ execute: ({ nodes }) => this.emit('nodeOpen', { nodes }),
797
+ },
798
+ delete: {
799
+ canExecute: ({ selection }) => permissions().deleteSelection
800
+ && (selection.nodeIds.length > 0 || selection.linkIds.length > 0),
801
+ execute: () => this.canvas.deleteSelection(),
802
+ },
803
+ properties: {
804
+ canExecute: ({ nodes }) => nodes.length > 0,
805
+ execute: ({ nodes }) => this.emit('nodeProperties', { nodes }),
806
+ },
807
+ help: {
808
+ canExecute: ({ nodes }) => this.helpEnabled && nodes.length > 0,
809
+ execute: ({ nodes }) => this.emit('nodeHelp', { nodes }),
810
+ },
808
811
  });
809
812
  }
810
813
 
@@ -1,4 +1,5 @@
1
- import type { JsonObject } from '../core/model.js';
1
+ import { toPortDynamicMode } from '../core/model.js';
2
+ import type { JsonObject, PortDynamicMode } from '../core/model.js';
2
3
 
3
4
  // Core data model — node/port catalog (palette) and the live diagram (DiagramNode + Link).
4
5
 
@@ -29,7 +30,7 @@ export interface PortInit {
29
30
  /// True when this port can be added/removed at runtime on a node.
30
31
  isDynamic?: boolean;
31
32
  /// "" / "manual" / "onConnect" — see PalettePortDto.dynamicMode docs.
32
- dynamicMode?: string;
33
+ dynamicMode?: PortDynamicMode;
33
34
  /// True when this port was spawned by the grow-on-connect pipeline.
34
35
  /// Distinguishes anchor + runtime siblings on save (only siblings are
35
36
  /// persisted in the diagram blob; anchors come from the palette schema).
@@ -47,7 +48,7 @@ export class Port {
47
48
  maxLinks: number;
48
49
  availableTypes: string[];
49
50
  isDynamic: boolean;
50
- dynamicMode: string;
51
+ dynamicMode: PortDynamicMode;
51
52
  isSibling: boolean;
52
53
 
53
54
  constructor(init: PortInit) {
@@ -58,7 +59,7 @@ export class Port {
58
59
  this.maxLinks = typeof init.maxLinks === 'number' ? init.maxLinks : 0;
59
60
  this.availableTypes = init.availableTypes ?? [];
60
61
  this.isDynamic = init.isDynamic ?? false;
61
- this.dynamicMode = init.dynamicMode ?? '';
62
+ this.dynamicMode = toPortDynamicMode(init.dynamicMode);
62
63
  this.isSibling = init.isSibling ?? false;
63
64
  }
64
65
 
@@ -268,7 +269,7 @@ export interface PortData {
268
269
  /// the anchor port spawns a sibling
269
270
  /// on each link drop and the link is rerouted to the sibling.
270
271
  isDynamic?: boolean;
271
- dynamicMode?: string;
272
+ dynamicMode?: PortDynamicMode;
272
273
  /// True when this port was spawned by the dynamic-growth pipeline rather
273
274
  /// than declared on the palette element. Persists in the diagram blob so
274
275
  /// reload restores the same set of sibling sockets.
package/src/embed.ts CHANGED
@@ -11,6 +11,8 @@ import { StockSharpDiagram } from './diagram/stocksharp-diagram.js';
11
11
  import { StockSharpCatalog } from './diagram/catalog.js';
12
12
  import { DiagramNode, Link, Node, Port, PortType } from './diagram/types.js';
13
13
  import type { FullscreenRequestedPayload } from './diagram/api.js';
14
+ import { toPortDynamicMode } from './core/model.js';
15
+ import type { PortDynamicMode } from './core/model.js';
14
16
 
15
17
  interface PalettePort {
16
18
  key: string;
@@ -19,7 +21,7 @@ interface PalettePort {
19
21
  maxLinks?: number;
20
22
  availableTypes?: string[];
21
23
  isDynamic?: boolean;
22
- dynamicMode?: string;
24
+ dynamicMode?: PortDynamicMode;
23
25
  }
24
26
 
25
27
  interface PaletteElement {
@@ -166,59 +168,136 @@ function toDiagramNodes(
166
168
  // Walk the persisted Content.Value.Scheme.Model.{Nodes,Links} structure into
167
169
  // the thin scheme. Defensive at every field so a partially
168
170
  // broken document yields as many nodes as are readable; a wholly unusable one yields an empty scheme.
171
+ // Every reader below narrows rather than asserts: the input is arbitrary JSON
172
+ // from a URL or an inline <script>, so a type assertion here would only be a
173
+ // promise the data never made. Anything unreadable is skipped, never thrown on.
174
+ function isRecord(value: unknown): value is Record<string, unknown> {
175
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
176
+ }
177
+
178
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
179
+ return isRecord(value) ? value : undefined;
180
+ }
181
+
182
+ function asArray(value: unknown): unknown[] {
183
+ return Array.isArray(value) ? value : [];
184
+ }
185
+
186
+ function asString(value: unknown): string {
187
+ return typeof value === 'string' ? value : '';
188
+ }
189
+
190
+ function asFiniteNumber(value: unknown): number {
191
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
192
+ }
193
+
169
194
  function nodeName(node: Record<string, unknown>): string {
170
- const settings = node?.Settings as Record<string, unknown> | undefined;
171
- const parameters = settings?.Parameters as Record<string, unknown> | undefined;
172
- const nameParam = parameters?.Name as Record<string, unknown> | undefined;
173
- const nameVal = nameParam?.Value;
174
- if (typeof nameVal === 'string' && nameVal.length > 0)
175
- return nameVal;
176
- if (typeof node?.Figure === 'string' && (node.Figure as string).length > 0)
177
- return node.Figure as string;
178
- return typeof node?.Key === 'string' ? node.Key as string : '';
195
+ const named = asRecord(asRecord(asRecord(node.Settings)?.Parameters)?.Name)?.Value;
196
+ if (typeof named === 'string' && named.length > 0)
197
+ return named;
198
+ const figure = asString(node.Figure);
199
+ return figure.length > 0 ? figure : asString(node.Key);
179
200
  }
180
201
 
181
202
  function parseRawScheme(raw: unknown): DiagramEmbedScheme {
182
203
  const nodes: DiagramEmbedSchemeNode[] = [];
183
204
  const links: DiagramEmbedSchemeLink[] = [];
184
205
 
185
- const root = raw as Record<string, unknown>;
186
- const content = root?.Content as Record<string, unknown> | undefined;
187
- const value = content?.Value as Record<string, unknown> | undefined;
188
- const scheme = value?.Scheme as Record<string, unknown> | undefined;
189
- const model = scheme?.Model as Record<string, unknown> | undefined;
190
- if (!model)
206
+ const model = asRecord(asRecord(asRecord(asRecord(raw)?.Content)?.Value)?.Scheme)?.Model;
207
+ const fields = asRecord(model);
208
+ if (fields === undefined)
191
209
  return { nodes, links };
192
210
 
193
- for (const raw of (model.Nodes as Record<string, unknown>[] ?? [])) {
194
- const key = raw?.Key;
211
+ for (const entry of asArray(fields.Nodes)) {
212
+ const node = asRecord(entry);
213
+ if (node === undefined)
214
+ continue;
215
+ const key = node.Key;
195
216
  if (typeof key !== 'string' || key.length === 0)
196
217
  continue;
197
218
  nodes.push({
198
219
  id: key,
199
- typeId: typeof raw.TypeId === 'string' ? raw.TypeId : '',
200
- name: nodeName(raw),
201
- x: typeof raw.X === 'number' ? raw.X : 0,
202
- y: typeof raw.Y === 'number' ? raw.Y : 0,
220
+ typeId: asString(node.TypeId),
221
+ name: nodeName(node),
222
+ x: asFiniteNumber(node.X),
223
+ y: asFiniteNumber(node.Y),
203
224
  });
204
225
  }
205
226
 
206
- for (const raw of (model.Links as Record<string, unknown>[] ?? [])) {
207
- const from = raw?.From;
208
- const to = raw?.To;
227
+ for (const entry of asArray(fields.Links)) {
228
+ const link = asRecord(entry);
229
+ if (link === undefined)
230
+ continue;
231
+ const from = link.From;
232
+ const to = link.To;
209
233
  if (typeof from !== 'string' || typeof to !== 'string')
210
234
  continue;
211
235
  links.push({
212
236
  from,
213
- fromPort: typeof raw.FromPort === 'string' ? raw.FromPort : '',
237
+ fromPort: asString(link.FromPort),
214
238
  to,
215
- toPort: typeof raw.ToPort === 'string' ? raw.ToPort : '',
239
+ toPort: asString(link.ToPort),
216
240
  });
217
241
  }
218
242
 
219
243
  return { nodes, links };
220
244
  }
221
245
 
246
+ function parsePalettePorts(value: unknown): PalettePort[] {
247
+ const ports: PalettePort[] = [];
248
+ for (const entry of asArray(value)) {
249
+ const port = asRecord(entry);
250
+ if (port === undefined)
251
+ continue;
252
+ const key = asString(port.key);
253
+ if (key.length === 0)
254
+ continue;
255
+ ports.push({
256
+ key,
257
+ name: asString(port.name),
258
+ type: asString(port.type),
259
+ maxLinks: asFiniteNumber(port.maxLinks),
260
+ availableTypes: asArray(port.availableTypes).filter((item): item is string => typeof item === 'string'),
261
+ isDynamic: port.isDynamic === true,
262
+ dynamicMode: toPortDynamicMode(port.dynamicMode),
263
+ });
264
+ }
265
+ return ports;
266
+ }
267
+
268
+ function parsePalette(value: unknown): Palette {
269
+ const socketTypes: Palette['socketTypes'] = [];
270
+ const elements: PaletteElement[] = [];
271
+ const root = asRecord(value);
272
+ if (root === undefined)
273
+ return { socketTypes, elements };
274
+
275
+ for (const entry of asArray(root.socketTypes)) {
276
+ const socket = asRecord(entry);
277
+ const name = asString(socket?.name);
278
+ if (name.length === 0)
279
+ continue;
280
+ socketTypes.push({ name, color: asString(socket?.color) });
281
+ }
282
+
283
+ for (const entry of asArray(root.elements)) {
284
+ const element = asRecord(entry);
285
+ const typeId = asString(element?.typeId);
286
+ if (element === undefined || typeId.length === 0)
287
+ continue;
288
+ elements.push({
289
+ typeId,
290
+ name: asString(element.name),
291
+ groupName: asString(element.groupName),
292
+ icon: asString(element.icon),
293
+ inPorts: parsePalettePorts(element.inPorts),
294
+ outPorts: parsePalettePorts(element.outPorts),
295
+ });
296
+ }
297
+
298
+ return { socketTypes, elements };
299
+ }
300
+
222
301
  export async function renderScheme(
223
302
  div: HTMLElement,
224
303
  paletteUrl: string,
@@ -237,7 +316,12 @@ async function renderSchemeAtRevision(
237
316
  ): Promise<DiagramEmbedHandle | null> {
238
317
  if (renderRevisions.get(div) !== revision)
239
318
  return null;
240
- const palette = (await fetch(paletteUrl).then((r) => r.json())) as Palette;
319
+ // Same treatment the schema fetch gets: a non-2xx answer is a failure, not a
320
+ // body to parse. Callers that degrade already catch it.
321
+ const response = await fetch(paletteUrl);
322
+ if (!response.ok)
323
+ throw new Error(`Palette request to ${paletteUrl} failed with status ${response.status}.`);
324
+ const palette = parsePalette(await response.json());
241
325
  if (renderRevisions.get(div) !== revision)
242
326
  return null;
243
327
  const catalog = buildCatalog(palette);
@@ -412,6 +496,25 @@ function stopDisconnectedHostObserverWhenIdle(): void {
412
496
  disconnectedHostObserver = null;
413
497
  }
414
498
 
499
+ interface EmbedErrorTexts {
500
+ load: string;
501
+ empty: string;
502
+ draw: string;
503
+ }
504
+
505
+ // The host overrides the inline notes with data-diagram-errors="load|empty|draw".
506
+ // Each slot falls back when it is empty rather than when it is absent: split on a
507
+ // missing attribute yields [''], one present empty string, so a destructuring
508
+ // default would never fire and the note would render blank.
509
+ function errorTexts(div: HTMLElement): EmbedErrorTexts {
510
+ const parts = (div.dataset.diagramErrors ?? '').split('|');
511
+ return {
512
+ load: parts[0] || 'Diagram source could not be loaded.',
513
+ empty: parts[1] || 'Diagram is empty or malformed.',
514
+ draw: parts[2] || 'Diagram could not be rendered.',
515
+ };
516
+ }
517
+
415
518
  function note(div: HTMLElement, message: string, revision: number): void {
416
519
  if (renderRevisions.get(div) !== revision) return;
417
520
  disposeActiveRender(div);
@@ -429,30 +532,29 @@ export async function renderFromSource(
429
532
  options: DiagramEmbedOptions = {},
430
533
  ): Promise<DiagramEmbedHandle | null> {
431
534
  const revision = beginRender(div);
432
- const errors = (div.dataset.diagramErrors ?? '').split('|');
433
- const [errLoad = 'Diagram source could not be loaded.', errEmpty = 'Diagram is empty or malformed.', errDraw = 'Diagram could not be rendered.'] = errors;
535
+ const errors = errorTexts(div);
434
536
 
435
537
  let raw: unknown;
436
538
  try {
437
539
  const resp = await fetch(srcUrl);
438
- if (!resp.ok) { note(div, errLoad, revision); return null; }
540
+ if (!resp.ok) { note(div, errors.load, revision); return null; }
439
541
  const text = (await resp.text()).replace(/^/, '');
440
542
  raw = JSON.parse(text);
441
543
  } catch {
442
- note(div, errLoad, revision);
544
+ note(div, errors.load, revision);
443
545
  return null;
444
546
  }
445
547
 
446
548
  const scheme = parseRawScheme(raw);
447
549
  if (scheme.nodes.length === 0) {
448
- note(div, errEmpty, revision);
550
+ note(div, errors.empty, revision);
449
551
  return null;
450
552
  }
451
553
 
452
554
  try {
453
555
  return await renderSchemeAtRevision(div, paletteUrl, scheme, revision, options);
454
556
  } catch {
455
- note(div, errDraw, revision);
557
+ note(div, errors.draw, revision);
456
558
  return null;
457
559
  }
458
560
  }
@@ -467,27 +569,26 @@ export async function renderFromInline(
467
569
  options: DiagramEmbedOptions = {},
468
570
  ): Promise<DiagramEmbedHandle | null> {
469
571
  const revision = beginRender(div);
470
- const errors = (div.dataset.diagramErrors ?? '').split('|');
471
- const [, errEmpty = 'Diagram is empty or malformed.', errDraw = 'Diagram could not be rendered.'] = errors;
572
+ const errors = errorTexts(div);
472
573
 
473
574
  let raw: unknown;
474
575
  try {
475
576
  raw = JSON.parse(json);
476
577
  } catch {
477
- note(div, errEmpty, revision);
578
+ note(div, errors.empty, revision);
478
579
  return null;
479
580
  }
480
581
 
481
582
  const scheme = parseRawScheme(raw);
482
583
  if (scheme.nodes.length === 0) {
483
- note(div, errEmpty, revision);
584
+ note(div, errors.empty, revision);
484
585
  return null;
485
586
  }
486
587
 
487
588
  try {
488
589
  return await renderSchemeAtRevision(div, paletteUrl, scheme, revision, options);
489
590
  } catch {
490
- note(div, errDraw, revision);
591
+ note(div, errors.draw, revision);
491
592
  return null;
492
593
  }
493
594
  }
package/src/index.ts CHANGED
@@ -36,6 +36,7 @@ export type {
36
36
  JsonObject,
37
37
  JsonPrimitive,
38
38
  JsonValue,
39
+ PortDynamicMode,
39
40
  } from './core/model.js';
40
41
 
41
42
  export {
@@ -43,6 +44,7 @@ export {
43
44
  createDiagramNodeRuntimeState,
44
45
  createDiagramPortRuntimeState,
45
46
  createDiagramRuntimeState,
47
+ cloneDiagramNodeErrors,
46
48
  cloneDiagramRuntimeState,
47
49
  createDiagramSelection,
48
50
  createDiagramViewState,
@@ -64,6 +66,7 @@ export type {
64
66
  DiagramGlobalErrorKind,
65
67
  DiagramInteractionPermissions,
66
68
  DiagramNodeErrorKind,
69
+ DiagramNodeErrors,
67
70
  DiagramNodePortRuntimeState,
68
71
  DiagramNodeRuntimeState,
69
72
  DiagramPortDirection,
@@ -71,6 +74,7 @@ export type {
71
74
  DiagramRuntimeState,
72
75
  DiagramSelectedPort,
73
76
  DiagramSelection,
77
+ DiagramSnapshot,
74
78
  DiagramViewState,
75
79
  } from './core/state.js';
76
80