@bian-womp/spark-workbench 0.3.79 → 0.3.81

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