@statelyai/sdk 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -46,20 +46,20 @@ embed.init({
46
46
 
47
47
  Creates an embed instance.
48
48
 
49
- | Option | Type | Description |
50
- |--------|------|-------------|
51
- | `baseUrl` | `string` | **Required.** Base URL of the Stately app |
52
- | `apiKey` | `string` | **Required.** Your Stately API key |
53
- | `origin` | `string` | Custom target origin for postMessage |
54
- | `onReady` | `() => void` | Called when embed is ready |
55
- | `onLoaded` | `(graph) => void` | Called when machine is loaded |
56
- | `onChange` | `(graph, machineConfig) => void` | Called on every change |
57
- | `onSave` | `(graph, machineConfig) => void` | Called on save |
58
- | `onError` | `({ code, message }) => void` | Called on error |
49
+ | Option | Type | Description |
50
+ | ---------- | -------------------------------- | ----------------------------------------- |
51
+ | `baseUrl` | `string` | **Required.** Base URL of the Stately app |
52
+ | `apiKey` | `string` | **Required.** Your Stately API key |
53
+ | `origin` | `string` | Custom target origin for postMessage |
54
+ | `onReady` | `() => void` | Called when embed is ready |
55
+ | `onLoaded` | `(graph) => void` | Called when machine is loaded |
56
+ | `onChange` | `(graph, machineConfig) => void` | Called on every change |
57
+ | `onSave` | `(graph, machineConfig) => void` | Called on save |
58
+ | `onError` | `({ code, message }) => void` | Called on error |
59
59
 
60
60
  ### Embed methods
61
61
 
62
- #### `mount(container)` / `attach(iframe)`
62
+ #### `embed.mount(container)` / `embed.attach(iframe)`
63
63
 
64
64
  Mount creates an iframe inside a container element. Attach connects to an existing iframe.
65
65
 
@@ -71,7 +71,7 @@ const iframe = embed.mount(document.getElementById('editor')!);
71
71
  embed.attach(document.querySelector('iframe')!);
72
72
  ```
73
73
 
74
- #### `init(options)`
74
+ #### `embed.init(options)`
75
75
 
76
76
  Initialize the embed with a machine and display options.
77
77
 
@@ -79,8 +79,8 @@ Initialize the embed with a machine and display options.
79
79
  embed.init({
80
80
  machine: machineConfig,
81
81
  format: 'xstate', // optional
82
- mode: 'editing', // 'editing' | 'viewing' | 'simulating'
83
- theme: 'dark', // 'light' | 'dark'
82
+ mode: 'editing', // 'editing' | 'viewing' | 'simulating'
83
+ theme: 'dark', // 'light' | 'dark'
84
84
  readOnly: false,
85
85
  depth: 3,
86
86
  panels: {
@@ -91,15 +91,15 @@ embed.init({
91
91
  });
92
92
  ```
93
93
 
94
- #### `updateMachine(machine, format?)`
94
+ #### `embed.updateMachine(machine, format?)`
95
95
 
96
96
  Update the displayed machine.
97
97
 
98
- #### `setMode(mode)` / `setTheme(theme)`
98
+ #### `embed.setMode(mode)` / `embed.setTheme(theme)`
99
99
 
100
100
  Change the embed mode or theme at runtime.
101
101
 
102
- #### `export(format, options?)`
102
+ #### `embed.export(format, options?)`
103
103
 
104
104
  Export the current machine. Returns a promise.
105
105
 
@@ -111,7 +111,7 @@ const mermaid = await embed.export('mermaid');
111
111
 
112
112
  Supported formats: `xstate`, `json`, `digraph`, `mermaid`, `scxml`
113
113
 
114
- #### `on(event, handler)` / `off(event, handler)`
114
+ #### `embed.on(event, handler)` / `embed.off(event, handler)`
115
115
 
116
116
  Subscribe/unsubscribe to embed events: `ready`, `loaded`, `change`, `save`, `error`.
117
117
 
@@ -121,10 +121,10 @@ embed.on('change', ({ graph, machineConfig }) => {
121
121
  });
122
122
  ```
123
123
 
124
- #### `toast(message, type?)`
124
+ #### `embed.toast(message, type?)`
125
125
 
126
126
  Show a toast notification in the embed. Type: `'success' | 'error' | 'info' | 'warning'`
127
127
 
128
- #### `destroy()`
128
+ #### `embed.destroy()`
129
129
 
130
130
  Tear down the embed. Removes listeners, rejects pending promises, and removes the iframe if it was created via `mount()`.
package/dist/index.d.mts CHANGED
@@ -1,77 +1,9 @@
1
+ import { a as EmbedEventHandler, c as EmbedMode, d as ExportFormatMap, f as InitOptions, i as createInspector, l as ExportCallOptions, n as InspectOptions, o as EmbedEventMap, p as ProtocolMessage, r as Inspector, s as EmbedEventName, t as CreateInspectorOptions, u as ExportFormat } from "./inspect-Bt5Ohkrp.mjs";
2
+
1
3
  //#region src/embed.d.ts
2
- interface ExportFormatMap {
3
- xstate: {
4
- options: {
5
- version?: 4 | 5;
6
- addTSTypes?: boolean;
7
- showIds?: boolean;
8
- showDescriptions?: boolean;
9
- };
10
- result: string;
11
- };
12
- json: {
13
- options: {
14
- simplifyArrays?: boolean;
15
- version?: 4 | 5;
16
- };
17
- result: Record<string, unknown>;
18
- };
19
- digraph: {
20
- options: Record<string, never>;
21
- result: Record<string, unknown>;
22
- };
23
- mermaid: {
24
- options: Record<string, never>;
25
- result: string;
26
- };
27
- scxml: {
28
- options: Record<string, never>;
29
- result: string;
30
- };
31
- }
32
- type ExportFormat = keyof ExportFormatMap;
33
- type ExportCallOptions<F extends ExportFormat> = ExportFormatMap[F]['options'] & {
34
- timeout?: number;
35
- };
36
- type EmbedMode = 'editing' | 'viewing' | 'simulating';
37
- interface InitOptions {
38
- machine: unknown;
39
- format?: string;
40
- mode?: EmbedMode;
41
- theme?: 'light' | 'dark';
42
- readOnly?: boolean;
43
- depth?: number;
44
- panels?: {
45
- leftPanels?: string[];
46
- rightPanels?: string[];
47
- activePanels?: string[];
48
- };
49
- }
50
- interface EmbedEventMap {
51
- ready: {
52
- version: string;
53
- };
54
- loaded: {
55
- graph: unknown;
56
- };
57
- change: {
58
- graph: unknown;
59
- machineConfig: unknown;
60
- };
61
- save: {
62
- graph: unknown;
63
- machineConfig: unknown;
64
- };
65
- error: {
66
- code: string;
67
- message: string;
68
- };
69
- }
70
- type EmbedEventName = keyof EmbedEventMap;
71
- type EmbedEventHandler<K extends EmbedEventName> = (data: EmbedEventMap[K]) => void;
72
4
  interface StatelyEmbedOptions {
73
5
  baseUrl: string;
74
- apiKey: string;
6
+ apiKey?: string;
75
7
  origin?: string;
76
8
  onReady?: () => void;
77
9
  onLoaded?: (graph: unknown) => void;
@@ -108,4 +40,34 @@ interface StatelyEmbed {
108
40
  }
109
41
  declare function createStatelyEmbed(options: StatelyEmbedOptions): StatelyEmbed;
110
42
  //#endregion
111
- export { type EmbedEventMap, type EmbedMode, type ExportCallOptions, type ExportFormat, type ExportFormatMap, type InitOptions, type StatelyEmbed, type StatelyEmbedOptions, createStatelyEmbed };
43
+ //#region src/transport.d.ts
44
+ interface Transport {
45
+ send(msg: ProtocolMessage): void;
46
+ onMessage(handler: (msg: ProtocolMessage) => void): () => void;
47
+ destroy(): void;
48
+ readonly ready: boolean;
49
+ onReady(handler: () => void): () => void;
50
+ }
51
+ interface PostMessageTransportOptions {
52
+ iframe: HTMLIFrameElement;
53
+ targetOrigin: string;
54
+ /** Filter by source. Defaults to iframe.contentWindow. */
55
+ source?: Window;
56
+ }
57
+ declare function createPostMessageTransport(options: PostMessageTransportOptions): Transport;
58
+ interface WebSocketTransportOptions {
59
+ url: string;
60
+ role: 'client' | 'viz';
61
+ sessionId: string;
62
+ metadata?: {
63
+ name?: string;
64
+ machineId?: string;
65
+ };
66
+ /** Reconnect on disconnect. Default true. */
67
+ reconnect?: boolean;
68
+ /** Max reconnect attempts. Default 10. */
69
+ maxReconnectAttempts?: number;
70
+ }
71
+ declare function createWebSocketTransport(options: WebSocketTransportOptions): Transport;
72
+ //#endregion
73
+ export { type CreateInspectorOptions, type EmbedEventHandler, type EmbedEventMap, type EmbedEventName, type EmbedMode, type ExportCallOptions, type ExportFormat, type ExportFormatMap, type InitOptions, type InspectOptions, type Inspector, type StatelyEmbed, type StatelyEmbedOptions, type Transport, createInspector, createPostMessageTransport, createStatelyEmbed, createWebSocketTransport };
package/dist/index.mjs CHANGED
@@ -1,94 +1,96 @@
1
+ import { a as createPendingExportManager, i as createEventRegistry, n as createPostMessageTransport, o as toInitMessage, r as createWebSocketTransport, t as createInspector } from "./inspect-C3Rjgn1o.mjs";
2
+
1
3
  //#region src/embed.ts
2
- const PREFIX = "@statelyai.";
3
4
  function createStatelyEmbed(options) {
4
- const embedUrl = `${options.baseUrl.replace(/\/+$/, "") + "/embed"}?api_key=${encodeURIComponent(options.apiKey)}`;
5
+ const base = options.baseUrl.replace(/\/+$/, "") + "/embed";
6
+ const embedUrl = options.apiKey ? `${base}?api_key=${encodeURIComponent(options.apiKey)}` : base;
5
7
  const targetOrigin = options.origin ?? new URL(embedUrl).origin;
6
8
  let iframe = null;
9
+ let transport = null;
7
10
  let ownedIframe = false;
8
- let ready = false;
9
11
  let destroyed = false;
10
12
  const pendingMessages = [];
11
- let pendingExport = null;
12
- const jsonResultFormats = new Set(["digraph", "json"]);
13
- const listeners = {};
14
- function emit(event, data) {
15
- const set = listeners[event];
16
- if (set) set.forEach((fn) => fn(data));
17
- }
13
+ const events = createEventRegistry();
14
+ const exportManager = createPendingExportManager((message) => send(message));
18
15
  function send(msg) {
19
- if (!ready) {
16
+ if (!transport?.ready) {
20
17
  pendingMessages.push(msg);
21
18
  return;
22
19
  }
23
- iframe?.contentWindow?.postMessage(msg, targetOrigin);
20
+ transport.send(msg);
24
21
  }
25
22
  function flush() {
23
+ if (!transport) return;
26
24
  while (pendingMessages.length > 0) {
27
25
  const msg = pendingMessages.shift();
28
- iframe?.contentWindow?.postMessage(msg, targetOrigin);
26
+ transport.send(msg);
29
27
  }
30
28
  }
31
- function handleMessage(e) {
29
+ function handleMessage(data) {
32
30
  if (destroyed) return;
33
- if (e.source !== iframe?.contentWindow) return;
34
- const data = e.data;
35
- if (!data?.type?.startsWith?.(PREFIX)) return;
36
31
  switch (data.type) {
37
32
  case "@statelyai.ready":
38
- ready = true;
39
33
  flush();
40
34
  options.onReady?.();
41
- emit("ready", { version: data.version });
35
+ events.emit("ready", { version: data.version });
42
36
  break;
43
37
  case "@statelyai.loaded":
44
38
  options.onLoaded?.(data.graph);
45
- emit("loaded", { graph: data.graph });
39
+ events.emit("loaded", { graph: data.graph });
46
40
  break;
47
41
  case "@statelyai.change":
48
42
  options.onChange?.(data.graph, data.machineConfig);
49
- emit("change", {
43
+ events.emit("change", {
50
44
  graph: data.graph,
51
45
  machineConfig: data.machineConfig
52
46
  });
53
47
  break;
54
48
  case "@statelyai.save":
55
49
  options.onSave?.(data.graph, data.machineConfig);
56
- emit("save", {
50
+ events.emit("save", {
57
51
  graph: data.graph,
58
52
  machineConfig: data.machineConfig
59
53
  });
60
54
  break;
61
55
  case "@statelyai.retrieved":
62
- if (pendingExport) {
63
- clearTimeout(pendingExport.timer);
64
- let result = data.data;
65
- if (jsonResultFormats.has(pendingExport.format) && typeof result === "string") result = JSON.parse(result);
66
- pendingExport.resolve(result);
67
- pendingExport = null;
68
- }
56
+ if (typeof data.requestId === "string") exportManager.resolve(data.requestId, data.data);
69
57
  break;
70
58
  case "@statelyai.error": {
71
59
  const err = {
72
60
  code: data.code,
73
61
  message: data.message
74
62
  };
63
+ exportManager.reject(new Error(err.message), typeof data.requestId === "string" ? data.requestId : void 0);
75
64
  options.onError?.(err);
76
- emit("error", err);
65
+ events.emit("error", err);
77
66
  break;
78
67
  }
79
68
  }
80
69
  }
81
- function startListening() {
82
- window.addEventListener("message", handleMessage);
70
+ function replaceTransport(nextTransport) {
71
+ transport?.destroy();
72
+ transport = nextTransport;
73
+ }
74
+ function setupTransport(el) {
75
+ replaceTransport(createPostMessageTransport({
76
+ iframe: el,
77
+ targetOrigin
78
+ }));
79
+ transport.onMessage(handleMessage);
80
+ transport.onReady(flush);
83
81
  }
84
82
  return {
85
83
  attach(el) {
86
84
  if (destroyed) return;
85
+ const currentSrc = el.getAttribute("src");
86
+ if (currentSrc && currentSrc !== "about:blank" && el.src !== embedUrl) console.warn("Replacing existing iframe src during attach()", {
87
+ currentSrc,
88
+ nextSrc: embedUrl
89
+ });
87
90
  iframe = el;
88
91
  ownedIframe = false;
89
- ready = false;
90
92
  el.src = embedUrl;
91
- startListening();
93
+ setupTransport(el);
92
94
  },
93
95
  mount(container) {
94
96
  if (destroyed) throw new Error("Embed is destroyed");
@@ -102,23 +104,11 @@ function createStatelyEmbed(options) {
102
104
  container.appendChild(el);
103
105
  iframe = el;
104
106
  ownedIframe = true;
105
- ready = false;
106
- startListening();
107
+ setupTransport(el);
107
108
  return el;
108
109
  },
109
110
  init(opts) {
110
- send({
111
- type: "@statelyai.init",
112
- machine: opts.machine,
113
- format: opts.format,
114
- mode: opts.mode,
115
- theme: opts.theme,
116
- readOnly: opts.readOnly,
117
- depth: opts.depth,
118
- leftPanels: opts.panels?.leftPanels,
119
- rightPanels: opts.panels?.rightPanels,
120
- activePanels: opts.panels?.activePanels
121
- });
111
+ send(toInitMessage(opts));
122
112
  },
123
113
  updateMachine(machine, format) {
124
114
  send({
@@ -146,49 +136,22 @@ function createStatelyEmbed(options) {
146
136
  toastType
147
137
  });
148
138
  },
149
- export(format, options) {
150
- const { timeout = 1e4, ...formatOptions } = options ?? {};
151
- return new Promise((resolve, reject) => {
152
- if (destroyed) {
153
- reject(/* @__PURE__ */ new Error("Embed is destroyed"));
154
- return;
155
- }
156
- if (pendingExport) {
157
- clearTimeout(pendingExport.timer);
158
- pendingExport.reject(/* @__PURE__ */ new Error("Superseded by new export"));
159
- }
160
- pendingExport = {
161
- resolve,
162
- reject,
163
- timer: setTimeout(() => {
164
- pendingExport = null;
165
- reject(/* @__PURE__ */ new Error("Export timed out"));
166
- }, timeout),
167
- format
168
- };
169
- send({
170
- type: "@statelyai.retrieve",
171
- format,
172
- ...Object.keys(formatOptions).length > 0 && { options: formatOptions }
173
- });
174
- });
139
+ export(format, callOptions) {
140
+ return exportManager.start(format, callOptions, "Embed is destroyed", () => destroyed);
175
141
  },
176
142
  on(event, handler) {
177
- if (!listeners[event]) listeners[event] = /* @__PURE__ */ new Set();
178
- listeners[event].add(handler);
143
+ events.on(event, handler);
179
144
  },
180
145
  off(event, handler) {
181
- listeners[event]?.delete(handler);
146
+ events.off(event, handler);
182
147
  },
183
148
  destroy() {
184
149
  if (destroyed) return;
185
150
  destroyed = true;
186
- window.removeEventListener("message", handleMessage);
187
- if (pendingExport) {
188
- clearTimeout(pendingExport.timer);
189
- pendingExport.reject(/* @__PURE__ */ new Error("Embed destroyed"));
190
- pendingExport = null;
191
- }
151
+ transport?.destroy();
152
+ transport = null;
153
+ exportManager.clear("Embed destroyed");
154
+ events.clear();
192
155
  pendingMessages.length = 0;
193
156
  if (ownedIframe && iframe) iframe.remove();
194
157
  iframe = null;
@@ -197,4 +160,4 @@ function createStatelyEmbed(options) {
197
160
  }
198
161
 
199
162
  //#endregion
200
- export { createStatelyEmbed };
163
+ export { createInspector, createPostMessageTransport, createStatelyEmbed, createWebSocketTransport };
@@ -0,0 +1,214 @@
1
+ //#region src/protocol.d.ts
2
+ type EmbedMode = 'editing' | 'viewing' | 'simulating' | 'inspecting';
3
+ interface ExportFormatMap {
4
+ xstate: {
5
+ options: {
6
+ version?: 4 | 5;
7
+ addTSTypes?: boolean;
8
+ showIds?: boolean;
9
+ showDescriptions?: boolean;
10
+ };
11
+ result: string;
12
+ };
13
+ json: {
14
+ options: {
15
+ simplifyArrays?: boolean;
16
+ version?: 4 | 5;
17
+ };
18
+ result: Record<string, unknown>;
19
+ };
20
+ digraph: {
21
+ options: Record<string, never>;
22
+ result: Record<string, unknown>;
23
+ };
24
+ mermaid: {
25
+ options: Record<string, never>;
26
+ result: string;
27
+ };
28
+ scxml: {
29
+ options: Record<string, never>;
30
+ result: string;
31
+ };
32
+ }
33
+ type ExportFormat = keyof ExportFormatMap;
34
+ type ExportCallOptions<F extends ExportFormat> = ExportFormatMap[F]['options'] & {
35
+ timeout?: number;
36
+ };
37
+ interface EmbedEventMap {
38
+ ready: {
39
+ version: string;
40
+ };
41
+ loaded: {
42
+ graph: unknown;
43
+ };
44
+ change: {
45
+ graph: unknown;
46
+ machineConfig: unknown;
47
+ };
48
+ save: {
49
+ graph: unknown;
50
+ machineConfig: unknown;
51
+ };
52
+ error: {
53
+ code: string;
54
+ message: string;
55
+ };
56
+ snapshot: {
57
+ snapshot: unknown;
58
+ event: unknown | null;
59
+ };
60
+ }
61
+ type EmbedEventName = keyof EmbedEventMap;
62
+ type EmbedEventHandler<K extends EmbedEventName> = (data: EmbedEventMap[K]) => void;
63
+ interface InitOptions {
64
+ machine: unknown;
65
+ format?: string;
66
+ mode?: EmbedMode;
67
+ theme?: 'light' | 'dark';
68
+ readOnly?: boolean;
69
+ depth?: number;
70
+ panels?: {
71
+ leftPanels?: string[];
72
+ rightPanels?: string[];
73
+ activePanels?: string[];
74
+ };
75
+ }
76
+ interface InitMessage {
77
+ type: '@statelyai.init';
78
+ machine: unknown;
79
+ format?: string;
80
+ mode?: EmbedMode;
81
+ theme?: 'light' | 'dark';
82
+ readOnly?: boolean;
83
+ depth?: number;
84
+ leftPanels?: string[];
85
+ rightPanels?: string[];
86
+ activePanels?: string[];
87
+ }
88
+ interface UpdateMessage {
89
+ type: '@statelyai.update';
90
+ machine: unknown;
91
+ format?: string;
92
+ }
93
+ interface SetModeMessage {
94
+ type: '@statelyai.setMode';
95
+ mode: EmbedMode;
96
+ }
97
+ interface SetThemeMessage {
98
+ type: '@statelyai.setTheme';
99
+ theme: 'light' | 'dark';
100
+ }
101
+ interface RetrieveMessage {
102
+ type: '@statelyai.retrieve';
103
+ requestId: string;
104
+ format: ExportFormat;
105
+ options?: Record<string, unknown>;
106
+ }
107
+ interface ToastMessage {
108
+ type: '@statelyai.toast';
109
+ message: string;
110
+ toastType?: 'success' | 'error' | 'info' | 'warning';
111
+ }
112
+ interface InspectSnapshotMessage {
113
+ type: '@statelyai.inspectSnapshot';
114
+ snapshot: unknown;
115
+ event: unknown | null;
116
+ }
117
+ /** All messages a client (embed/inspector) can send to the viz. */
118
+ type ClientMessage = InitMessage | UpdateMessage | SetModeMessage | SetThemeMessage | RetrieveMessage | ToastMessage | InspectSnapshotMessage;
119
+ interface ReadyMessage {
120
+ type: '@statelyai.ready';
121
+ version: string;
122
+ }
123
+ interface LoadedMessage {
124
+ type: '@statelyai.loaded';
125
+ graph: unknown;
126
+ }
127
+ interface ChangeMessage {
128
+ type: '@statelyai.change';
129
+ graph: unknown;
130
+ machineConfig: unknown;
131
+ }
132
+ interface SaveMessage {
133
+ type: '@statelyai.save';
134
+ graph: unknown;
135
+ machineConfig: unknown;
136
+ }
137
+ interface RetrievedMessage {
138
+ type: '@statelyai.retrieved';
139
+ requestId: string;
140
+ data: unknown;
141
+ }
142
+ interface ErrorMessage {
143
+ type: '@statelyai.error';
144
+ code: string;
145
+ message: string;
146
+ requestId?: string;
147
+ }
148
+ /** All messages the viz can send back to a client. */
149
+ type VizMessage = ReadyMessage | LoadedMessage | ChangeMessage | SaveMessage | RetrievedMessage | ErrorMessage;
150
+ interface RegisterMessage {
151
+ type: '@statelyai.register';
152
+ role: 'client' | 'viz';
153
+ sessionId: string;
154
+ metadata?: {
155
+ name?: string;
156
+ machineId?: string;
157
+ };
158
+ }
159
+ interface RegisteredMessage {
160
+ type: '@statelyai.registered';
161
+ sessionId: string;
162
+ }
163
+ interface RequestOpenMessage {
164
+ type: '@statelyai.requestOpen';
165
+ sessionId: string;
166
+ }
167
+ /** All session-management messages for the WS relay. */
168
+ type SessionMessage = RegisterMessage | RegisteredMessage | RequestOpenMessage;
169
+ /** Any valid protocol message. */
170
+ type ProtocolMessage = ClientMessage | VizMessage | SessionMessage;
171
+ //#endregion
172
+ //#region src/inspect.d.ts
173
+ interface CreateInspectorOptions {
174
+ /** WebSocket URL of the devtools server. Default: 'ws://localhost:4242' */
175
+ url?: string;
176
+ /** Auto-open browser to visualizer. Default: true */
177
+ autoOpen?: boolean;
178
+ /** Unique session ID. Auto-generated if not provided. */
179
+ sessionId?: string;
180
+ /** Display name for this connection in the visualizer. */
181
+ name?: string;
182
+ }
183
+ interface Inspector {
184
+ /** Send a machine for inspection (starts visualization). */
185
+ inspect(options: InspectOptions): void;
186
+ /** Update the machine currently being inspected. */
187
+ update(machine: unknown, format?: string): void;
188
+ /** Change the visualization mode. */
189
+ setMode(mode: EmbedMode): void;
190
+ /** Export the current machine in a given format. Returns a promise. */
191
+ export<F extends ExportFormat>(format: F, options?: ExportCallOptions<F>): Promise<ExportFormatMap[F]['result']>;
192
+ /** Subscribe to events from the visualizer. */
193
+ on<K extends EmbedEventName>(event: K, handler: EmbedEventHandler<K>): void;
194
+ /** Unsubscribe from an event. */
195
+ off<K extends EmbedEventName>(event: K, handler: EmbedEventHandler<K>): void;
196
+ /** Send a state snapshot for real-time inspection. */
197
+ sendSnapshot(snapshot: unknown, event?: unknown): void;
198
+ /** Clean up the connection. */
199
+ destroy(): void;
200
+ /** Session ID for this connection. */
201
+ readonly sessionId: string;
202
+ }
203
+ interface InspectOptions {
204
+ machine: unknown;
205
+ format?: string;
206
+ mode?: EmbedMode;
207
+ theme?: 'light' | 'dark';
208
+ readOnly?: boolean;
209
+ depth?: number;
210
+ panels?: InitOptions['panels'];
211
+ }
212
+ declare function createInspector(options?: CreateInspectorOptions): Inspector;
213
+ //#endregion
214
+ export { EmbedEventHandler as a, EmbedMode as c, ExportFormatMap as d, InitOptions as f, createInspector as i, ExportCallOptions as l, InspectOptions as n, EmbedEventMap as o, ProtocolMessage as p, Inspector as r, EmbedEventName as s, CreateInspectorOptions as t, ExportFormat as u };
@@ -0,0 +1,369 @@
1
+ //#region src/clientUtils.ts
2
+ const jsonResultFormats = new Set(["digraph", "json"]);
3
+ function createRequestId() {
4
+ const cryptoObject = globalThis.crypto;
5
+ if (cryptoObject?.randomUUID) return cryptoObject.randomUUID();
6
+ return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
7
+ }
8
+ function createEventRegistry() {
9
+ const listeners = {};
10
+ return {
11
+ emit(event, data) {
12
+ listeners[event]?.forEach((handler) => handler(data));
13
+ },
14
+ on(event, handler) {
15
+ if (!listeners[event]) listeners[event] = /* @__PURE__ */ new Set();
16
+ listeners[event].add(handler);
17
+ },
18
+ off(event, handler) {
19
+ listeners[event]?.delete(handler);
20
+ },
21
+ clear() {
22
+ Object.values(listeners).forEach((handlers) => handlers?.clear());
23
+ }
24
+ };
25
+ }
26
+ function parseExportedData(format, data) {
27
+ if (jsonResultFormats.has(format) && typeof data === "string") return JSON.parse(data);
28
+ return data;
29
+ }
30
+ function createPendingExportManager(sendRetrieve) {
31
+ const pendingExports = /* @__PURE__ */ new Map();
32
+ return {
33
+ start(format, callOptions, destroyedMessage, isDestroyed) {
34
+ const { timeout = 1e4, ...formatOptions } = callOptions ?? {};
35
+ return new Promise((resolve, reject) => {
36
+ if (isDestroyed()) {
37
+ reject(new Error(destroyedMessage));
38
+ return;
39
+ }
40
+ const requestId = createRequestId();
41
+ const timer = setTimeout(() => {
42
+ pendingExports.delete(requestId);
43
+ reject(/* @__PURE__ */ new Error("Export timed out"));
44
+ }, timeout);
45
+ pendingExports.set(requestId, {
46
+ resolve,
47
+ reject,
48
+ timer,
49
+ format
50
+ });
51
+ sendRetrieve({
52
+ type: "@statelyai.retrieve",
53
+ requestId,
54
+ format,
55
+ ...Object.keys(formatOptions).length > 0 && { options: formatOptions }
56
+ });
57
+ });
58
+ },
59
+ resolve(requestId, data) {
60
+ const pendingExport = pendingExports.get(requestId);
61
+ if (!pendingExport) return;
62
+ pendingExports.delete(requestId);
63
+ const { resolve, timer, format } = pendingExport;
64
+ clearTimeout(timer);
65
+ resolve(parseExportedData(format, data));
66
+ },
67
+ reject(error, requestId) {
68
+ if (requestId) {
69
+ const pendingExport = pendingExports.get(requestId);
70
+ if (!pendingExport) return;
71
+ pendingExports.delete(requestId);
72
+ clearTimeout(pendingExport.timer);
73
+ pendingExport.reject(error);
74
+ return;
75
+ }
76
+ pendingExports.forEach((pendingExport, pendingRequestId) => {
77
+ pendingExports.delete(pendingRequestId);
78
+ clearTimeout(pendingExport.timer);
79
+ pendingExport.reject(error);
80
+ });
81
+ },
82
+ clear(message) {
83
+ this.reject(new Error(message));
84
+ }
85
+ };
86
+ }
87
+ function toInitMessage(options) {
88
+ return {
89
+ type: "@statelyai.init",
90
+ machine: options.machine,
91
+ format: options.format,
92
+ mode: options.mode,
93
+ theme: options.theme,
94
+ readOnly: options.readOnly,
95
+ depth: options.depth,
96
+ leftPanels: options.panels?.leftPanels,
97
+ rightPanels: options.panels?.rightPanels,
98
+ activePanels: options.panels?.activePanels
99
+ };
100
+ }
101
+
102
+ //#endregion
103
+ //#region src/protocol.ts
104
+ const PREFIX = "@statelyai.";
105
+
106
+ //#endregion
107
+ //#region src/transport.ts
108
+ function createPostMessageTransport(options) {
109
+ const { iframe, targetOrigin } = options;
110
+ let destroyed = false;
111
+ let isReady = false;
112
+ const readyHandlers = /* @__PURE__ */ new Set();
113
+ const messageHandlers = /* @__PURE__ */ new Set();
114
+ function handleMessage(e) {
115
+ if (destroyed) return;
116
+ const expectedSource = options.source ?? iframe.contentWindow;
117
+ if (e.source !== expectedSource) return;
118
+ if (targetOrigin !== "*" && e.origin !== targetOrigin) return;
119
+ const data = e.data;
120
+ if (!data?.type?.startsWith?.(PREFIX)) return;
121
+ if (data.type === "@statelyai.ready" && !isReady) {
122
+ isReady = true;
123
+ readyHandlers.forEach((fn) => fn());
124
+ readyHandlers.clear();
125
+ }
126
+ messageHandlers.forEach((fn) => fn(data));
127
+ }
128
+ window.addEventListener("message", handleMessage);
129
+ return {
130
+ send(msg) {
131
+ if (destroyed) return;
132
+ iframe.contentWindow?.postMessage(msg, targetOrigin);
133
+ },
134
+ onMessage(handler) {
135
+ messageHandlers.add(handler);
136
+ return () => {
137
+ messageHandlers.delete(handler);
138
+ };
139
+ },
140
+ destroy() {
141
+ if (destroyed) return;
142
+ destroyed = true;
143
+ window.removeEventListener("message", handleMessage);
144
+ messageHandlers.clear();
145
+ readyHandlers.clear();
146
+ },
147
+ get ready() {
148
+ return isReady;
149
+ },
150
+ onReady(handler) {
151
+ if (isReady) {
152
+ handler();
153
+ return () => {};
154
+ }
155
+ readyHandlers.add(handler);
156
+ return () => {
157
+ readyHandlers.delete(handler);
158
+ };
159
+ }
160
+ };
161
+ }
162
+ function createWebSocketTransport(options) {
163
+ const { url, role, sessionId, metadata, reconnect = true, maxReconnectAttempts = 10 } = options;
164
+ let ws = null;
165
+ let destroyed = false;
166
+ let isReady = false;
167
+ let reconnectAttempts = 0;
168
+ let reconnectTimer = null;
169
+ const readyHandlers = /* @__PURE__ */ new Set();
170
+ const messageHandlers = /* @__PURE__ */ new Set();
171
+ function connect() {
172
+ if (destroyed) return;
173
+ ws = new WebSocket(url);
174
+ ws.onopen = () => {
175
+ reconnectAttempts = 0;
176
+ ws.send(JSON.stringify({
177
+ type: "@statelyai.register",
178
+ role,
179
+ sessionId,
180
+ ...metadata && { metadata }
181
+ }));
182
+ };
183
+ ws.onmessage = (event) => {
184
+ if (destroyed) return;
185
+ let raw;
186
+ try {
187
+ raw = JSON.parse(event.data);
188
+ } catch {
189
+ return;
190
+ }
191
+ if (!raw?.type || typeof raw.type !== "string") return;
192
+ if (!raw.type.startsWith(PREFIX)) return;
193
+ const data = raw;
194
+ if (data.type === "@statelyai.registered" && !isReady) {
195
+ isReady = true;
196
+ readyHandlers.forEach((fn) => fn());
197
+ readyHandlers.clear();
198
+ }
199
+ messageHandlers.forEach((fn) => fn(data));
200
+ };
201
+ ws.onclose = () => {
202
+ if (destroyed) return;
203
+ isReady = false;
204
+ if (reconnect && reconnectAttempts < maxReconnectAttempts) {
205
+ const delay = Math.min(1e3 * 2 ** reconnectAttempts, 3e4);
206
+ reconnectAttempts++;
207
+ reconnectTimer = setTimeout(connect, delay);
208
+ }
209
+ };
210
+ ws.onerror = () => {};
211
+ }
212
+ connect();
213
+ return {
214
+ send(msg) {
215
+ if (destroyed || !ws || ws.readyState !== WebSocket.OPEN) return;
216
+ ws.send(JSON.stringify(msg));
217
+ },
218
+ onMessage(handler) {
219
+ messageHandlers.add(handler);
220
+ return () => {
221
+ messageHandlers.delete(handler);
222
+ };
223
+ },
224
+ destroy() {
225
+ if (destroyed) return;
226
+ destroyed = true;
227
+ if (reconnectTimer) clearTimeout(reconnectTimer);
228
+ ws?.close();
229
+ ws = null;
230
+ messageHandlers.clear();
231
+ readyHandlers.clear();
232
+ },
233
+ get ready() {
234
+ return isReady;
235
+ },
236
+ onReady(handler) {
237
+ if (isReady) {
238
+ handler();
239
+ return () => {};
240
+ }
241
+ readyHandlers.add(handler);
242
+ return () => {
243
+ readyHandlers.delete(handler);
244
+ };
245
+ }
246
+ };
247
+ }
248
+
249
+ //#endregion
250
+ //#region src/inspect.ts
251
+ function generateId() {
252
+ return createRequestId();
253
+ }
254
+ function createInspector(options) {
255
+ const { url = "ws://localhost:4242", autoOpen = true, sessionId = generateId(), name } = options ?? {};
256
+ let destroyed = false;
257
+ const pendingMessages = [];
258
+ const events = createEventRegistry();
259
+ const transport = createWebSocketTransport({
260
+ url,
261
+ role: "client",
262
+ sessionId,
263
+ ...name && { metadata: { name } }
264
+ });
265
+ const exportManager = createPendingExportManager((message) => send(message));
266
+ function send(msg) {
267
+ if (!transport.ready) {
268
+ pendingMessages.push(msg);
269
+ return;
270
+ }
271
+ transport.send(msg);
272
+ }
273
+ function flush() {
274
+ while (pendingMessages.length > 0) {
275
+ const msg = pendingMessages.shift();
276
+ transport.send(msg);
277
+ }
278
+ }
279
+ transport.onReady(() => {
280
+ flush();
281
+ });
282
+ if (autoOpen) transport.onReady(() => {
283
+ transport.send({
284
+ type: "@statelyai.requestOpen",
285
+ sessionId
286
+ });
287
+ });
288
+ transport.onMessage((msg) => {
289
+ if (destroyed) return;
290
+ switch (msg.type) {
291
+ case "@statelyai.ready":
292
+ events.emit("ready", { version: msg.version });
293
+ break;
294
+ case "@statelyai.loaded":
295
+ events.emit("loaded", { graph: msg.graph });
296
+ break;
297
+ case "@statelyai.change":
298
+ events.emit("change", {
299
+ graph: msg.graph,
300
+ machineConfig: msg.machineConfig
301
+ });
302
+ break;
303
+ case "@statelyai.save":
304
+ events.emit("save", {
305
+ graph: msg.graph,
306
+ machineConfig: msg.machineConfig
307
+ });
308
+ break;
309
+ case "@statelyai.retrieved":
310
+ exportManager.resolve(msg.requestId, msg.data);
311
+ break;
312
+ case "@statelyai.error":
313
+ exportManager.reject(new Error(msg.message), msg.requestId);
314
+ events.emit("error", {
315
+ code: msg.code,
316
+ message: msg.message
317
+ });
318
+ break;
319
+ }
320
+ });
321
+ return {
322
+ get sessionId() {
323
+ return sessionId;
324
+ },
325
+ inspect(opts) {
326
+ send(toInitMessage(opts));
327
+ },
328
+ update(machine, format) {
329
+ send({
330
+ type: "@statelyai.update",
331
+ machine,
332
+ format
333
+ });
334
+ },
335
+ setMode(mode) {
336
+ send({
337
+ type: "@statelyai.setMode",
338
+ mode
339
+ });
340
+ },
341
+ export(format, callOptions) {
342
+ return exportManager.start(format, callOptions, "Inspector is destroyed", () => destroyed);
343
+ },
344
+ on(event, handler) {
345
+ events.on(event, handler);
346
+ },
347
+ off(event, handler) {
348
+ events.off(event, handler);
349
+ },
350
+ sendSnapshot(snapshot, event) {
351
+ send({
352
+ type: "@statelyai.inspectSnapshot",
353
+ snapshot,
354
+ event: event ?? null
355
+ });
356
+ },
357
+ destroy() {
358
+ if (destroyed) return;
359
+ destroyed = true;
360
+ transport.destroy();
361
+ exportManager.clear("Inspector destroyed");
362
+ events.clear();
363
+ pendingMessages.length = 0;
364
+ }
365
+ };
366
+ }
367
+
368
+ //#endregion
369
+ export { createPendingExportManager as a, createEventRegistry as i, createPostMessageTransport as n, toInitMessage as o, createWebSocketTransport as r, createInspector as t };
@@ -0,0 +1,2 @@
1
+ import { a as EmbedEventHandler, c as EmbedMode, d as ExportFormatMap, f as InitOptions, i as createInspector, l as ExportCallOptions, n as InspectOptions, o as EmbedEventMap, r as Inspector, s as EmbedEventName, t as CreateInspectorOptions, u as ExportFormat } from "./inspect-Bt5Ohkrp.mjs";
2
+ export { CreateInspectorOptions, EmbedEventHandler, EmbedEventMap, EmbedEventName, EmbedMode, ExportCallOptions, ExportFormat, ExportFormatMap, InitOptions, InspectOptions, Inspector, createInspector };
@@ -0,0 +1,3 @@
1
+ import { t as createInspector } from "./inspect-C3Rjgn1o.mjs";
2
+
3
+ export { createInspector };
package/package.json CHANGED
@@ -1,24 +1,32 @@
1
1
  {
2
2
  "name": "@statelyai/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
+ "files": [
6
+ "dist"
7
+ ],
5
8
  "type": "module",
6
9
  "main": "./dist/index.mjs",
7
10
  "types": "./dist/index.d.mts",
8
11
  "exports": {
9
12
  ".": {
10
- "import": "./dist/index.mjs",
11
- "types": "./dist/index.d.mts"
13
+ "types": "./dist/index.d.mts",
14
+ "import": "./dist/index.mjs"
15
+ },
16
+ "./inspect": {
17
+ "types": "./dist/inspect.d.mts",
18
+ "import": "./dist/inspect.mjs"
12
19
  }
13
20
  },
14
- "files": [
15
- "dist"
16
- ],
17
21
  "devDependencies": {
22
+ "jsdom": "^27.4.0",
18
23
  "tsdown": "0.21.0-beta.2",
19
- "typescript": "^5.9.3"
24
+ "typescript": "^5.9.3",
25
+ "vitest": "^3.2.4"
20
26
  },
21
27
  "scripts": {
22
- "build": "tsdown"
28
+ "build": "tsdown",
29
+ "test": "vitest run",
30
+ "test:watch": "vitest"
23
31
  }
24
32
  }