@3plate/graph-core 0.1.9 → 0.1.12

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.
package/dist/index.cjs CHANGED
@@ -3678,7 +3678,7 @@ var Ingest = class {
3678
3678
  * Apply an incoming ingest message to the API.
3679
3679
  * - snapshot: rebuild state from nodes/edges (clears prior history)
3680
3680
  * - update: apply incremental update
3681
- * - history: initialize from a set of frames (clears prior history)
3681
+ * - history: initialize from a set of updates (clears prior history)
3682
3682
  */
3683
3683
  async apply(msg) {
3684
3684
  switch (msg.type) {
@@ -3699,7 +3699,7 @@ var Ingest = class {
3699
3699
  break;
3700
3700
  }
3701
3701
  case "history": {
3702
- await this.api.replaceHistory(msg.frames);
3702
+ await this.api.replaceHistory(msg.history);
3703
3703
  break;
3704
3704
  }
3705
3705
  }
@@ -3717,11 +3717,11 @@ var WebSocketSource = class {
3717
3717
  connectStartTime = null;
3718
3718
  totalTimeoutMs = 1e4;
3719
3719
  totalTimeoutTimer = null;
3720
- constructor(url, onMessage, onStatus, reconnectMs = 1500) {
3721
- this.url = url;
3722
- this.onMessage = onMessage;
3723
- this.onStatus = onStatus;
3724
- this.reconnectMs = reconnectMs;
3720
+ constructor(args) {
3721
+ this.url = args.url;
3722
+ this.onMessage = args.onMessage;
3723
+ this.onStatus = args.onStatus;
3724
+ this.reconnectMs = args.reconnectMs ?? 1500;
3725
3725
  }
3726
3726
  connect() {
3727
3727
  this.closedByUser = false;
@@ -3822,11 +3822,11 @@ var FileSource = class {
3822
3822
  lastContent = "";
3823
3823
  intervalMs = 1e3;
3824
3824
  closed = false;
3825
- constructor(url, onMessage, onStatus, intervalMs = 1e3) {
3826
- this.url = url;
3827
- this.onMessage = onMessage;
3828
- this.onStatus = onStatus;
3829
- this.intervalMs = intervalMs;
3825
+ constructor(args) {
3826
+ this.url = args.url;
3827
+ this.onMessage = args.onMessage;
3828
+ this.onStatus = args.onStatus;
3829
+ this.intervalMs = args.intervalMs ?? 1e3;
3830
3830
  }
3831
3831
  async connect() {
3832
3832
  this.closed = false;
@@ -3888,6 +3888,68 @@ var FileSource = class {
3888
3888
  }
3889
3889
  };
3890
3890
 
3891
+ // src/api/sources/FileSystemSource.ts
3892
+ var FileSystemSource = class {
3893
+ handle = null;
3894
+ onMessage;
3895
+ onStatus;
3896
+ timer = null;
3897
+ lastSize = 0;
3898
+ filename;
3899
+ intervalMs;
3900
+ constructor(args) {
3901
+ this.filename = args.filename;
3902
+ this.onMessage = args.onMessage;
3903
+ this.onStatus = args.onStatus;
3904
+ this.intervalMs = args.intervalMs ?? 1e3;
3905
+ }
3906
+ async openDirectory() {
3907
+ try {
3908
+ const dir = await window.showDirectoryPicker?.();
3909
+ if (!dir) throw new Error("File System Access not supported or cancelled");
3910
+ const handle = await dir.getFileHandle(this.filename, { create: false });
3911
+ this.handle = handle;
3912
+ this.onStatus?.("opened", { file: this.filename });
3913
+ this.lastSize = 0;
3914
+ this.startPolling();
3915
+ } catch (e) {
3916
+ this.onStatus?.("error", e);
3917
+ }
3918
+ }
3919
+ close() {
3920
+ if (this.timer) {
3921
+ window.clearInterval(this.timer);
3922
+ this.timer = null;
3923
+ }
3924
+ this.handle = null;
3925
+ this.onStatus?.("closed");
3926
+ }
3927
+ startPolling() {
3928
+ if (this.timer) window.clearInterval(this.timer);
3929
+ this.timer = window.setInterval(() => this.readNewLines(), this.intervalMs);
3930
+ }
3931
+ async readNewLines() {
3932
+ try {
3933
+ if (!this.handle) return;
3934
+ this.onStatus?.("reading");
3935
+ const file = await this.handle.getFile();
3936
+ if (file.size === this.lastSize) return;
3937
+ const slice = await file.slice(this.lastSize).text();
3938
+ this.lastSize = file.size;
3939
+ const lines = slice.split("\n").map((l) => l.trim()).filter(Boolean);
3940
+ for (const line of lines) {
3941
+ try {
3942
+ const obj = JSON.parse(line);
3943
+ this.onMessage(obj);
3944
+ } catch {
3945
+ }
3946
+ }
3947
+ } catch (e) {
3948
+ this.onStatus?.("error", e);
3949
+ }
3950
+ }
3951
+ };
3952
+
3891
3953
  // src/api/api.ts
3892
3954
  var log11 = logger("api");
3893
3955
  var API = class {
@@ -3970,29 +4032,19 @@ var API = class {
3970
4032
  /** Connect to the configured ingestion source */
3971
4033
  connectIngestion() {
3972
4034
  if (!this.ingestionConfig || !this.ingest) return;
3973
- const handleMessage = (msg) => {
3974
- this.ingest.apply(msg);
4035
+ const args = {
4036
+ ...this.ingestionConfig,
4037
+ onMessage: (msg) => {
4038
+ this.ingest.apply(msg);
4039
+ }
3975
4040
  };
3976
- switch (this.ingestionConfig.type) {
3977
- case "websocket":
3978
- this.ingestionSource = new WebSocketSource(
3979
- this.ingestionConfig.url,
3980
- handleMessage,
3981
- void 0,
3982
- this.ingestionConfig.reconnectMs
3983
- );
3984
- this.ingestionSource.connect();
3985
- break;
3986
- case "file":
3987
- this.ingestionSource = new FileSource(
3988
- this.ingestionConfig.url,
3989
- handleMessage,
3990
- void 0,
3991
- this.ingestionConfig.intervalMs
3992
- );
3993
- this.ingestionSource.connect();
3994
- break;
3995
- }
4041
+ const source = {
4042
+ "websocket": WebSocketSource,
4043
+ "file": FileSource,
4044
+ "filesystem": FileSystemSource
4045
+ }[this.ingestionConfig.type];
4046
+ this.ingestionSource = new source[this.ingestionConfig.type](args);
4047
+ this.ingestionSource?.connect();
3996
4048
  }
3997
4049
  /** Disconnect from the ingestion source */
3998
4050
  disconnectIngestion() {
@@ -4021,9 +4073,9 @@ var API = class {
4021
4073
  this.canvas.editMode.editable = editable;
4022
4074
  }
4023
4075
  /** Replace entire history (clears prior) */
4024
- async replaceHistory(frames) {
4076
+ async replaceHistory(history) {
4025
4077
  this.reset();
4026
- this.history = frames;
4078
+ this.history = history;
4027
4079
  await this.applyHistory();
4028
4080
  }
4029
4081
  /** Rebuild from snapshot (nodes/edges) */
@@ -4490,8 +4542,8 @@ var API = class {
4490
4542
  }
4491
4543
  } else if (prev.history && isHistoryPrefix(prev.history, props.history)) {
4492
4544
  const prevLength = prev.history.length;
4493
- const newFrames = props.history.slice(prevLength);
4494
- for (const frame of newFrames) {
4545
+ const newUpdates = props.history.slice(prevLength);
4546
+ for (const frame of newUpdates) {
4495
4547
  this.update((u) => {
4496
4548
  if (frame.addNodes) u.addNodes(...frame.addNodes);
4497
4549
  if (frame.removeNodes) u.deleteNodes(...frame.removeNodes);
@@ -4578,68 +4630,6 @@ function shallowEqualUpdate(a, b) {
4578
4630
  return true;
4579
4631
  }
4580
4632
 
4581
- // src/api/sources/FileSystemSource.ts
4582
- var FileSystemSource = class {
4583
- handle = null;
4584
- onMessage;
4585
- onStatus;
4586
- timer = null;
4587
- lastSize = 0;
4588
- filename;
4589
- intervalMs;
4590
- constructor(onMessage, onStatus, filename = "graph.ndjson", intervalMs = 1e3) {
4591
- this.onMessage = onMessage;
4592
- this.onStatus = onStatus;
4593
- this.filename = filename;
4594
- this.intervalMs = intervalMs;
4595
- }
4596
- async openDirectory() {
4597
- try {
4598
- const dir = await window.showDirectoryPicker?.();
4599
- if (!dir) throw new Error("File System Access not supported or cancelled");
4600
- const handle = await dir.getFileHandle(this.filename, { create: false });
4601
- this.handle = handle;
4602
- this.onStatus?.("opened", { file: this.filename });
4603
- this.lastSize = 0;
4604
- this.startPolling();
4605
- } catch (e) {
4606
- this.onStatus?.("error", e);
4607
- }
4608
- }
4609
- close() {
4610
- if (this.timer) {
4611
- window.clearInterval(this.timer);
4612
- this.timer = null;
4613
- }
4614
- this.handle = null;
4615
- this.onStatus?.("closed");
4616
- }
4617
- startPolling() {
4618
- if (this.timer) window.clearInterval(this.timer);
4619
- this.timer = window.setInterval(() => this.readNewLines(), this.intervalMs);
4620
- }
4621
- async readNewLines() {
4622
- try {
4623
- if (!this.handle) return;
4624
- this.onStatus?.("reading");
4625
- const file = await this.handle.getFile();
4626
- if (file.size === this.lastSize) return;
4627
- const slice = await file.slice(this.lastSize).text();
4628
- this.lastSize = file.size;
4629
- const lines = slice.split("\n").map((l) => l.trim()).filter(Boolean);
4630
- for (const line of lines) {
4631
- try {
4632
- const obj = JSON.parse(line);
4633
- this.onMessage(obj);
4634
- } catch {
4635
- }
4636
- }
4637
- } catch (e) {
4638
- this.onStatus?.("error", e);
4639
- }
4640
- }
4641
- };
4642
-
4643
4633
  // src/playground/playground.ts
4644
4634
  var import_styles2 = __toESM(require("./styles.css?raw"), 1);
4645
4635
  var Playground = class {
@@ -4918,10 +4908,18 @@ var Playground = class {
4918
4908
  this.disconnectAllSources();
4919
4909
  if (example.source.type === "websocket") {
4920
4910
  this.wsUrl = example.source.url;
4921
- this.wsSource = new WebSocketSource(example.source.url, this.handleIngestMessage.bind(this), this.updateWsStatus);
4911
+ this.wsSource = new WebSocketSource({
4912
+ url: example.source.url,
4913
+ onMessage: this.handleIngestMessage.bind(this),
4914
+ onStatus: this.updateWsStatus
4915
+ });
4922
4916
  this.wsSource.connect();
4923
4917
  } else if (example.source.type === "file") {
4924
- this.fileSource = new FileSource(example.source.path, this.handleIngestMessage.bind(this), this.updateFileStatus);
4918
+ this.fileSource = new FileSource({
4919
+ url: example.source.path,
4920
+ onMessage: this.handleIngestMessage.bind(this),
4921
+ onStatus: this.updateFileStatus
4922
+ });
4925
4923
  this.fileSource.connect();
4926
4924
  }
4927
4925
  }
@@ -5321,7 +5319,11 @@ var Playground = class {
5321
5319
  if (this.wsSource) {
5322
5320
  this.wsSource.disconnect();
5323
5321
  }
5324
- this.wsSource = new WebSocketSource(url, this.handleIngestMessage.bind(this), this.updateWsStatus);
5322
+ this.wsSource = new WebSocketSource({
5323
+ url,
5324
+ onMessage: this.handleIngestMessage.bind(this),
5325
+ onStatus: this.updateWsStatus
5326
+ });
5325
5327
  this.wsSource.connect();
5326
5328
  this.updateSourceModal();
5327
5329
  }
@@ -5342,7 +5344,11 @@ var Playground = class {
5342
5344
  }
5343
5345
  async handleOpenFolder() {
5344
5346
  if (!this.fsSource) {
5345
- this.fsSource = new FileSystemSource(this.handleIngestMessage.bind(this), this.updateFsStatus);
5347
+ this.fsSource = new FileSystemSource({
5348
+ filename: "graph.ndjson",
5349
+ onMessage: this.handleIngestMessage.bind(this),
5350
+ onStatus: this.updateFsStatus
5351
+ });
5346
5352
  }
5347
5353
  this.updateSourceModal();
5348
5354
  await this.fsSource.openDirectory();
package/dist/index.d.cts CHANGED
@@ -3,6 +3,7 @@ type MergeOrder = Side[];
3
3
  type NodeAlign = 'natural' | 'top' | 'bottom' | 'left' | 'right';
4
4
  type Orientation = 'TB' | 'BT' | 'LR' | 'RL';
5
5
  type Nav = 'first' | 'last' | 'prev' | 'next';
6
+ type PortStyle = 'inside' | 'outside' | 'custom';
6
7
  type LayoutStep = 'alignChildren' | 'alignParents' | 'compact';
7
8
  type Dims = {
8
9
  w: number;
@@ -11,19 +12,112 @@ type Dims = {
11
12
 
12
13
  type MarkerType = 'arrow' | 'circle' | 'diamond' | 'bar' | 'none';
13
14
 
15
+ type SnapshotMessage<N, E> = {
16
+ type: 'snapshot';
17
+ nodes: N[];
18
+ edges: E[];
19
+ description?: string;
20
+ };
21
+ type UpdateMessage<N, E> = {
22
+ type: 'update';
23
+ description?: string;
24
+ } & Update<N, E>;
25
+ type HistoryMessage<N, E> = {
26
+ type: 'history';
27
+ history: Update<N, E>[];
28
+ };
29
+ type IngestMessage<N, E> = SnapshotMessage<N, E> | UpdateMessage<N, E> | HistoryMessage<N, E>;
14
30
  /**
15
- * Ingestion source configuration.
16
- * Used to connect to an external data source for graph updates.
31
+ * Ingest class handles applying ingest messages to an API instance.
32
+ * This is the core ingestion functionality, separate from UI concerns.
17
33
  */
18
- type IngestionConfig = {
34
+ declare class Ingest<N, E> {
35
+ api: API<N, E>;
36
+ constructor(api: API<N, E>);
37
+ /**
38
+ * Apply an incoming ingest message to the API.
39
+ * - snapshot: rebuild state from nodes/edges (clears prior history)
40
+ * - update: apply incremental update
41
+ * - history: initialize from a set of updates (clears prior history)
42
+ */
43
+ apply(msg: IngestMessage<N, E>): Promise<void>;
44
+ }
45
+
46
+ type WebSocketStatus = 'connecting' | 'connected' | 'reconnecting' | 'closed' | 'error';
47
+ type WebSocketStatusListener = (status: WebSocketStatus, detail?: any) => void;
48
+ type WebSocketSourceArgs<N, E> = {
49
+ url: string;
50
+ onMessage: (msg: IngestMessage<N, E>) => void;
51
+ onStatus?: WebSocketStatusListener;
52
+ reconnectMs?: number;
53
+ };
54
+ declare class WebSocketSource<N, E> {
55
+ private url;
56
+ private ws;
57
+ private onMessage;
58
+ private onStatus?;
59
+ private reconnectMs;
60
+ private closedByUser;
61
+ private connectStartTime;
62
+ private totalTimeoutMs;
63
+ private totalTimeoutTimer;
64
+ constructor(args: WebSocketSourceArgs<N, E>);
65
+ connect(): void;
66
+ disconnect(): void;
67
+ private startTotalTimeout;
68
+ private clearTotalTimeout;
69
+ private open;
70
+ }
71
+
72
+ type FileStatus = 'idle' | 'opened' | 'reading' | 'error' | 'closed';
73
+ type FileStatusListener = (status: FileStatus, detail?: any) => void;
74
+ type FileSourceArgs<N, E> = {
75
+ url: string;
76
+ onMessage: (msg: IngestMessage<N, E>) => void;
77
+ onStatus?: FileStatusListener;
78
+ intervalMs?: number;
79
+ };
80
+ declare class FileSource<N, E> {
81
+ private url;
82
+ private onMessage;
83
+ private onStatus?;
84
+ private timer;
85
+ private lastETag;
86
+ private lastContent;
87
+ private intervalMs;
88
+ private closed;
89
+ constructor(args: FileSourceArgs<N, E>);
90
+ connect(): Promise<void>;
91
+ close(): void;
92
+ private startPolling;
93
+ private poll;
94
+ }
95
+
96
+ /** WebSocket ingestion configuration */
97
+ type WebSocketIngestionConfig = {
19
98
  type: 'websocket';
99
+ /** WebSocket URL */
20
100
  url: string;
101
+ /** Reconnect interval in milliseconds */
21
102
  reconnectMs?: number;
22
- } | {
103
+ /** Status listener */
104
+ onStatus?: WebSocketStatusListener;
105
+ };
106
+ /** File ingestion configuration */
107
+ type FileIngestionConfig = {
23
108
  type: 'file';
109
+ /** File URL */
24
110
  url: string;
111
+ /** Polling interval in milliseconds */
25
112
  intervalMs?: number;
113
+ /** Status listener */
114
+ onStatus?: FileStatusListener;
26
115
  };
116
+ /**
117
+ * Ingestion source configuration.
118
+ * Used to connect to an external data source for graph updates.
119
+ */
120
+ type IngestionConfig = WebSocketIngestionConfig | FileIngestionConfig;
27
121
  /**
28
122
  * Arguments to the API constructor.
29
123
  *
@@ -406,7 +500,7 @@ declare class API<N, E> {
406
500
  /** Toggle canvas editable mode without re-creating the graph */
407
501
  setEditable(editable: boolean): void;
408
502
  /** Replace entire history (clears prior) */
409
- replaceHistory(frames: Update<N, E>[]): Promise<void>;
503
+ replaceHistory(history: Update<N, E>[]): Promise<void>;
410
504
  /** Rebuild from snapshot (nodes/edges) */
411
505
  replaceSnapshot(nodes: N[], edges: E[], description?: string): Promise<void>;
412
506
  private get graph();
@@ -487,57 +581,13 @@ declare class API<N, E> {
487
581
  destroy(): void;
488
582
  }
489
583
 
490
- type SnapshotMessage<N, E> = {
491
- type: 'snapshot';
492
- nodes: N[];
493
- edges: E[];
494
- description?: string;
495
- };
496
- type UpdateMessage<N, E> = {
497
- type: 'update';
498
- description?: string;
499
- } & Update<N, E>;
500
- type HistoryMessage<N, E> = {
501
- type: 'history';
502
- frames: Update<N, E>[];
584
+ type StatusListener = (status: 'idle' | 'opened' | 'reading' | 'error' | 'closed', detail?: any) => void;
585
+ type FileSystemSourceArgs<N, E> = {
586
+ filename: string;
587
+ onMessage: (msg: IngestMessage<N, E>) => void;
588
+ onStatus?: StatusListener;
589
+ intervalMs?: number;
503
590
  };
504
- type IngestMessage<N, E> = SnapshotMessage<N, E> | UpdateMessage<N, E> | HistoryMessage<N, E>;
505
- /**
506
- * Ingest class handles applying ingest messages to an API instance.
507
- * This is the core ingestion functionality, separate from UI concerns.
508
- */
509
- declare class Ingest<N, E> {
510
- api: API<N, E>;
511
- constructor(api: API<N, E>);
512
- /**
513
- * Apply an incoming ingest message to the API.
514
- * - snapshot: rebuild state from nodes/edges (clears prior history)
515
- * - update: apply incremental update
516
- * - history: initialize from a set of frames (clears prior history)
517
- */
518
- apply(msg: IngestMessage<N, E>): Promise<void>;
519
- }
520
-
521
- type StatusListener$2 = (status: 'connecting' | 'connected' | 'reconnecting' | 'closed' | 'error', detail?: any) => void;
522
- declare class WebSocketSource<N, E> {
523
- private url;
524
- private ws;
525
- private onMessage;
526
- private onStatus?;
527
- private reconnectMs;
528
- private closedByUser;
529
- private connectStartTime;
530
- private totalTimeoutMs;
531
- private totalTimeoutTimer;
532
- constructor(url: string, onMessage: (msg: IngestMessage<N, E>) => void, onStatus?: StatusListener$2, reconnectMs?: number);
533
- connect(): void;
534
- disconnect(): void;
535
- private startTotalTimeout;
536
- private clearTotalTimeout;
537
- private open;
538
- }
539
-
540
- type StatusListener$1 = (status: 'idle' | 'opened' | 'reading' | 'error' | 'closed', detail?: any) => void;
541
591
  declare class FileSystemSource<N, E> {
542
592
  private handle;
543
593
  private onMessage;
@@ -546,30 +596,13 @@ declare class FileSystemSource<N, E> {
546
596
  private lastSize;
547
597
  private filename;
548
598
  private intervalMs;
549
- constructor(onMessage: (msg: IngestMessage<N, E>) => void, onStatus?: StatusListener$1, filename?: string, intervalMs?: number);
599
+ constructor(args: FileSystemSourceArgs<N, E>);
550
600
  openDirectory(): Promise<void>;
551
601
  close(): void;
552
602
  private startPolling;
553
603
  private readNewLines;
554
604
  }
555
605
 
556
- type StatusListener = (status: 'idle' | 'opened' | 'reading' | 'error' | 'closed', detail?: any) => void;
557
- declare class FileSource<N, E> {
558
- private url;
559
- private onMessage;
560
- private onStatus?;
561
- private timer;
562
- private lastETag;
563
- private lastContent;
564
- private intervalMs;
565
- private closed;
566
- constructor(url: string, onMessage: (msg: IngestMessage<N, E>) => void, onStatus?: StatusListener, intervalMs?: number);
567
- connect(): Promise<void>;
568
- close(): void;
569
- private startPolling;
570
- private poll;
571
- }
572
-
573
606
  /**
574
607
  * Types for the Playground component
575
608
  */
@@ -685,4 +718,4 @@ declare class Playground {
685
718
 
686
719
  declare function graph<N, E>(args?: APIArguments<N, E>): Promise<API<N, E>>;
687
720
 
688
- export { API, type APIArguments, type APIOptions, type EventsOptions, type Example, type ExampleEdge, type ExampleNode, type ExampleOptions, FileSource, FileSystemSource, type HistoryMessage, Ingest, type IngestMessage, type IngestionConfig, Playground, type PlaygroundOptions, type SnapshotMessage, type Update, type UpdateMessage, Updater, WebSocketSource, graph as default, graph };
721
+ export { API, type APIArguments, type APIOptions, type CanvasTheme, type ColorMode, type EdgeProps, type EdgeTheme, type EventsOptions, type Example, type ExampleEdge, type ExampleNode, type ExampleOptions, FileSource, type FileSourceArgs, type FileStatus, type FileStatusListener, FileSystemSource, type FileSystemSourceArgs, type HistoryMessage, Ingest, type IngestMessage, type IngestionConfig, type NewEdge, type NewNode, type NodeAlign, type NodeProps, type NodeTheme, type Orientation, Playground, type PlaygroundOptions, type PortProps, type PortStyle, type PortTheme, type RenderNode, type SnapshotMessage, type ThemeVars, type Update, type UpdateMessage, Updater, WebSocketSource, type WebSocketSourceArgs, type WebSocketStatus, type WebSocketStatusListener, graph as default, graph };
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ type MergeOrder = Side[];
3
3
  type NodeAlign = 'natural' | 'top' | 'bottom' | 'left' | 'right';
4
4
  type Orientation = 'TB' | 'BT' | 'LR' | 'RL';
5
5
  type Nav = 'first' | 'last' | 'prev' | 'next';
6
+ type PortStyle = 'inside' | 'outside' | 'custom';
6
7
  type LayoutStep = 'alignChildren' | 'alignParents' | 'compact';
7
8
  type Dims = {
8
9
  w: number;
@@ -11,19 +12,112 @@ type Dims = {
11
12
 
12
13
  type MarkerType = 'arrow' | 'circle' | 'diamond' | 'bar' | 'none';
13
14
 
15
+ type SnapshotMessage<N, E> = {
16
+ type: 'snapshot';
17
+ nodes: N[];
18
+ edges: E[];
19
+ description?: string;
20
+ };
21
+ type UpdateMessage<N, E> = {
22
+ type: 'update';
23
+ description?: string;
24
+ } & Update<N, E>;
25
+ type HistoryMessage<N, E> = {
26
+ type: 'history';
27
+ history: Update<N, E>[];
28
+ };
29
+ type IngestMessage<N, E> = SnapshotMessage<N, E> | UpdateMessage<N, E> | HistoryMessage<N, E>;
14
30
  /**
15
- * Ingestion source configuration.
16
- * Used to connect to an external data source for graph updates.
31
+ * Ingest class handles applying ingest messages to an API instance.
32
+ * This is the core ingestion functionality, separate from UI concerns.
17
33
  */
18
- type IngestionConfig = {
34
+ declare class Ingest<N, E> {
35
+ api: API<N, E>;
36
+ constructor(api: API<N, E>);
37
+ /**
38
+ * Apply an incoming ingest message to the API.
39
+ * - snapshot: rebuild state from nodes/edges (clears prior history)
40
+ * - update: apply incremental update
41
+ * - history: initialize from a set of updates (clears prior history)
42
+ */
43
+ apply(msg: IngestMessage<N, E>): Promise<void>;
44
+ }
45
+
46
+ type WebSocketStatus = 'connecting' | 'connected' | 'reconnecting' | 'closed' | 'error';
47
+ type WebSocketStatusListener = (status: WebSocketStatus, detail?: any) => void;
48
+ type WebSocketSourceArgs<N, E> = {
49
+ url: string;
50
+ onMessage: (msg: IngestMessage<N, E>) => void;
51
+ onStatus?: WebSocketStatusListener;
52
+ reconnectMs?: number;
53
+ };
54
+ declare class WebSocketSource<N, E> {
55
+ private url;
56
+ private ws;
57
+ private onMessage;
58
+ private onStatus?;
59
+ private reconnectMs;
60
+ private closedByUser;
61
+ private connectStartTime;
62
+ private totalTimeoutMs;
63
+ private totalTimeoutTimer;
64
+ constructor(args: WebSocketSourceArgs<N, E>);
65
+ connect(): void;
66
+ disconnect(): void;
67
+ private startTotalTimeout;
68
+ private clearTotalTimeout;
69
+ private open;
70
+ }
71
+
72
+ type FileStatus = 'idle' | 'opened' | 'reading' | 'error' | 'closed';
73
+ type FileStatusListener = (status: FileStatus, detail?: any) => void;
74
+ type FileSourceArgs<N, E> = {
75
+ url: string;
76
+ onMessage: (msg: IngestMessage<N, E>) => void;
77
+ onStatus?: FileStatusListener;
78
+ intervalMs?: number;
79
+ };
80
+ declare class FileSource<N, E> {
81
+ private url;
82
+ private onMessage;
83
+ private onStatus?;
84
+ private timer;
85
+ private lastETag;
86
+ private lastContent;
87
+ private intervalMs;
88
+ private closed;
89
+ constructor(args: FileSourceArgs<N, E>);
90
+ connect(): Promise<void>;
91
+ close(): void;
92
+ private startPolling;
93
+ private poll;
94
+ }
95
+
96
+ /** WebSocket ingestion configuration */
97
+ type WebSocketIngestionConfig = {
19
98
  type: 'websocket';
99
+ /** WebSocket URL */
20
100
  url: string;
101
+ /** Reconnect interval in milliseconds */
21
102
  reconnectMs?: number;
22
- } | {
103
+ /** Status listener */
104
+ onStatus?: WebSocketStatusListener;
105
+ };
106
+ /** File ingestion configuration */
107
+ type FileIngestionConfig = {
23
108
  type: 'file';
109
+ /** File URL */
24
110
  url: string;
111
+ /** Polling interval in milliseconds */
25
112
  intervalMs?: number;
113
+ /** Status listener */
114
+ onStatus?: FileStatusListener;
26
115
  };
116
+ /**
117
+ * Ingestion source configuration.
118
+ * Used to connect to an external data source for graph updates.
119
+ */
120
+ type IngestionConfig = WebSocketIngestionConfig | FileIngestionConfig;
27
121
  /**
28
122
  * Arguments to the API constructor.
29
123
  *
@@ -406,7 +500,7 @@ declare class API<N, E> {
406
500
  /** Toggle canvas editable mode without re-creating the graph */
407
501
  setEditable(editable: boolean): void;
408
502
  /** Replace entire history (clears prior) */
409
- replaceHistory(frames: Update<N, E>[]): Promise<void>;
503
+ replaceHistory(history: Update<N, E>[]): Promise<void>;
410
504
  /** Rebuild from snapshot (nodes/edges) */
411
505
  replaceSnapshot(nodes: N[], edges: E[], description?: string): Promise<void>;
412
506
  private get graph();
@@ -487,57 +581,13 @@ declare class API<N, E> {
487
581
  destroy(): void;
488
582
  }
489
583
 
490
- type SnapshotMessage<N, E> = {
491
- type: 'snapshot';
492
- nodes: N[];
493
- edges: E[];
494
- description?: string;
495
- };
496
- type UpdateMessage<N, E> = {
497
- type: 'update';
498
- description?: string;
499
- } & Update<N, E>;
500
- type HistoryMessage<N, E> = {
501
- type: 'history';
502
- frames: Update<N, E>[];
584
+ type StatusListener = (status: 'idle' | 'opened' | 'reading' | 'error' | 'closed', detail?: any) => void;
585
+ type FileSystemSourceArgs<N, E> = {
586
+ filename: string;
587
+ onMessage: (msg: IngestMessage<N, E>) => void;
588
+ onStatus?: StatusListener;
589
+ intervalMs?: number;
503
590
  };
504
- type IngestMessage<N, E> = SnapshotMessage<N, E> | UpdateMessage<N, E> | HistoryMessage<N, E>;
505
- /**
506
- * Ingest class handles applying ingest messages to an API instance.
507
- * This is the core ingestion functionality, separate from UI concerns.
508
- */
509
- declare class Ingest<N, E> {
510
- api: API<N, E>;
511
- constructor(api: API<N, E>);
512
- /**
513
- * Apply an incoming ingest message to the API.
514
- * - snapshot: rebuild state from nodes/edges (clears prior history)
515
- * - update: apply incremental update
516
- * - history: initialize from a set of frames (clears prior history)
517
- */
518
- apply(msg: IngestMessage<N, E>): Promise<void>;
519
- }
520
-
521
- type StatusListener$2 = (status: 'connecting' | 'connected' | 'reconnecting' | 'closed' | 'error', detail?: any) => void;
522
- declare class WebSocketSource<N, E> {
523
- private url;
524
- private ws;
525
- private onMessage;
526
- private onStatus?;
527
- private reconnectMs;
528
- private closedByUser;
529
- private connectStartTime;
530
- private totalTimeoutMs;
531
- private totalTimeoutTimer;
532
- constructor(url: string, onMessage: (msg: IngestMessage<N, E>) => void, onStatus?: StatusListener$2, reconnectMs?: number);
533
- connect(): void;
534
- disconnect(): void;
535
- private startTotalTimeout;
536
- private clearTotalTimeout;
537
- private open;
538
- }
539
-
540
- type StatusListener$1 = (status: 'idle' | 'opened' | 'reading' | 'error' | 'closed', detail?: any) => void;
541
591
  declare class FileSystemSource<N, E> {
542
592
  private handle;
543
593
  private onMessage;
@@ -546,30 +596,13 @@ declare class FileSystemSource<N, E> {
546
596
  private lastSize;
547
597
  private filename;
548
598
  private intervalMs;
549
- constructor(onMessage: (msg: IngestMessage<N, E>) => void, onStatus?: StatusListener$1, filename?: string, intervalMs?: number);
599
+ constructor(args: FileSystemSourceArgs<N, E>);
550
600
  openDirectory(): Promise<void>;
551
601
  close(): void;
552
602
  private startPolling;
553
603
  private readNewLines;
554
604
  }
555
605
 
556
- type StatusListener = (status: 'idle' | 'opened' | 'reading' | 'error' | 'closed', detail?: any) => void;
557
- declare class FileSource<N, E> {
558
- private url;
559
- private onMessage;
560
- private onStatus?;
561
- private timer;
562
- private lastETag;
563
- private lastContent;
564
- private intervalMs;
565
- private closed;
566
- constructor(url: string, onMessage: (msg: IngestMessage<N, E>) => void, onStatus?: StatusListener, intervalMs?: number);
567
- connect(): Promise<void>;
568
- close(): void;
569
- private startPolling;
570
- private poll;
571
- }
572
-
573
606
  /**
574
607
  * Types for the Playground component
575
608
  */
@@ -685,4 +718,4 @@ declare class Playground {
685
718
 
686
719
  declare function graph<N, E>(args?: APIArguments<N, E>): Promise<API<N, E>>;
687
720
 
688
- export { API, type APIArguments, type APIOptions, type EventsOptions, type Example, type ExampleEdge, type ExampleNode, type ExampleOptions, FileSource, FileSystemSource, type HistoryMessage, Ingest, type IngestMessage, type IngestionConfig, Playground, type PlaygroundOptions, type SnapshotMessage, type Update, type UpdateMessage, Updater, WebSocketSource, graph as default, graph };
721
+ export { API, type APIArguments, type APIOptions, type CanvasTheme, type ColorMode, type EdgeProps, type EdgeTheme, type EventsOptions, type Example, type ExampleEdge, type ExampleNode, type ExampleOptions, FileSource, type FileSourceArgs, type FileStatus, type FileStatusListener, FileSystemSource, type FileSystemSourceArgs, type HistoryMessage, Ingest, type IngestMessage, type IngestionConfig, type NewEdge, type NewNode, type NodeAlign, type NodeProps, type NodeTheme, type Orientation, Playground, type PlaygroundOptions, type PortProps, type PortStyle, type PortTheme, type RenderNode, type SnapshotMessage, type ThemeVars, type Update, type UpdateMessage, Updater, WebSocketSource, type WebSocketSourceArgs, type WebSocketStatus, type WebSocketStatusListener, graph as default, graph };
package/dist/index.js CHANGED
@@ -3635,7 +3635,7 @@ var Ingest = class {
3635
3635
  * Apply an incoming ingest message to the API.
3636
3636
  * - snapshot: rebuild state from nodes/edges (clears prior history)
3637
3637
  * - update: apply incremental update
3638
- * - history: initialize from a set of frames (clears prior history)
3638
+ * - history: initialize from a set of updates (clears prior history)
3639
3639
  */
3640
3640
  async apply(msg) {
3641
3641
  switch (msg.type) {
@@ -3656,7 +3656,7 @@ var Ingest = class {
3656
3656
  break;
3657
3657
  }
3658
3658
  case "history": {
3659
- await this.api.replaceHistory(msg.frames);
3659
+ await this.api.replaceHistory(msg.history);
3660
3660
  break;
3661
3661
  }
3662
3662
  }
@@ -3674,11 +3674,11 @@ var WebSocketSource = class {
3674
3674
  connectStartTime = null;
3675
3675
  totalTimeoutMs = 1e4;
3676
3676
  totalTimeoutTimer = null;
3677
- constructor(url, onMessage, onStatus, reconnectMs = 1500) {
3678
- this.url = url;
3679
- this.onMessage = onMessage;
3680
- this.onStatus = onStatus;
3681
- this.reconnectMs = reconnectMs;
3677
+ constructor(args) {
3678
+ this.url = args.url;
3679
+ this.onMessage = args.onMessage;
3680
+ this.onStatus = args.onStatus;
3681
+ this.reconnectMs = args.reconnectMs ?? 1500;
3682
3682
  }
3683
3683
  connect() {
3684
3684
  this.closedByUser = false;
@@ -3779,11 +3779,11 @@ var FileSource = class {
3779
3779
  lastContent = "";
3780
3780
  intervalMs = 1e3;
3781
3781
  closed = false;
3782
- constructor(url, onMessage, onStatus, intervalMs = 1e3) {
3783
- this.url = url;
3784
- this.onMessage = onMessage;
3785
- this.onStatus = onStatus;
3786
- this.intervalMs = intervalMs;
3782
+ constructor(args) {
3783
+ this.url = args.url;
3784
+ this.onMessage = args.onMessage;
3785
+ this.onStatus = args.onStatus;
3786
+ this.intervalMs = args.intervalMs ?? 1e3;
3787
3787
  }
3788
3788
  async connect() {
3789
3789
  this.closed = false;
@@ -3845,6 +3845,68 @@ var FileSource = class {
3845
3845
  }
3846
3846
  };
3847
3847
 
3848
+ // src/api/sources/FileSystemSource.ts
3849
+ var FileSystemSource = class {
3850
+ handle = null;
3851
+ onMessage;
3852
+ onStatus;
3853
+ timer = null;
3854
+ lastSize = 0;
3855
+ filename;
3856
+ intervalMs;
3857
+ constructor(args) {
3858
+ this.filename = args.filename;
3859
+ this.onMessage = args.onMessage;
3860
+ this.onStatus = args.onStatus;
3861
+ this.intervalMs = args.intervalMs ?? 1e3;
3862
+ }
3863
+ async openDirectory() {
3864
+ try {
3865
+ const dir = await window.showDirectoryPicker?.();
3866
+ if (!dir) throw new Error("File System Access not supported or cancelled");
3867
+ const handle = await dir.getFileHandle(this.filename, { create: false });
3868
+ this.handle = handle;
3869
+ this.onStatus?.("opened", { file: this.filename });
3870
+ this.lastSize = 0;
3871
+ this.startPolling();
3872
+ } catch (e) {
3873
+ this.onStatus?.("error", e);
3874
+ }
3875
+ }
3876
+ close() {
3877
+ if (this.timer) {
3878
+ window.clearInterval(this.timer);
3879
+ this.timer = null;
3880
+ }
3881
+ this.handle = null;
3882
+ this.onStatus?.("closed");
3883
+ }
3884
+ startPolling() {
3885
+ if (this.timer) window.clearInterval(this.timer);
3886
+ this.timer = window.setInterval(() => this.readNewLines(), this.intervalMs);
3887
+ }
3888
+ async readNewLines() {
3889
+ try {
3890
+ if (!this.handle) return;
3891
+ this.onStatus?.("reading");
3892
+ const file = await this.handle.getFile();
3893
+ if (file.size === this.lastSize) return;
3894
+ const slice = await file.slice(this.lastSize).text();
3895
+ this.lastSize = file.size;
3896
+ const lines = slice.split("\n").map((l) => l.trim()).filter(Boolean);
3897
+ for (const line of lines) {
3898
+ try {
3899
+ const obj = JSON.parse(line);
3900
+ this.onMessage(obj);
3901
+ } catch {
3902
+ }
3903
+ }
3904
+ } catch (e) {
3905
+ this.onStatus?.("error", e);
3906
+ }
3907
+ }
3908
+ };
3909
+
3848
3910
  // src/api/api.ts
3849
3911
  var log11 = logger("api");
3850
3912
  var API = class {
@@ -3927,29 +3989,19 @@ var API = class {
3927
3989
  /** Connect to the configured ingestion source */
3928
3990
  connectIngestion() {
3929
3991
  if (!this.ingestionConfig || !this.ingest) return;
3930
- const handleMessage = (msg) => {
3931
- this.ingest.apply(msg);
3992
+ const args = {
3993
+ ...this.ingestionConfig,
3994
+ onMessage: (msg) => {
3995
+ this.ingest.apply(msg);
3996
+ }
3932
3997
  };
3933
- switch (this.ingestionConfig.type) {
3934
- case "websocket":
3935
- this.ingestionSource = new WebSocketSource(
3936
- this.ingestionConfig.url,
3937
- handleMessage,
3938
- void 0,
3939
- this.ingestionConfig.reconnectMs
3940
- );
3941
- this.ingestionSource.connect();
3942
- break;
3943
- case "file":
3944
- this.ingestionSource = new FileSource(
3945
- this.ingestionConfig.url,
3946
- handleMessage,
3947
- void 0,
3948
- this.ingestionConfig.intervalMs
3949
- );
3950
- this.ingestionSource.connect();
3951
- break;
3952
- }
3998
+ const source = {
3999
+ "websocket": WebSocketSource,
4000
+ "file": FileSource,
4001
+ "filesystem": FileSystemSource
4002
+ }[this.ingestionConfig.type];
4003
+ this.ingestionSource = new source[this.ingestionConfig.type](args);
4004
+ this.ingestionSource?.connect();
3953
4005
  }
3954
4006
  /** Disconnect from the ingestion source */
3955
4007
  disconnectIngestion() {
@@ -3978,9 +4030,9 @@ var API = class {
3978
4030
  this.canvas.editMode.editable = editable;
3979
4031
  }
3980
4032
  /** Replace entire history (clears prior) */
3981
- async replaceHistory(frames) {
4033
+ async replaceHistory(history) {
3982
4034
  this.reset();
3983
- this.history = frames;
4035
+ this.history = history;
3984
4036
  await this.applyHistory();
3985
4037
  }
3986
4038
  /** Rebuild from snapshot (nodes/edges) */
@@ -4447,8 +4499,8 @@ var API = class {
4447
4499
  }
4448
4500
  } else if (prev.history && isHistoryPrefix(prev.history, props.history)) {
4449
4501
  const prevLength = prev.history.length;
4450
- const newFrames = props.history.slice(prevLength);
4451
- for (const frame of newFrames) {
4502
+ const newUpdates = props.history.slice(prevLength);
4503
+ for (const frame of newUpdates) {
4452
4504
  this.update((u) => {
4453
4505
  if (frame.addNodes) u.addNodes(...frame.addNodes);
4454
4506
  if (frame.removeNodes) u.deleteNodes(...frame.removeNodes);
@@ -4535,68 +4587,6 @@ function shallowEqualUpdate(a, b) {
4535
4587
  return true;
4536
4588
  }
4537
4589
 
4538
- // src/api/sources/FileSystemSource.ts
4539
- var FileSystemSource = class {
4540
- handle = null;
4541
- onMessage;
4542
- onStatus;
4543
- timer = null;
4544
- lastSize = 0;
4545
- filename;
4546
- intervalMs;
4547
- constructor(onMessage, onStatus, filename = "graph.ndjson", intervalMs = 1e3) {
4548
- this.onMessage = onMessage;
4549
- this.onStatus = onStatus;
4550
- this.filename = filename;
4551
- this.intervalMs = intervalMs;
4552
- }
4553
- async openDirectory() {
4554
- try {
4555
- const dir = await window.showDirectoryPicker?.();
4556
- if (!dir) throw new Error("File System Access not supported or cancelled");
4557
- const handle = await dir.getFileHandle(this.filename, { create: false });
4558
- this.handle = handle;
4559
- this.onStatus?.("opened", { file: this.filename });
4560
- this.lastSize = 0;
4561
- this.startPolling();
4562
- } catch (e) {
4563
- this.onStatus?.("error", e);
4564
- }
4565
- }
4566
- close() {
4567
- if (this.timer) {
4568
- window.clearInterval(this.timer);
4569
- this.timer = null;
4570
- }
4571
- this.handle = null;
4572
- this.onStatus?.("closed");
4573
- }
4574
- startPolling() {
4575
- if (this.timer) window.clearInterval(this.timer);
4576
- this.timer = window.setInterval(() => this.readNewLines(), this.intervalMs);
4577
- }
4578
- async readNewLines() {
4579
- try {
4580
- if (!this.handle) return;
4581
- this.onStatus?.("reading");
4582
- const file = await this.handle.getFile();
4583
- if (file.size === this.lastSize) return;
4584
- const slice = await file.slice(this.lastSize).text();
4585
- this.lastSize = file.size;
4586
- const lines = slice.split("\n").map((l) => l.trim()).filter(Boolean);
4587
- for (const line of lines) {
4588
- try {
4589
- const obj = JSON.parse(line);
4590
- this.onMessage(obj);
4591
- } catch {
4592
- }
4593
- }
4594
- } catch (e) {
4595
- this.onStatus?.("error", e);
4596
- }
4597
- }
4598
- };
4599
-
4600
4590
  // src/playground/playground.ts
4601
4591
  import styles2 from "./styles.css?raw";
4602
4592
  var Playground = class {
@@ -4875,10 +4865,18 @@ var Playground = class {
4875
4865
  this.disconnectAllSources();
4876
4866
  if (example.source.type === "websocket") {
4877
4867
  this.wsUrl = example.source.url;
4878
- this.wsSource = new WebSocketSource(example.source.url, this.handleIngestMessage.bind(this), this.updateWsStatus);
4868
+ this.wsSource = new WebSocketSource({
4869
+ url: example.source.url,
4870
+ onMessage: this.handleIngestMessage.bind(this),
4871
+ onStatus: this.updateWsStatus
4872
+ });
4879
4873
  this.wsSource.connect();
4880
4874
  } else if (example.source.type === "file") {
4881
- this.fileSource = new FileSource(example.source.path, this.handleIngestMessage.bind(this), this.updateFileStatus);
4875
+ this.fileSource = new FileSource({
4876
+ url: example.source.path,
4877
+ onMessage: this.handleIngestMessage.bind(this),
4878
+ onStatus: this.updateFileStatus
4879
+ });
4882
4880
  this.fileSource.connect();
4883
4881
  }
4884
4882
  }
@@ -5278,7 +5276,11 @@ var Playground = class {
5278
5276
  if (this.wsSource) {
5279
5277
  this.wsSource.disconnect();
5280
5278
  }
5281
- this.wsSource = new WebSocketSource(url, this.handleIngestMessage.bind(this), this.updateWsStatus);
5279
+ this.wsSource = new WebSocketSource({
5280
+ url,
5281
+ onMessage: this.handleIngestMessage.bind(this),
5282
+ onStatus: this.updateWsStatus
5283
+ });
5282
5284
  this.wsSource.connect();
5283
5285
  this.updateSourceModal();
5284
5286
  }
@@ -5299,7 +5301,11 @@ var Playground = class {
5299
5301
  }
5300
5302
  async handleOpenFolder() {
5301
5303
  if (!this.fsSource) {
5302
- this.fsSource = new FileSystemSource(this.handleIngestMessage.bind(this), this.updateFsStatus);
5304
+ this.fsSource = new FileSystemSource({
5305
+ filename: "graph.ndjson",
5306
+ onMessage: this.handleIngestMessage.bind(this),
5307
+ onStatus: this.updateFsStatus
5308
+ });
5303
5309
  }
5304
5310
  this.updateSourceModal();
5305
5311
  await this.fsSource.openDirectory();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@3plate/graph-core",
3
- "version": "0.1.9",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "license": "GPL-3.0",
6
6
  "repository": {
@@ -8,13 +8,15 @@
8
8
  "url": "https://github.com/3plt/graph",
9
9
  "directory": "packages/core"
10
10
  },
11
- "main": "./dist/index.js",
12
- "module": "./dist/index.mjs",
11
+ "main": "./dist/index.cjs",
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
13
14
  "exports": {
14
15
  ".": {
15
16
  "source": "./src/index.ts",
16
- "import": "./dist/index.mjs",
17
- "require": "./dist/index.js"
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "require": "./dist/index.cjs"
18
20
  }
19
21
  },
20
22
  "files": [