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