@bian-womp/spark-workbench 0.3.78 → 0.3.80

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/lib/cjs/index.cjs CHANGED
@@ -1425,10 +1425,7 @@ class LocalGraphRunner extends AbstractGraphRunner {
1425
1425
  const snapshot = await this.snapshotFull();
1426
1426
  const converter = sparkGraph.buildValueConverter(converterConfig);
1427
1427
  const converted = sparkGraph.convertSnapshot(snapshot, converter);
1428
- await this.applySnapshotFull(converted, {
1429
- skipBuild: true,
1430
- dry: options?.dry,
1431
- });
1428
+ await this.applySnapshotFull(converted, { skipBuild: true, dry: options?.dry });
1432
1429
  return converted;
1433
1430
  }
1434
1431
  async pause() {
@@ -1506,139 +1503,101 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1506
1503
  getCacheKey(nodeId, handle, io) {
1507
1504
  return `${nodeId}.${io}.${handle}`;
1508
1505
  }
1509
- /**
1510
- * Fetch full registry description from remote and register it locally.
1511
- * Simplified with straightforward retry loop.
1512
- * Ensures only one fetch happens at a time, even with concurrent calls.
1513
- */
1514
- async fetchRegistry(client) {
1515
- if (this.registryFetched) {
1516
- return;
1517
- }
1518
- // If already fetching, wait for that fetch to complete
1519
- if (this.registryFetchPromise) {
1520
- return this.registryFetchPromise;
1521
- }
1522
- // Create promise wrapper and assign atomically to prevent race conditions
1523
- // This ensures concurrent calls will see the promise and wait for it
1524
- let promise;
1525
- this.registryFetchPromise = promise = (async () => {
1526
- try {
1527
- await this._doFetchRegistry(client);
1506
+ applyRegistryDescriptor(desc) {
1507
+ for (const t of desc.types) {
1508
+ if (t.options) {
1509
+ this.registry.registerEnum({
1510
+ id: t.id,
1511
+ options: t.options,
1512
+ bakeTarget: t.bakeTarget,
1513
+ });
1528
1514
  }
1529
- finally {
1530
- // Clear the promise after completion (success or failure)
1531
- this.registryFetchPromise = undefined;
1515
+ else if (!this.registry.types.has(t.id)) {
1516
+ this.registry.registerType({
1517
+ id: t.id,
1518
+ displayName: t.displayName,
1519
+ validate: (_v) => true,
1520
+ bakeTarget: t.bakeTarget,
1521
+ });
1532
1522
  }
1533
- })();
1534
- return promise;
1535
- }
1536
- /**
1537
- * Internal method that performs the actual registry fetch.
1538
- */
1539
- async _doFetchRegistry(client) {
1540
- let lastError;
1541
- for (let attempt = 1; attempt <= this.MAX_REGISTRY_FETCH_ATTEMPTS; attempt++) {
1542
- try {
1543
- // Add timeout to registry fetch - if it exceeds 3s, retry
1544
- let timeoutId;
1545
- const timeoutPromise = new Promise((_, reject) => {
1546
- timeoutId = setTimeout(() => reject(new Error("Registry fetch timeout (3s exceeded)")), this.REGISTRY_FETCH_TIMEOUT_MS);
1523
+ }
1524
+ for (const c of desc.categories || []) {
1525
+ if (!this.registry.categories.has(c.id)) {
1526
+ const category = {
1527
+ id: c.id,
1528
+ displayName: c.displayName,
1529
+ createRuntime: () => ({
1530
+ async onInputsChanged() { },
1531
+ }),
1532
+ policy: { asyncConcurrency: "switch" },
1533
+ };
1534
+ this.registry.categories.register(category);
1535
+ }
1536
+ }
1537
+ for (const c of desc.coercions) {
1538
+ if (c.async) {
1539
+ this.registry.registerAsyncCoercion(c.from, c.to, async (v) => v, {
1540
+ nonTransitive: c.nonTransitive,
1547
1541
  });
1548
- const fetchPromise = client.api.describeRegistry().finally(() => {
1549
- // Clear timeout if request completes first
1550
- if (timeoutId) {
1551
- clearTimeout(timeoutId);
1552
- timeoutId = undefined;
1553
- }
1542
+ }
1543
+ else {
1544
+ this.registry.registerCoercion(c.from, c.to, (v) => v, {
1545
+ nonTransitive: c.nonTransitive,
1554
1546
  });
1555
- const desc = await Promise.race([fetchPromise, timeoutPromise]);
1556
- // Register types
1557
- for (const t of desc.types) {
1558
- if (t.options) {
1559
- this.registry.registerEnum({
1560
- id: t.id,
1561
- options: t.options,
1562
- bakeTarget: t.bakeTarget,
1563
- });
1564
- }
1565
- else {
1566
- if (!this.registry.types.has(t.id)) {
1567
- this.registry.registerType({
1568
- id: t.id,
1569
- displayName: t.displayName,
1570
- validate: (_v) => true,
1571
- bakeTarget: t.bakeTarget,
1572
- });
1573
- }
1574
- }
1575
- }
1576
- // Register categories
1577
- for (const c of desc.categories || []) {
1578
- if (!this.registry.categories.has(c.id)) {
1579
- // Create placeholder category descriptor
1580
- const category = {
1581
- id: c.id,
1582
- displayName: c.displayName,
1583
- createRuntime: () => ({
1584
- async onInputsChanged() { },
1585
- }),
1586
- policy: { asyncConcurrency: "switch" },
1587
- };
1588
- this.registry.categories.register(category);
1589
- }
1590
- }
1591
- // Register coercions
1592
- for (const c of desc.coercions) {
1593
- if (c.async) {
1594
- this.registry.registerAsyncCoercion(c.from, c.to, async (v) => v, {
1595
- nonTransitive: c.nonTransitive,
1596
- });
1597
- }
1598
- else {
1599
- this.registry.registerCoercion(c.from, c.to, (v) => v, {
1600
- nonTransitive: c.nonTransitive,
1601
- });
1602
- }
1603
- }
1604
- // Register nodes
1605
- for (const n of desc.nodes) {
1606
- if (!this.registry.nodes.has(n.id)) {
1607
- this.registry.registerNode({
1608
- id: n.id,
1609
- categoryId: n.categoryId,
1610
- displayName: n.displayName,
1611
- inputs: n.inputs || {},
1612
- outputs: n.outputs || {},
1613
- policy: n.policy || {},
1614
- impl: () => { },
1615
- });
1616
- }
1617
- }
1618
- this.registryFetched = true;
1619
- this.emit("registry", this.registry);
1620
- return;
1621
1547
  }
1622
- catch (err) {
1623
- lastError = err instanceof Error ? err : new Error(String(err));
1624
- if (attempt < this.MAX_REGISTRY_FETCH_ATTEMPTS) {
1625
- const delayMs = this.INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
1626
- console.warn(`Failed to fetch registry (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying in ${delayMs}ms...`, lastError);
1627
- // Wait before retrying
1628
- await new Promise((resolve) => setTimeout(resolve, delayMs));
1629
- }
1548
+ }
1549
+ for (const n of desc.nodes) {
1550
+ if (!this.registry.nodes.has(n.id)) {
1551
+ this.registry.registerNode({
1552
+ id: n.id,
1553
+ categoryId: n.categoryId,
1554
+ displayName: n.displayName,
1555
+ inputs: n.inputs || {},
1556
+ outputs: n.outputs || {},
1557
+ policy: n.policy || {},
1558
+ impl: () => { },
1559
+ });
1630
1560
  }
1631
1561
  }
1632
- // Max attempts reached, emit final error and throw
1633
- console.error(`Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts:`, lastError);
1634
- this.emit("error", {
1635
- kind: "registry",
1636
- message: `Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts. Please check your connection and try refreshing.`,
1637
- err: lastError,
1638
- attempt: this.MAX_REGISTRY_FETCH_ATTEMPTS,
1639
- maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
1562
+ this.registryFetched = true;
1563
+ if (this.registryBootstrapResolve) {
1564
+ this.registryBootstrapResolve();
1565
+ this.registryBootstrapResolve = undefined;
1566
+ this.registryBootstrapReject = undefined;
1567
+ }
1568
+ this.emit("registry", this.registry);
1569
+ }
1570
+ isRecord(value) {
1571
+ return typeof value === "object" && value !== null;
1572
+ }
1573
+ isRegistryDescriptorEvent(message) {
1574
+ if (!this.isRecord(message))
1575
+ return false;
1576
+ if (message.type !== "registry-descriptor")
1577
+ return false;
1578
+ if (!this.isRecord(message.payload))
1579
+ return false;
1580
+ return "registry" in message.payload;
1581
+ }
1582
+ waitForRegistryBootstrap() {
1583
+ if (this.registryFetched)
1584
+ return Promise.resolve();
1585
+ if (this.registryBootstrapPromise)
1586
+ return this.registryBootstrapPromise;
1587
+ this.registryBootstrapPromise = new Promise((resolve, reject) => {
1588
+ this.registryBootstrapResolve = resolve;
1589
+ this.registryBootstrapReject = reject;
1590
+ setTimeout(() => {
1591
+ if (!this.registryFetched && this.registryBootstrapReject) {
1592
+ this.registryBootstrapReject(new Error(`Registry bootstrap timeout (${this.REGISTRY_BOOTSTRAP_TIMEOUT_MS}ms exceeded)`));
1593
+ this.registryBootstrapResolve = undefined;
1594
+ this.registryBootstrapReject = undefined;
1595
+ }
1596
+ }, this.REGISTRY_BOOTSTRAP_TIMEOUT_MS);
1597
+ }).finally(() => {
1598
+ this.registryBootstrapPromise = undefined;
1640
1599
  });
1641
- throw lastError;
1600
+ return this.registryBootstrapPromise;
1642
1601
  }
1643
1602
  /**
1644
1603
  * Build RemoteRuntimeClient config from RemoteExecutionBackend config.
@@ -1700,13 +1659,27 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1700
1659
  const clientConfig = this.buildClientConfig(backend);
1701
1660
  // Wrap custom event handler to intercept viewport events and emit viewport event
1702
1661
  const wrappedOnCustomEvent = (event) => {
1703
- const msg = event?.message;
1704
- if (msg && typeof msg === "object" && "type" in msg && msg.type === "viewport") {
1705
- const viewport = msg.payload?.viewport;
1662
+ const msg = event.message;
1663
+ if (this.isRecord(msg) && msg.type === "viewport" && this.isRecord(msg.payload)) {
1664
+ const viewport = msg.payload.viewport;
1706
1665
  if (isValidViewport(viewport)) {
1707
1666
  this.emit("viewport", { viewport });
1708
1667
  }
1709
1668
  }
1669
+ else if (this.isRecord(msg) && msg.type === "flow-opened" && this.isRecord(msg.payload)) {
1670
+ const sessionId = msg.payload.sessionId;
1671
+ const resumed = msg.payload.resumed;
1672
+ if (typeof sessionId === "number") {
1673
+ console.info(`[RemoteGraphRunner] Flow opened (runner=${this.runnerId}, sessionId=${sessionId}, resumed=${Boolean(resumed)})`);
1674
+ }
1675
+ }
1676
+ else if (this.isRecord(msg) && msg.type === "flow-closed" && this.isRecord(msg.payload)) {
1677
+ const reason = msg.payload.reason;
1678
+ console.warn(`[RemoteGraphRunner] Flow closed (runner=${this.runnerId}, reason=${typeof reason === "string" ? reason : "unknown"})`);
1679
+ }
1680
+ else if (this.isRegistryDescriptorEvent(msg)) {
1681
+ this.applyRegistryDescriptor(msg.payload.registry);
1682
+ }
1710
1683
  // Call original handler if provided
1711
1684
  if (backend.onCustomEvent) {
1712
1685
  backend.onCustomEvent(event);
@@ -1724,12 +1697,11 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1724
1697
  this.client = client;
1725
1698
  this.valueCache.clear();
1726
1699
  this.listenersBound = false;
1727
- // Fetch registry before returning (wait for it to complete)
1728
- // Only fetch if not already fetched (handles concurrent calls)
1700
+ // Wait for registry descriptor pushed by backend on flow-open.
1729
1701
  if (!this.registryFetched) {
1730
- console.info("[RemoteGraphRunner] Loading registry from remote...");
1731
- await this.fetchRegistry(client);
1732
- console.info("[RemoteGraphRunner] Loaded registry from remote");
1702
+ console.info("[RemoteGraphRunner] Waiting for registry bootstrap event...");
1703
+ await this.waitForRegistryBootstrap();
1704
+ console.info("[RemoteGraphRunner] Received registry bootstrap event");
1733
1705
  }
1734
1706
  // Clear promise on success
1735
1707
  this.clientPromise = undefined;
@@ -1743,9 +1715,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1743
1715
  this.valueCache = new Map();
1744
1716
  this.listenersBound = false;
1745
1717
  this.registryFetched = false;
1746
- this.MAX_REGISTRY_FETCH_ATTEMPTS = 3;
1747
- this.INITIAL_RETRY_DELAY_MS = 1000; // 1 second
1748
- this.REGISTRY_FETCH_TIMEOUT_MS = 3000; // 3 seconds
1718
+ this.REGISTRY_BOOTSTRAP_TIMEOUT_MS = 15000; // 15 seconds
1749
1719
  // Generate readable ID for this runner instance (e.g., remote-001, remote-002)
1750
1720
  remoteRunnerCounter++;
1751
1721
  this.runnerId = `remote-${String(remoteRunnerCounter).padStart(3, "0")}`;
@@ -2032,9 +2002,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
2032
2002
  this.hydrateValueCache(payload, { dry: options?.dry });
2033
2003
  // Then sync with backend
2034
2004
  const client = await this.ensureClient();
2035
- await client.api.applySnapshotFull(payload, {
2036
- skipBuild: options?.skipBuild,
2037
- });
2005
+ await client.api.applySnapshotFull(payload, { skipBuild: options?.skipBuild });
2038
2006
  }
2039
2007
  async convertSnapshot(converterConfig, options) {
2040
2008
  const client = await this.ensureClient();