@bian-womp/spark-remote 0.2.40 → 0.2.41

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.
@@ -1,1036 +0,0 @@
1
- import { GraphBuilder } from '@bian-womp/spark-graph';
2
-
3
- class SeqGenerator {
4
- constructor() {
5
- this.lastMs = 0;
6
- this.seqInMs = 0;
7
- }
8
- next() {
9
- const now = Date.now();
10
- if (now === this.lastMs) {
11
- this.seqInMs = (this.seqInMs + 1) % 1000;
12
- }
13
- else {
14
- this.lastMs = now;
15
- this.seqInMs = 0;
16
- }
17
- return now * 1000 + this.seqInMs;
18
- }
19
- }
20
-
21
- const OPEN = 1;
22
- class WebSocketTransport {
23
- constructor(url) {
24
- this.listeners = new Set();
25
- this.seq = new SeqGenerator();
26
- this.baseUrl = url;
27
- }
28
- async connect(options) {
29
- if (this.ws && this.ws.readyState === OPEN)
30
- return;
31
- // Build URL with connection params
32
- // Handle both ws:///wss:// URLs and http:///https:// URLs (convert to ws)
33
- let url;
34
- if (this.baseUrl.startsWith("ws://") || this.baseUrl.startsWith("wss://")) {
35
- url = new URL(this.baseUrl);
36
- }
37
- else {
38
- // Convert http/https to ws/wss
39
- const wsBaseUrl = this.baseUrl.replace(/^http/, "ws");
40
- url = new URL(wsBaseUrl);
41
- }
42
- if (options?.params) {
43
- for (const [key, value] of Object.entries(options.params)) {
44
- if (value !== undefined && value !== null) {
45
- url.searchParams.set(key, String(value));
46
- }
47
- }
48
- }
49
- const wsUrl = url.toString();
50
- this.ws = window
51
- ? new window.WebSocket(wsUrl)
52
- : new (await import('ws')).default(wsUrl);
53
- await new Promise((resolve, reject) => {
54
- if (!this.ws)
55
- return reject(new Error("ws init failed"));
56
- this.ws.onopen = () => resolve();
57
- this.ws.onerror = (e) => reject(e);
58
- this.ws.onmessage = (ev) => {
59
- try {
60
- const env = JSON.parse(String(ev.data));
61
- for (const l of Array.from(this.listeners))
62
- l(env);
63
- }
64
- catch { }
65
- };
66
- });
67
- }
68
- async request(msg) {
69
- // For now, just send and wait for the next message with matching seq (simple demo)
70
- const seq = this.seq.next();
71
- const env = { ...msg, seq };
72
- const p = new Promise((resolve) => {
73
- const off = this.subscribe((incoming) => {
74
- if (incoming.seq === seq) {
75
- off();
76
- resolve(incoming);
77
- }
78
- });
79
- });
80
- this.send(env);
81
- return p;
82
- }
83
- send(msg) {
84
- if (!this.ws || this.ws.readyState !== OPEN)
85
- return;
86
- this.ws.send(JSON.stringify(msg));
87
- }
88
- subscribe(cb) {
89
- this.listeners.add(cb);
90
- return () => this.listeners.delete(cb);
91
- }
92
- async close() {
93
- if (!this.ws)
94
- return;
95
- const ws = this.ws;
96
- this.ws = undefined;
97
- await new Promise((resolve) => {
98
- ws.onclose = () => resolve();
99
- try {
100
- ws.close();
101
- }
102
- catch {
103
- resolve();
104
- }
105
- });
106
- }
107
- }
108
-
109
- class HttpPollingTransport {
110
- constructor(baseUrl) {
111
- this.listeners = new Set();
112
- this.baseUrl = baseUrl.replace(/\/$/, "");
113
- }
114
- async connect(options) {
115
- // Store connection params for use in requests
116
- this.connectParams = options?.params;
117
- // Start polling loop
118
- if (this.polling)
119
- return;
120
- const tick = async () => {
121
- try {
122
- const url = new URL(this.baseUrl + "/events");
123
- if (this.cursor)
124
- url.searchParams.set("cursor", this.cursor);
125
- // Add connection params to all requests
126
- if (this.connectParams) {
127
- for (const [key, value] of Object.entries(this.connectParams)) {
128
- if (value !== undefined && value !== null) {
129
- url.searchParams.set(key, String(value));
130
- }
131
- }
132
- }
133
- const res = await fetch(url.toString());
134
- if (!res.ok)
135
- return;
136
- const batch = (await res.json());
137
- for (const env of batch) {
138
- this.cursor = String(env.seq ?? Date.now());
139
- for (const l of Array.from(this.listeners))
140
- l(env);
141
- }
142
- }
143
- catch { }
144
- };
145
- this.polling = setInterval(tick, 200);
146
- }
147
- async request(msg) {
148
- const url = new URL(this.baseUrl + "/command");
149
- // Add connection params to command requests
150
- if (this.connectParams) {
151
- for (const [key, value] of Object.entries(this.connectParams)) {
152
- if (value !== undefined && value !== null) {
153
- url.searchParams.set(key, String(value));
154
- }
155
- }
156
- }
157
- const res = await fetch(url.toString(), {
158
- method: "POST",
159
- headers: { "content-type": "application/json" },
160
- body: JSON.stringify(msg),
161
- });
162
- const data = (await res.json());
163
- return data;
164
- }
165
- send(msg) {
166
- const url = new URL(this.baseUrl + "/command");
167
- // Add connection params to send requests
168
- if (this.connectParams) {
169
- for (const [key, value] of Object.entries(this.connectParams)) {
170
- if (value !== undefined && value !== null) {
171
- url.searchParams.set(key, String(value));
172
- }
173
- }
174
- }
175
- fetch(url.toString(), {
176
- method: "POST",
177
- headers: { "content-type": "application/json" },
178
- body: JSON.stringify(msg),
179
- });
180
- }
181
- subscribe(cb) {
182
- this.listeners.add(cb);
183
- return () => this.listeners.delete(cb);
184
- }
185
- async close() {
186
- if (this.polling)
187
- clearInterval(this.polling);
188
- this.polling = undefined;
189
- }
190
- }
191
-
192
- class RemoteEngine {
193
- constructor(transport) {
194
- this.transport = transport;
195
- this.listeners = new Map();
196
- this.cache = new Map();
197
- this.transport.subscribe((env) => this.onEnvelope(env));
198
- }
199
- onEnvelope(env) {
200
- const msg = env.message;
201
- if (!msg)
202
- return;
203
- if (msg.type === "value") {
204
- const key = `${msg.payload.nodeId}.${msg.payload.handle}`;
205
- this.cache.set(key, {
206
- value: msg.payload.value,
207
- runtimeTypeId: msg.payload.runtimeTypeId,
208
- });
209
- this.emit("value", msg.payload);
210
- }
211
- else if (msg.type === "error") {
212
- this.emit("error", msg.payload);
213
- }
214
- else if (msg.type === "invalidate") {
215
- this.emit("invalidate", msg.payload);
216
- }
217
- else if (msg.type === "stats") {
218
- this.emit("stats", msg.payload);
219
- }
220
- }
221
- launch(invalidate) {
222
- this.transport.send({
223
- message: { type: "Launch", payload: { invalidate } },
224
- });
225
- }
226
- setInput(nodeId, handle, value) {
227
- this.transport.send({
228
- message: { type: "SetInput", payload: { nodeId, handle, value } },
229
- });
230
- }
231
- // Batch inputs for a single network round-trip
232
- setInputs(nodeId, inputs) {
233
- this.transport.send({
234
- message: { type: "SetInputs", payload: { nodeId, inputs } },
235
- });
236
- }
237
- triggerExternal(nodeId, event) {
238
- this.transport.send({
239
- message: { type: "TriggerExternal", payload: { nodeId, event } },
240
- });
241
- }
242
- on(event, handler) {
243
- if (!this.listeners.has(event))
244
- this.listeners.set(event, new Set());
245
- const set = this.listeners.get(event);
246
- set.add(handler);
247
- return () => set.delete(handler);
248
- }
249
- emit(event, payload) {
250
- const set = this.listeners.get(event);
251
- if (set)
252
- for (const h of Array.from(set))
253
- h(payload);
254
- }
255
- getOutput(nodeId, output) {
256
- return this.cache.get(`${nodeId}.${output}`)?.value;
257
- }
258
- async whenIdle() {
259
- await this.transport.request({ message: { type: "WhenIdle" } });
260
- }
261
- dispose() {
262
- this.transport.send({ message: { type: "Dispose" } });
263
- }
264
- }
265
-
266
- class RuntimeApiClient {
267
- constructor(config, options) {
268
- this.customEventListeners = new Set();
269
- this.transportEventListeners = new Set();
270
- this.disposed = false;
271
- this.config = config;
272
- this.onCustomEvent = options?.onCustomEvent;
273
- // Engine will be created after transport is connected
274
- }
275
- /**
276
- * Create transport instance based on config.
277
- * Extracted to a separate method for better testability and clarity.
278
- */
279
- async createTransport() {
280
- const kind = this.config.kind;
281
- if (kind === "remote-http") {
282
- if (!HttpPollingTransport) {
283
- throw new Error("HttpPollingTransport not available");
284
- }
285
- return new HttpPollingTransport(this.config.baseUrl);
286
- }
287
- else if (kind === "remote-ws") {
288
- if (!WebSocketTransport) {
289
- throw new Error("WebSocketTransport not available");
290
- }
291
- return new WebSocketTransport(this.config.url);
292
- }
293
- else if (kind === "remote-unix") {
294
- // Dynamic import to avoid bundling in browser builds
295
- const { UnixSocketTransport } = await import('./UnixSocketTransport-CfdRX_2F.js');
296
- return new UnixSocketTransport(this.config.socketPath);
297
- }
298
- else {
299
- throw new Error(`Invalid transport kind: ${kind}`);
300
- }
301
- }
302
- /**
303
- * Connect to the remote runtime API.
304
- * Creates and connects the transport, then initializes the engine.
305
- * Safe to call multiple times - concurrent calls will wait for the same connection.
306
- */
307
- async connect() {
308
- if (this.disposed) {
309
- throw new Error("Cannot connect: RuntimeApiClient has been disposed");
310
- }
311
- if (this.transport) {
312
- // Already connected
313
- return;
314
- }
315
- // If already connecting, wait for that connection to complete
316
- if (this.connectingPromise) {
317
- return this.connectingPromise;
318
- }
319
- const kind = this.config.kind;
320
- this.emitTransportStatus({ state: "connecting", kind });
321
- // Create connection promise to prevent concurrent connections
322
- this.connectingPromise = (async () => {
323
- try {
324
- const transport = await this.createTransport();
325
- await transport.connect(this.config.connectOptions);
326
- this.transport = transport;
327
- // Subscribe to all transport events
328
- this.transportUnsubscribe = transport.subscribe((event) => {
329
- this.handleTransportEvent(event);
330
- });
331
- // Create engine with connected transport
332
- this.engine = new RemoteEngine(transport);
333
- this.emitTransportStatus({ state: "connected", kind });
334
- }
335
- catch (error) {
336
- // Clear connecting promise on error so retry is possible
337
- this.connectingPromise = undefined;
338
- this.emitTransportStatus({ state: "disconnected", kind });
339
- throw error;
340
- }
341
- finally {
342
- // Clear connecting promise on success
343
- this.connectingPromise = undefined;
344
- }
345
- })();
346
- return this.connectingPromise;
347
- }
348
- /**
349
- * Handle events from transport.
350
- * Routes standard runtime events to engine, custom events to custom handlers.
351
- */
352
- handleTransportEvent(event) {
353
- const msg = event.message;
354
- if (!msg || typeof msg !== "object" || !("type" in msg))
355
- return;
356
- const type = msg.type;
357
- // Standard runtime events: stats, value, error, invalidate
358
- // These are handled by RemoteEngine via transport subscription
359
- // Custom events are anything else (e.g., flow-opened, flow-latest)
360
- if (!["stats", "value", "error", "invalidate"].includes(type)) {
361
- // Emit to custom event listeners
362
- for (const listener of this.customEventListeners) {
363
- listener(event);
364
- }
365
- // Also call the constructor-provided handler if present
366
- this.onCustomEvent?.(event);
367
- }
368
- }
369
- /**
370
- * Subscribe to custom events (non-standard runtime events like flow-opened, flow-latest).
371
- * Returns an unsubscribe function.
372
- */
373
- subscribeCustomEvents(listener) {
374
- this.customEventListeners.add(listener);
375
- return () => {
376
- this.customEventListeners.delete(listener);
377
- };
378
- }
379
- /**
380
- * Subscribe to transport status changes.
381
- * Returns an unsubscribe function.
382
- */
383
- onTransportStatus(listener) {
384
- this.transportEventListeners.add(listener);
385
- // Immediately emit current status if connected
386
- if (this.transport) {
387
- const kind = this.config.kind;
388
- listener({ state: "connected", kind });
389
- }
390
- return () => {
391
- this.transportEventListeners.delete(listener);
392
- };
393
- }
394
- emitTransportStatus(status) {
395
- for (const listener of this.transportEventListeners) {
396
- listener(status);
397
- }
398
- }
399
- /**
400
- * Ensure transport is connected. Called automatically by API methods.
401
- * Returns the transport instance, throwing if connection fails or client is disposed.
402
- */
403
- async ensureConnected() {
404
- if (this.disposed) {
405
- throw new Error("Cannot ensure connection: RuntimeApiClient has been disposed");
406
- }
407
- if (!this.transport) {
408
- await this.connect();
409
- }
410
- // After connect(), transport is guaranteed to be set
411
- if (!this.transport) {
412
- throw new Error("Failed to establish transport connection");
413
- }
414
- return this.transport;
415
- }
416
- async build(def, opts) {
417
- const transport = await this.ensureConnected();
418
- await transport.request({
419
- message: {
420
- type: "Build",
421
- payload: { def, environment: opts?.environment },
422
- },
423
- });
424
- }
425
- async update(def) {
426
- const transport = await this.ensureConnected();
427
- await transport.request({
428
- message: { type: "Update", payload: { def } },
429
- });
430
- }
431
- async describeRegistry() {
432
- const transport = await this.ensureConnected();
433
- const res = await transport.request({
434
- message: { type: "DescribeRegistry" },
435
- });
436
- const payload = res?.message || {};
437
- return (payload.registry || {
438
- types: [],
439
- categories: [],
440
- nodes: [],
441
- coercions: [],
442
- schemaVersion: 4,
443
- });
444
- }
445
- async applyRegistry(deltas) {
446
- const transport = await this.ensureConnected();
447
- await transport.request({
448
- message: { type: "RegistryApply", payload: { deltas } },
449
- });
450
- }
451
- async snapshot() {
452
- const transport = await this.ensureConnected();
453
- const res = await transport.request({
454
- message: { type: "Snapshot" },
455
- });
456
- const payload = res?.message || {};
457
- return payload.snapshot || { inputs: {}, outputs: {} };
458
- }
459
- async snapshotFull() {
460
- const transport = await this.ensureConnected();
461
- const res = await transport.request({
462
- message: { type: "SnapshotFull" },
463
- });
464
- const payload = res?.message || {};
465
- return (payload.snapshot || {
466
- def: undefined,
467
- environment: {},
468
- inputs: {},
469
- outputs: {},
470
- });
471
- }
472
- async applySnapshotFull(payload) {
473
- const transport = await this.ensureConnected();
474
- await transport.request({
475
- message: { type: "ApplySnapshotFull", payload },
476
- });
477
- }
478
- async setEnvironment(environment, opts) {
479
- const transport = await this.ensureConnected();
480
- await transport.request({
481
- message: {
482
- type: "SetEnvironment",
483
- payload: { environment, merge: opts?.merge },
484
- },
485
- });
486
- }
487
- async getEnvironment() {
488
- const transport = await this.ensureConnected();
489
- const res = await transport.request({
490
- message: { type: "GetEnvironment" },
491
- });
492
- const payload = res?.message || {};
493
- return payload.environment || {};
494
- }
495
- async coerce(from, to, value) {
496
- const transport = await this.ensureConnected();
497
- const res = await transport.request({
498
- message: { type: "Coerce", payload: { from, to, value } },
499
- });
500
- const payload = res?.message || {};
501
- return payload.value;
502
- }
503
- getEngine() {
504
- if (!this.engine) {
505
- throw new Error("Engine not available: call connect() first");
506
- }
507
- return this.engine;
508
- }
509
- /**
510
- * Dispose the client and close the transport connection.
511
- * Idempotent: safe to call multiple times.
512
- */
513
- async dispose() {
514
- if (this.disposed)
515
- return;
516
- this.disposed = true;
517
- // Clear connecting promise if any
518
- this.connectingPromise = undefined;
519
- // Unsubscribe from transport events
520
- if (this.transportUnsubscribe) {
521
- this.transportUnsubscribe();
522
- this.transportUnsubscribe = undefined;
523
- }
524
- // Close transport connection
525
- if (this.transport) {
526
- const transportToClose = this.transport;
527
- this.transport = undefined;
528
- this.engine = undefined; // Clear engine reference
529
- await transportToClose.close().catch((err) => {
530
- console.warn("[RuntimeApiClient] Error closing transport:", err);
531
- });
532
- }
533
- // Clear listeners
534
- this.customEventListeners.clear();
535
- this.transportEventListeners.clear();
536
- this.emitTransportStatus({ state: "disconnected", kind: this.config.kind });
537
- }
538
- }
539
-
540
- /* eslint-disable @typescript-eslint/no-explicit-any */
541
- function summarize(value, maxLen = 120) {
542
- try {
543
- const s = typeof value === "string" ? value : JSON.stringify(value);
544
- return s.length > maxLen ? s.slice(0, maxLen) + "…" : s;
545
- }
546
- catch {
547
- return String(value);
548
- }
549
- }
550
- function serializeError(err) {
551
- try {
552
- if (!err)
553
- return { message: String(err) };
554
- const errAny = err;
555
- if (err instanceof Error) {
556
- const base = {
557
- name: err.name,
558
- message: err.message,
559
- stack: err.stack,
560
- };
561
- for (const k of Object.keys(errAny))
562
- base[k] = errAny[k];
563
- return base;
564
- }
565
- if (typeof err === "object") {
566
- const maybeMsg = errAny?.message;
567
- const maybeName = errAny?.name;
568
- const maybeStack = errAny?.stack;
569
- const out = { ...errAny };
570
- if (maybeMsg && !out.message)
571
- out.message = String(maybeMsg);
572
- if (maybeName && !out.name)
573
- out.name = String(maybeName);
574
- if (maybeStack && !out.stack)
575
- out.stack = String(maybeStack);
576
- return out;
577
- }
578
- return { message: String(err) };
579
- }
580
- catch {
581
- return { message: String(err) };
582
- }
583
- }
584
-
585
- class RuntimeApiServer {
586
- constructor(runtimeApi, label, onError) {
587
- this.runtimeApi = runtimeApi;
588
- this.label = label;
589
- this.onError = onError;
590
- this.middlewares = [];
591
- }
592
- /**
593
- * Register middleware to preprocess commands before handling.
594
- * Middlewares are executed in registration order.
595
- */
596
- use(middleware) {
597
- this.middlewares.push(middleware);
598
- }
599
- /**
600
- * Handle command with error handling and middleware support.
601
- */
602
- async handleCommand(env, ack) {
603
- try {
604
- // Execute middlewares in sequence, then process command
605
- await this.executeMiddlewares(env, async () => {
606
- await this.processCommand(env, ack);
607
- });
608
- }
609
- catch (err) {
610
- this.handleError(env, err);
611
- }
612
- }
613
- /**
614
- * Execute middlewares in sequence, then call the final handler.
615
- */
616
- async executeMiddlewares(env, finalHandler) {
617
- let index = 0;
618
- const next = async () => {
619
- if (index >= this.middlewares.length) {
620
- await finalHandler();
621
- return;
622
- }
623
- const middleware = this.middlewares[index++];
624
- await middleware(env, next);
625
- };
626
- await next();
627
- }
628
- /**
629
- * Process the command by routing to the appropriate handler.
630
- */
631
- async processCommand(env, ack) {
632
- const msg = env.message;
633
- switch (msg.type) {
634
- case "Build": {
635
- this.logCommand("Build", env, {
636
- nodes: msg.payload.def.nodes?.length ?? 0,
637
- edges: msg.payload.def.edges?.length ?? 0,
638
- envKeys: Object.keys(msg.payload.environment ?? {}).join(","),
639
- });
640
- await this.runtimeApi.build(msg.payload.def, msg.payload.environment);
641
- ack();
642
- break;
643
- }
644
- case "Update": {
645
- this.logCommand("Update", env, {
646
- nodes: msg.payload.def.nodes?.length ?? 0,
647
- edges: msg.payload.def.edges?.length ?? 0,
648
- });
649
- await this.runtimeApi.update(msg.payload.def);
650
- ack();
651
- break;
652
- }
653
- case "SetEnvironment": {
654
- this.logCommand("SetEnvironment", env);
655
- this.runtimeApi.setEnvironment(msg.payload.environment, {
656
- merge: Boolean(msg.payload.merge),
657
- });
658
- ack();
659
- break;
660
- }
661
- case "SetInput": {
662
- this.logCommand("SetInput", env, {
663
- nodeId: msg.payload.nodeId,
664
- handle: msg.payload.handle,
665
- value: summarize(msg.payload.value),
666
- });
667
- this.runtimeApi.setInput(msg.payload.nodeId, msg.payload.handle, msg.payload.value);
668
- ack();
669
- break;
670
- }
671
- case "SetInputs": {
672
- this.logCommand("SetInputs", env, {
673
- nodeId: msg.payload.nodeId,
674
- keys: Object.keys(msg.payload.inputs || {}).join(","),
675
- });
676
- const nodeId = msg.payload.nodeId;
677
- const inputs = msg.payload.inputs;
678
- this.runtimeApi.setInputs(nodeId, inputs);
679
- ack();
680
- break;
681
- }
682
- case "TriggerExternal": {
683
- this.logCommand("TriggerExternal", env, {
684
- nodeId: msg.payload.nodeId,
685
- event: summarize(msg.payload.event),
686
- });
687
- this.runtimeApi.triggerExternal(msg.payload.nodeId, msg.payload.event);
688
- ack();
689
- break;
690
- }
691
- case "Launch": {
692
- this.logCommand("Launch", env);
693
- this.runtimeApi.launch(msg.payload.invalidate);
694
- ack();
695
- break;
696
- }
697
- case "WhenIdle": {
698
- this.logCommand("WhenIdle", env);
699
- await this.runtimeApi.whenIdle();
700
- ack();
701
- break;
702
- }
703
- case "Snapshot": {
704
- this.logCommand("Snapshot", env);
705
- const snap = this.runtimeApi.snapshot();
706
- ack({ snapshot: snap });
707
- break;
708
- }
709
- case "SnapshotFull": {
710
- this.logCommand("SnapshotFull", env);
711
- const snap = this.runtimeApi.snapshotFull();
712
- ack({ snapshot: snap });
713
- break;
714
- }
715
- case "GetEnvironment": {
716
- this.logCommand("GetEnvironment", env);
717
- const environment = this.runtimeApi.getEnvironment();
718
- ack({ environment });
719
- break;
720
- }
721
- case "ApplySnapshotFull": {
722
- this.logCommand("ApplySnapshotFull", env);
723
- await this.runtimeApi.applySnapshotFull(msg.payload);
724
- ack();
725
- break;
726
- }
727
- case "Coerce": {
728
- this.logCommand("Coerce", env, {
729
- from: msg.payload.from,
730
- to: msg.payload.to,
731
- });
732
- const value = await this.runtimeApi.coerce(msg.payload?.from, msg.payload?.to, msg.payload?.value);
733
- ack({ value });
734
- break;
735
- }
736
- case "DescribeRegistry": {
737
- this.logCommand("DescribeRegistry", env);
738
- const desc = this.runtimeApi.describeRegistry();
739
- ack({ registry: desc });
740
- break;
741
- }
742
- case "RegistryApply": {
743
- this.logCommand("RegistryApply", env);
744
- await this.runtimeApi.applyRegistry(msg.payload.deltas || []);
745
- ack();
746
- break;
747
- }
748
- case "Dispose": {
749
- this.logCommand("Dispose", env);
750
- this.runtimeApi.dispose();
751
- ack();
752
- break;
753
- }
754
- case "Pause":
755
- case "Resume": {
756
- this.logCommand(`${msg.type} (not-impl)`, env);
757
- ack();
758
- break;
759
- }
760
- default: {
761
- this.logCommand("Unknown type", env);
762
- ack();
763
- }
764
- }
765
- }
766
- /**
767
- * Helper method to log commands with consistent format.
768
- */
769
- logCommand(type, env, extra) {
770
- const extraStr = extra
771
- ? ` ${Object.entries(extra)
772
- .map(([k, v]) => `${k}=${v}`)
773
- .join(" ")}`
774
- : "";
775
- console.debug(`[${this.label}] rx ${type} seq=${env.seq}${extraStr}`);
776
- }
777
- /**
778
- * Handle errors and notify error handler if provided.
779
- * Formats errors as SystemError since these are infrastructure/command processing errors,
780
- * not node execution errors.
781
- */
782
- handleError(env, err) {
783
- console.error(`[${this.label}] error handling command:`, err);
784
- const serialized = serializeError(err);
785
- const systemError = {
786
- kind: "system",
787
- message: serialized?.message || String(err) || "Unknown error",
788
- code: serialized?.code || serialized?.statusCode || 500,
789
- err: serialized,
790
- };
791
- const errorEnv = {
792
- seq: env.seq ?? Date.now(),
793
- ts: Date.now(),
794
- message: { type: "error", payload: systemError },
795
- };
796
- if (this.onError) {
797
- this.onError(errorEnv);
798
- }
799
- }
800
- }
801
-
802
- async function createRuntimeAdapter(createRegistry, send, extensions) {
803
- const registry = await createRegistry();
804
- const builder = new GraphBuilder(registry);
805
- let graphRuntime;
806
- let extData = {};
807
- // Helper to get current context
808
- const getContext = () => ({
809
- registry,
810
- builder,
811
- graphRuntime,
812
- extData,
813
- });
814
- // Original implementations - define as separate functions first to allow cross-references
815
- const originalApi = {
816
- coerce: async (from, to, value) => {
817
- const resolved = registry.resolveCoercion(from, to);
818
- if (!resolved)
819
- return value;
820
- if (resolved.kind === "sync")
821
- return resolved.convert(value);
822
- const ac = new AbortController();
823
- return await resolved.convertAsync(value, ac.signal);
824
- },
825
- getEnvironment: () => {
826
- return graphRuntime?.getEnvironment?.() ?? {};
827
- },
828
- applyRegistry: async (deltas) => {
829
- // Pause runtime if exists
830
- // Apply each delta to the live registry
831
- for (const d of deltas || []) {
832
- if (!d || typeof d !== "object")
833
- continue;
834
- if (d.kind === "register-enum") {
835
- registry.registerEnum({
836
- id: d.id,
837
- displayName: d.displayName,
838
- options: d.options || [],
839
- bakeTarget: d.bakeTarget,
840
- opts: d.opts,
841
- });
842
- }
843
- else if (d.kind === "register-type") {
844
- registry.registerType({
845
- id: d.id,
846
- displayName: d.displayName,
847
- bakeTarget: d.bakeTarget,
848
- validate: (_v) => true,
849
- });
850
- }
851
- else if (d.kind === "register-node") {
852
- const desc = d.desc || {};
853
- registry.registerNode({
854
- id: String(desc.id || ""),
855
- categoryId: String(desc.categoryId || "compute"),
856
- displayName: desc.displayName,
857
- inputs: desc.inputs || {},
858
- outputs: desc.outputs || {},
859
- // impl must be empty per frontend registration contract
860
- impl: () => { },
861
- });
862
- }
863
- }
864
- // Notify clients (include deltas in invalidate payload)
865
- send({
866
- message: {
867
- type: "invalidate",
868
- payload: { reason: "registry-changed", deltas },
869
- },
870
- });
871
- },
872
- build: async (def, opts) => {
873
- const env = opts || {};
874
- graphRuntime = builder.build(def, { environment: env });
875
- graphRuntime.on("value", (p) => send({ message: { type: "value", payload: p } }));
876
- graphRuntime.on("invalidate", (p) => send({ message: { type: "invalidate", payload: p } }));
877
- graphRuntime.on("error", (p) => send({ message: { type: "error", payload: p } }));
878
- graphRuntime.on("stats", (p) => send({ message: { type: "stats", payload: p } }));
879
- },
880
- setExtData: (data) => {
881
- if (!data || typeof data !== "object") {
882
- extData = {};
883
- return;
884
- }
885
- // Replace to keep semantics deterministic
886
- extData = { ...data };
887
- },
888
- getExtData: () => {
889
- return extData;
890
- },
891
- snapshot: () => {
892
- const inputs = {};
893
- const outputs = {};
894
- if (!graphRuntime)
895
- return { inputs, outputs };
896
- const nodes = graphRuntime.getNodeIds();
897
- for (const nodeId of nodes) {
898
- const data = graphRuntime.getNodeData(nodeId);
899
- if (data?.inputs && Object.keys(data.inputs).length > 0) {
900
- inputs[nodeId] = { ...data.inputs };
901
- }
902
- if (data?.outputs && Object.keys(data.outputs).length > 0) {
903
- outputs[nodeId] = { ...data.outputs };
904
- }
905
- }
906
- return { inputs, outputs };
907
- },
908
- snapshotFull: () => {
909
- const snap = originalApi.snapshot();
910
- const env = graphRuntime?.getEnvironment?.() ?? {};
911
- const def = graphRuntime?.getGraphDef();
912
- return {
913
- def,
914
- environment: env,
915
- inputs: snap.inputs,
916
- outputs: snap.outputs,
917
- };
918
- },
919
- applySnapshotFull: async (payload) => {
920
- const def = payload.def;
921
- if (!def)
922
- return;
923
- await originalApi.build(def, payload.environment);
924
- // Hydrate inputs/outputs exactly, then re-emit outputs without scheduling runs
925
- graphRuntime?.hydrate({
926
- inputs: payload.inputs,
927
- outputs: payload.outputs,
928
- });
929
- },
930
- describeRegistry: () => {
931
- // types (include enum options when available)
932
- const types = Array.from(registry.types.entries()).map(([id, d]) => {
933
- const en = registry.enums.get(id);
934
- return {
935
- id,
936
- displayName: d.displayName,
937
- bakeTarget: d.bakeTarget,
938
- ...(en ? { options: en.options } : {}),
939
- };
940
- });
941
- // categories: not directly enumerable; derive from node descriptors
942
- const nodeDescs = Array.from(registry.nodes.values());
943
- const catIds = new Set(nodeDescs.map((n) => n.categoryId));
944
- const categories = Array.from(catIds).map((id) => {
945
- const cat = registry.categories.get?.(id);
946
- return { id, displayName: cat?.displayName };
947
- });
948
- const nodes = nodeDescs.map((n) => ({
949
- id: n.id,
950
- categoryId: n.categoryId,
951
- displayName: n.displayName,
952
- inputs: n.inputs || {},
953
- outputs: n.outputs || {},
954
- inputDefaults: n.inputDefaults || {},
955
- }));
956
- const coercions = registry.listCoercions();
957
- return { types, categories, nodes, coercions, schemaVersion: 4 };
958
- },
959
- update: async (def) => {
960
- if (!graphRuntime)
961
- return;
962
- graphRuntime.update(def, registry);
963
- send({
964
- message: {
965
- type: "invalidate",
966
- payload: { reason: "graph-updated" },
967
- },
968
- });
969
- },
970
- setEnvironment: (env, opts) => {
971
- if (!graphRuntime)
972
- return;
973
- if (opts?.merge) {
974
- const current = graphRuntime.getEnvironment();
975
- const next = { ...(current || {}), ...(env || {}) };
976
- graphRuntime.setEnvironment(next);
977
- return;
978
- }
979
- graphRuntime.setEnvironment(env);
980
- },
981
- setInput: (nodeId, handle, value) => {
982
- graphRuntime?.setInput(nodeId, handle, value);
983
- },
984
- setInputs: (nodeId, inputs) => {
985
- graphRuntime?.setInputs(nodeId, inputs);
986
- },
987
- triggerExternal: (nodeId, event) => {
988
- graphRuntime?.triggerExternal(nodeId, event);
989
- },
990
- launch: (invalidate) => {
991
- graphRuntime?.launch(invalidate);
992
- },
993
- whenIdle: () => {
994
- return graphRuntime?.whenIdle?.() ?? Promise.resolve();
995
- },
996
- dispose: () => {
997
- graphRuntime?.dispose?.();
998
- graphRuntime = undefined;
999
- },
1000
- };
1001
- // Helper to wrap a method with extension support
1002
- const wrapMethod = (key, original) => {
1003
- const extension = extensions?.[key];
1004
- if (!extension) {
1005
- return original;
1006
- }
1007
- return ((...args) => {
1008
- return extension(original, getContext(), ...args);
1009
- });
1010
- };
1011
- // Create API with extensions applied
1012
- const extendedApi = {
1013
- coerce: wrapMethod("coerce", originalApi.coerce),
1014
- getEnvironment: wrapMethod("getEnvironment", originalApi.getEnvironment),
1015
- applyRegistry: wrapMethod("applyRegistry", originalApi.applyRegistry),
1016
- build: wrapMethod("build", originalApi.build),
1017
- setExtData: wrapMethod("setExtData", originalApi.setExtData),
1018
- getExtData: wrapMethod("getExtData", originalApi.getExtData),
1019
- snapshot: wrapMethod("snapshot", originalApi.snapshot),
1020
- snapshotFull: wrapMethod("snapshotFull", originalApi.snapshotFull),
1021
- applySnapshotFull: wrapMethod("applySnapshotFull", originalApi.applySnapshotFull),
1022
- describeRegistry: wrapMethod("describeRegistry", originalApi.describeRegistry),
1023
- update: wrapMethod("update", originalApi.update),
1024
- setEnvironment: wrapMethod("setEnvironment", originalApi.setEnvironment),
1025
- setInput: wrapMethod("setInput", originalApi.setInput),
1026
- setInputs: wrapMethod("setInputs", originalApi.setInputs),
1027
- triggerExternal: wrapMethod("triggerExternal", originalApi.triggerExternal),
1028
- launch: wrapMethod("launch", originalApi.launch),
1029
- whenIdle: wrapMethod("whenIdle", originalApi.whenIdle),
1030
- dispose: wrapMethod("dispose", originalApi.dispose),
1031
- };
1032
- return extendedApi;
1033
- }
1034
-
1035
- export { HttpPollingTransport as H, RemoteEngine as R, SeqGenerator as S, WebSocketTransport as W, RuntimeApiClient as a, serializeError as b, RuntimeApiServer as c, createRuntimeAdapter as d, summarize as s };
1036
- //# sourceMappingURL=index-FlgLfVyO.js.map