@bian-womp/spark-workbench 0.3.11 → 0.3.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/lib/cjs/index.cjs CHANGED
@@ -1387,116 +1387,145 @@ let remoteRunnerCounter = 0;
1387
1387
  class RemoteGraphRunner extends AbstractGraphRunner {
1388
1388
  /**
1389
1389
  * Fetch full registry description from remote and register it locally.
1390
- * Called automatically on first connection with retry mechanism.
1390
+ * Simplified with straightforward retry loop.
1391
+ * Ensures only one fetch happens at a time, even with concurrent calls.
1391
1392
  */
1392
- async fetchRegistry(client, attempt = 1) {
1393
- if (this.registryFetching) {
1394
- // Already fetching, don't start another fetch
1393
+ async fetchRegistry(client) {
1394
+ if (this.registryFetched) {
1395
1395
  return;
1396
1396
  }
1397
- this.registryFetching = true;
1398
- try {
1399
- const desc = await client.api.describeRegistry();
1400
- // Register types
1401
- for (const t of desc.types) {
1402
- if (t.options) {
1403
- this.registry.registerEnum({
1404
- id: t.id,
1405
- options: t.options,
1406
- bakeTarget: t.bakeTarget,
1407
- });
1408
- }
1409
- else {
1410
- if (!this.registry.types.has(t.id)) {
1411
- this.registry.registerType({
1397
+ // If already fetching, wait for that fetch to complete
1398
+ if (this.registryFetchPromise) {
1399
+ return this.registryFetchPromise;
1400
+ }
1401
+ // Create promise wrapper and assign atomically to prevent race conditions
1402
+ // This ensures concurrent calls will see the promise and wait for it
1403
+ let promise;
1404
+ this.registryFetchPromise = promise = (async () => {
1405
+ try {
1406
+ await this._doFetchRegistry(client);
1407
+ }
1408
+ finally {
1409
+ // Clear the promise after completion (success or failure)
1410
+ this.registryFetchPromise = undefined;
1411
+ }
1412
+ })();
1413
+ return promise;
1414
+ }
1415
+ /**
1416
+ * Internal method that performs the actual registry fetch.
1417
+ */
1418
+ async _doFetchRegistry(client) {
1419
+ let lastError;
1420
+ for (let attempt = 1; attempt <= this.MAX_REGISTRY_FETCH_ATTEMPTS; attempt++) {
1421
+ try {
1422
+ // Add timeout to registry fetch - if it exceeds 3s, retry
1423
+ let timeoutId;
1424
+ const timeoutPromise = new Promise((_, reject) => {
1425
+ timeoutId = setTimeout(() => reject(new Error("Registry fetch timeout (3s exceeded)")), this.REGISTRY_FETCH_TIMEOUT_MS);
1426
+ });
1427
+ const fetchPromise = client.api.describeRegistry().finally(() => {
1428
+ // Clear timeout if request completes first
1429
+ if (timeoutId) {
1430
+ clearTimeout(timeoutId);
1431
+ timeoutId = undefined;
1432
+ }
1433
+ });
1434
+ const desc = await Promise.race([fetchPromise, timeoutPromise]);
1435
+ // Register types
1436
+ for (const t of desc.types) {
1437
+ if (t.options) {
1438
+ this.registry.registerEnum({
1412
1439
  id: t.id,
1413
- displayName: t.displayName,
1414
- validate: (_v) => true,
1440
+ options: t.options,
1415
1441
  bakeTarget: t.bakeTarget,
1416
1442
  });
1417
1443
  }
1444
+ else {
1445
+ if (!this.registry.types.has(t.id)) {
1446
+ this.registry.registerType({
1447
+ id: t.id,
1448
+ displayName: t.displayName,
1449
+ validate: (_v) => true,
1450
+ bakeTarget: t.bakeTarget,
1451
+ });
1452
+ }
1453
+ }
1418
1454
  }
1419
- }
1420
- // Register categories
1421
- for (const c of desc.categories || []) {
1422
- if (!this.registry.categories.has(c.id)) {
1423
- // Create placeholder category descriptor
1424
- const category = {
1425
- id: c.id,
1426
- displayName: c.displayName,
1427
- createRuntime: () => ({
1428
- async onInputsChanged() { },
1429
- }),
1430
- policy: { asyncConcurrency: "switch" },
1431
- };
1432
- this.registry.categories.register(category);
1455
+ // Register categories
1456
+ for (const c of desc.categories || []) {
1457
+ if (!this.registry.categories.has(c.id)) {
1458
+ // Create placeholder category descriptor
1459
+ const category = {
1460
+ id: c.id,
1461
+ displayName: c.displayName,
1462
+ createRuntime: () => ({
1463
+ async onInputsChanged() { },
1464
+ }),
1465
+ policy: { asyncConcurrency: "switch" },
1466
+ };
1467
+ this.registry.categories.register(category);
1468
+ }
1433
1469
  }
1434
- }
1435
- // Register coercions
1436
- for (const c of desc.coercions) {
1437
- if (c.async) {
1438
- this.registry.registerAsyncCoercion(c.from, c.to, async (v) => v, {
1439
- nonTransitive: c.nonTransitive,
1440
- });
1470
+ // Register coercions
1471
+ for (const c of desc.coercions) {
1472
+ if (c.async) {
1473
+ this.registry.registerAsyncCoercion(c.from, c.to, async (v) => v, {
1474
+ nonTransitive: c.nonTransitive,
1475
+ });
1476
+ }
1477
+ else {
1478
+ this.registry.registerCoercion(c.from, c.to, (v) => v, {
1479
+ nonTransitive: c.nonTransitive,
1480
+ });
1481
+ }
1441
1482
  }
1442
- else {
1443
- this.registry.registerCoercion(c.from, c.to, (v) => v, {
1444
- nonTransitive: c.nonTransitive,
1445
- });
1483
+ // Register nodes
1484
+ for (const n of desc.nodes) {
1485
+ if (!this.registry.nodes.has(n.id)) {
1486
+ this.registry.registerNode({
1487
+ id: n.id,
1488
+ categoryId: n.categoryId,
1489
+ displayName: n.displayName,
1490
+ inputs: n.inputs || {},
1491
+ outputs: n.outputs || {},
1492
+ policy: n.policy || {},
1493
+ impl: () => { },
1494
+ });
1495
+ }
1446
1496
  }
1497
+ this.registryFetched = true;
1498
+ this.emit("registry", this.registry);
1499
+ return;
1447
1500
  }
1448
- // Register nodes
1449
- for (const n of desc.nodes) {
1450
- if (!this.registry.nodes.has(n.id)) {
1451
- this.registry.registerNode({
1452
- id: n.id,
1453
- categoryId: n.categoryId,
1454
- displayName: n.displayName,
1455
- inputs: n.inputs || {},
1456
- outputs: n.outputs || {},
1457
- policy: n.policy || {},
1458
- impl: () => { },
1501
+ catch (err) {
1502
+ lastError = err instanceof Error ? err : new Error(String(err));
1503
+ if (attempt < this.MAX_REGISTRY_FETCH_ATTEMPTS) {
1504
+ const delayMs = this.INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
1505
+ console.warn(`Failed to fetch registry (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying in ${delayMs}ms...`, lastError);
1506
+ // Emit error event for UI feedback
1507
+ this.emit("error", {
1508
+ kind: "registry",
1509
+ message: `Registry fetch failed (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying...`,
1510
+ err: lastError,
1511
+ attempt,
1512
+ maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
1459
1513
  });
1514
+ // Wait before retrying
1515
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1460
1516
  }
1461
1517
  }
1462
- this.registryFetched = true;
1463
- this.registryFetching = false;
1464
- this.emit("registry", this.registry);
1465
- }
1466
- catch (err) {
1467
- this.registryFetching = false;
1468
- const error = err instanceof Error ? err : new Error(String(err));
1469
- // Retry with exponential backoff if attempts remaining
1470
- if (attempt < this.MAX_REGISTRY_FETCH_ATTEMPTS) {
1471
- const delayMs = this.INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
1472
- console.warn(`Failed to fetch registry (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying in ${delayMs}ms...`, error);
1473
- // Emit error event for UI feedback
1474
- this.emit("error", {
1475
- kind: "registry",
1476
- message: `Registry fetch failed (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying...`,
1477
- err: error,
1478
- attempt,
1479
- maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
1480
- });
1481
- // Retry after delay
1482
- setTimeout(() => {
1483
- this.fetchRegistry(client, attempt + 1).catch(() => {
1484
- // Final failure handled below
1485
- });
1486
- }, delayMs);
1487
- }
1488
- else {
1489
- // Max attempts reached, emit final error
1490
- console.error(`Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts:`, error);
1491
- this.emit("error", {
1492
- kind: "registry",
1493
- message: `Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts. Please check your connection and try refreshing.`,
1494
- err: error,
1495
- attempt: this.MAX_REGISTRY_FETCH_ATTEMPTS,
1496
- maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
1497
- });
1498
- }
1499
1518
  }
1519
+ // Max attempts reached, emit final error and throw
1520
+ console.error(`Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts:`, lastError);
1521
+ this.emit("error", {
1522
+ kind: "registry",
1523
+ message: `Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts. Please check your connection and try refreshing.`,
1524
+ err: lastError,
1525
+ attempt: this.MAX_REGISTRY_FETCH_ATTEMPTS,
1526
+ maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
1527
+ });
1528
+ throw lastError;
1500
1529
  }
1501
1530
  /**
1502
1531
  * Build RemoteRuntimeClient config from RemoteExecutionBackend config.
@@ -1551,7 +1580,9 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1551
1580
  return this.clientPromise;
1552
1581
  const backend = this.backend;
1553
1582
  // Create connection promise to prevent concurrent connections
1554
- this.clientPromise = (async () => {
1583
+ // Create promise wrapper first, then assign immediately before async work starts
1584
+ let promise;
1585
+ this.clientPromise = promise = (async () => {
1555
1586
  // Build client config from backend config
1556
1587
  const clientConfig = this.buildClientConfig(backend);
1557
1588
  // Wrap custom event handler to intercept flow-viewport events and emit viewport event
@@ -1583,23 +1614,18 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1583
1614
  this.client = client;
1584
1615
  this.valueCache.clear();
1585
1616
  this.listenersBound = false;
1586
- // Auto-fetch registry on first connection (only once)
1587
- if (!this.registryFetched && !this.registryFetching) {
1588
- // Log loading state (UI can listen to transport status for loading indication)
1617
+ // Fetch registry before returning (wait for it to complete)
1618
+ // Only fetch if not already fetched (handles concurrent calls)
1619
+ if (!this.registryFetched) {
1589
1620
  console.info("[RemoteGraphRunner] Loading registry from remote...");
1590
- this.fetchRegistry(client)
1591
- .then(() => {
1592
- console.info("[RemoteGraphRunner] Loaded registry from remote");
1593
- })
1594
- .catch((err) => {
1595
- console.error("[RemoteGraphRunner] Failed to fetch registry:", err);
1596
- });
1621
+ await this.fetchRegistry(client);
1622
+ console.info("[RemoteGraphRunner] Loaded registry from remote");
1597
1623
  }
1598
1624
  // Clear promise on success
1599
1625
  this.clientPromise = undefined;
1600
1626
  return client;
1601
1627
  })();
1602
- return this.clientPromise;
1628
+ return promise;
1603
1629
  }
1604
1630
  constructor(backend) {
1605
1631
  super(backend);
@@ -1607,9 +1633,9 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1607
1633
  this.valueCache = new Map();
1608
1634
  this.listenersBound = false;
1609
1635
  this.registryFetched = false;
1610
- this.registryFetching = false;
1611
1636
  this.MAX_REGISTRY_FETCH_ATTEMPTS = 3;
1612
1637
  this.INITIAL_RETRY_DELAY_MS = 1000; // 1 second
1638
+ this.REGISTRY_FETCH_TIMEOUT_MS = 3000; // 3 seconds
1613
1639
  // Generate readable ID for this runner instance (e.g., remote-001, remote-002)
1614
1640
  remoteRunnerCounter++;
1615
1641
  this.runnerId = `remote-${String(remoteRunnerCounter).padStart(3, "0")}`;
@@ -2034,7 +2060,6 @@ class RemoteGraphRunner extends AbstractGraphRunner {
2034
2060
  const clientToDispose = this.client;
2035
2061
  this.client = undefined;
2036
2062
  this.registryFetched = false; // Reset so registry is fetched again on reconnect
2037
- this.registryFetching = false; // Reset fetching state
2038
2063
  if (clientToDispose) {
2039
2064
  try {
2040
2065
  await clientToDispose.dispose();
@@ -4389,7 +4414,7 @@ function WorkbenchProvider({ wb, runner, overrides, uiVersion, children, }) {
4389
4414
  console.error("[WorkbenchContext] Error updating graph:", err);
4390
4415
  }
4391
4416
  });
4392
- const offWbdSetValidation = wb.on("validationChanged", (r) => setValidation(r));
4417
+ const offWbSetValidation = wb.on("validationChanged", (r) => setValidation(r));
4393
4418
  const offWbSelectionChanged = wb.on("selectionChanged", async (sel) => {
4394
4419
  setSelectedNodeId(sel.nodes?.[0]);
4395
4420
  setSelectedEdgeId(sel.edges?.[0]);
@@ -4462,20 +4487,8 @@ function WorkbenchProvider({ wb, runner, overrides, uiVersion, children, }) {
4462
4487
  });
4463
4488
  const offWbError = wb.on("error", add("workbench", "error"));
4464
4489
  // Registry updates: swap registry and refresh graph validation/UI
4465
- const offRunnerRegistry = runner.on("registry", async (newReg) => {
4466
- try {
4467
- wb.setRegistry(newReg);
4468
- // Trigger a graph update so the UI revalidates with new types/enums/nodes
4469
- try {
4470
- await runner.update(wb.def);
4471
- }
4472
- catch {
4473
- console.error("Failed to update graph definition after registry changed");
4474
- }
4475
- }
4476
- catch {
4477
- console.error("Failed to handle registry changed event");
4478
- }
4490
+ const offRunnerRegistry = runner.on("registry", async (registry) => {
4491
+ wb.setRegistry(registry);
4479
4492
  });
4480
4493
  const offFlowViewport = runner.on("viewport", (event) => {
4481
4494
  const viewport = event.viewport;
@@ -4542,7 +4555,7 @@ function WorkbenchProvider({ wb, runner, overrides, uiVersion, children, }) {
4542
4555
  offWbValidationChanged();
4543
4556
  offWbError();
4544
4557
  offWbGraphChangedForUpdate();
4545
- offWbdSetValidation();
4558
+ offWbSetValidation();
4546
4559
  offWbSelectionChanged();
4547
4560
  offRunnerRegistry();
4548
4561
  offRunnerTransport();
@@ -5847,14 +5860,9 @@ const WorkbenchCanvasComponent = React.forwardRef((props, ref) => {
5847
5860
  const { nodeTypes, resolveNodeType } = React.useMemo(() => {
5848
5861
  // Build nodeTypes map using UI extension registry
5849
5862
  const custom = new Map(); // Include all types present in registry AND current graph to avoid timing issues
5850
- const ids = new Set([
5851
- ...Array.from(wb.registry.nodes.keys()),
5852
- ...wb.def.nodes.map((n) => n.typeId),
5853
- ]);
5854
- for (const typeId of ids) {
5855
- const renderer = ui.getNodeRenderer(typeId);
5856
- if (renderer)
5857
- custom.set(typeId, renderer);
5863
+ const allNodeRenderers = ui.getAllNodeRenderers();
5864
+ for (const typeId of Object.keys(allNodeRenderers)) {
5865
+ custom.set(typeId, allNodeRenderers[typeId]);
5858
5866
  }
5859
5867
  const types = {
5860
5868
  "spark-default": DefaultNode,
@@ -5867,7 +5875,7 @@ const WorkbenchCanvasComponent = React.forwardRef((props, ref) => {
5867
5875
  return { nodeTypes: types, resolveNodeType: resolver };
5868
5876
  // Include uiVersion to recompute when custom renderers are registered
5869
5877
  // Include registryVersion to recompute when registry enums/types change
5870
- }, [wb, wb.registry, registryVersion, uiVersion, ui]);
5878
+ }, [wb, wb.registry, registryVersion, ui, uiVersion]);
5871
5879
  const edgeTypes = React.useMemo(() => {
5872
5880
  // Use default edge renderer override if registered, otherwise use DefaultEdge
5873
5881
  const customEdgeRenderer = ui.getEdgeRenderer();
@@ -6526,10 +6534,7 @@ function WorkbenchStudioCanvas({ autoScroll, onAutoScrollChange, example, onExam
6526
6534
  return;
6527
6535
  const setInitialGraph = async (d, inputs) => {
6528
6536
  await wb.load(d);
6529
- try {
6530
- runner.build(wb.def);
6531
- }
6532
- catch { }
6537
+ runner.build(wb.def);
6533
6538
  if (inputs) {
6534
6539
  for (const [nodeId, map] of Object.entries(inputs)) {
6535
6540
  runner.setInputs(nodeId, map, { dry: true });
@@ -6573,13 +6578,13 @@ function WorkbenchStudioCanvas({ autoScroll, onAutoScrollChange, example, onExam
6573
6578
  const ex = examples.find((e) => e.id === key) ?? examples[0];
6574
6579
  if (!ex)
6575
6580
  return;
6576
- const { registry: r, def, inputs } = await ex.load();
6581
+ const { registry, def, inputs } = await ex.load();
6577
6582
  // Keep registry consistent with backend:
6578
6583
  // - For local backend, allow example to provide its own registry
6579
6584
  // - For remote backend, registry is automatically managed by RemoteGraphRunner
6580
6585
  if (backendKind === "local") {
6581
- if (r) {
6582
- wb.setRegistry(r);
6586
+ if (registry) {
6587
+ wb.setRegistry(registry);
6583
6588
  }
6584
6589
  }
6585
6590
  await wb.load(def);