@elaraai/e3-ui-components 1.0.78 → 1.0.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.
Files changed (35) hide show
  1. package/README.md +1 -1
  2. package/dist/components/DatasetPreview.d.ts.map +1 -1
  3. package/dist/components/PagedDatasetPreview.d.ts.map +1 -1
  4. package/dist/components/TaskPreview.d.ts.map +1 -1
  5. package/dist/components/UITaskPreview.d.ts +7 -3
  6. package/dist/components/UITaskPreview.d.ts.map +1 -1
  7. package/dist/hooks/index.d.ts +1 -1
  8. package/dist/hooks/index.d.ts.map +1 -1
  9. package/dist/hooks/useDatasetValue.d.ts +13 -2
  10. package/dist/hooks/useDatasetValue.d.ts.map +1 -1
  11. package/dist/hooks/useTaskDetails.d.ts +1 -5
  12. package/dist/hooks/useTaskDetails.d.ts.map +1 -1
  13. package/dist/hooks/workspaces.d.ts +16 -3
  14. package/dist/hooks/workspaces.d.ts.map +1 -1
  15. package/dist/index.cjs +740 -319
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.js +745 -324
  18. package/dist/index.js.map +1 -1
  19. package/dist/platform/bind-runtime.d.ts +14 -5
  20. package/dist/platform/bind-runtime.d.ts.map +1 -1
  21. package/dist/platform/dataset-hooks.d.ts +12 -4
  22. package/dist/platform/dataset-hooks.d.ts.map +1 -1
  23. package/dist/platform/dataset-store.d.ts +47 -11
  24. package/dist/platform/dataset-store.d.ts.map +1 -1
  25. package/dist/platform/func-runtime.d.ts +14 -4
  26. package/dist/platform/func-runtime.d.ts.map +1 -1
  27. package/dist/platform/index.d.ts +1 -1
  28. package/dist/platform/index.d.ts.map +1 -1
  29. package/dist/platform/paged-runtime.d.ts +183 -49
  30. package/dist/platform/paged-runtime.d.ts.map +1 -1
  31. package/dist/platform/record-runtime.d.ts +14 -4
  32. package/dist/platform/record-runtime.d.ts.map +1 -1
  33. package/dist/platform/staged-store.d.ts +19 -2
  34. package/dist/platform/staged-store.d.ts.map +1 -1
  35. package/package.json +13 -13
package/dist/index.cjs CHANGED
@@ -2077,6 +2077,7 @@ const realClock = {
2077
2077
  return { clear: () => clearInterval(id3) };
2078
2078
  }
2079
2079
  };
2080
+ const HASH_POLL_MS = 1e3;
2080
2081
  function createDefaultDatasetApi(apiUrl, repo, getToken) {
2081
2082
  const opts = () => ({ token: getToken() });
2082
2083
  return {
@@ -2142,12 +2143,17 @@ class ReactiveDatasetCache {
2142
2143
  __publicField(this, "keyVersions", /* @__PURE__ */ new Map());
2143
2144
  // Workspace status polling — groups subscriptions by workspace for
2144
2145
  // efficiency. Each entry holds the active interval handle from the
2145
- // injected {@link Clock} so tests can drive ticking deterministically.
2146
+ // injected {@link Clock} so tests can drive ticking deterministically,
2147
+ // and each polled path with how many watchers poll it.
2146
2148
  __publicField(this, "workspacePollers", /* @__PURE__ */ new Map());
2147
2149
  __publicField(this, "clock");
2148
2150
  // In-flight poll dedup — concurrent callers (interval tick + a
2149
2151
  // post-write trigger) share a single round-trip rather than racing.
2150
2152
  __publicField(this, "inFlightPolls", /* @__PURE__ */ new Map());
2153
+ // Hash-only watches, by workspace and path: each callback with the hash it
2154
+ // was last told. A watched path rides its workspace's poll, but its
2155
+ // content is never fetched — a paged source reads windows, never the value.
2156
+ __publicField(this, "hashWatches", /* @__PURE__ */ new Map());
2151
2157
  // Batching
2152
2158
  __publicField(this, "batchDepth", 0);
2153
2159
  __publicField(this, "changedKeys", /* @__PURE__ */ new Set());
@@ -2236,9 +2242,7 @@ class ReactiveDatasetCache {
2236
2242
  hash: void 0,
2237
2243
  status: east.variant("stale", null)
2238
2244
  });
2239
- if (this.workspacePollers.has(workspace)) {
2240
- void this.pollWorkspaceStatus(workspace);
2241
- }
2245
+ void this.refresh(workspace);
2242
2246
  } catch (error3) {
2243
2247
  if (!this.destroyed && this.writeEpochs.get(key) === myEpoch) {
2244
2248
  const baseline = this.writeBaselines.get(key);
@@ -2344,15 +2348,15 @@ class ReactiveDatasetCache {
2344
2348
  * 3. Only fetches full content when hash changes
2345
2349
  *
2346
2350
  * Multiple subscriptions to the same workspace share a single poller.
2347
- * Pair with {@link clearRefetchInterval} on unmount so the poller can
2348
- * stop when nothing is watching.
2351
+ * Pair each call with one {@link clearRefetchInterval} on unmount so the
2352
+ * poller can stop when nothing is watching.
2349
2353
  */
2350
2354
  setRefetchInterval(workspace, path2, intervalMs) {
2351
2355
  var _a2;
2352
2356
  const pathStr = datasetPathToString(path2);
2353
2357
  let poller = this.workspacePollers.get(workspace);
2354
2358
  if (poller) {
2355
- poller.paths.add(pathStr);
2359
+ poller.paths.set(pathStr, (poller.paths.get(pathStr) ?? 0) + 1);
2356
2360
  if (intervalMs < poller.intervalMs) {
2357
2361
  (_a2 = poller.handle) == null ? void 0 : _a2.clear();
2358
2362
  poller.intervalMs = intervalMs;
@@ -2364,7 +2368,7 @@ class ReactiveDatasetCache {
2364
2368
  } else {
2365
2369
  poller = {
2366
2370
  intervalMs,
2367
- paths: /* @__PURE__ */ new Set([pathStr]),
2371
+ paths: /* @__PURE__ */ new Map([[pathStr, 1]]),
2368
2372
  handle: this.clock.setInterval(
2369
2373
  () => this.pollWorkspaceStatus(workspace),
2370
2374
  intervalMs
@@ -2375,23 +2379,79 @@ class ReactiveDatasetCache {
2375
2379
  }
2376
2380
  }
2377
2381
  /**
2378
- * Stop polling a dataset; the workspace poller is torn down when its
2379
- * last watched path is cleared.
2382
+ * Stop polling a dataset for one watcher; the path goes when its last
2383
+ * watcher clears it, and the workspace poller is torn down when its last
2384
+ * path goes.
2380
2385
  */
2381
2386
  clearRefetchInterval(workspace, path2) {
2382
- var _a2;
2383
2387
  const poller = this.workspacePollers.get(workspace);
2384
2388
  if (!poller) return;
2385
- poller.paths.delete(datasetPathToString(path2));
2386
- if (poller.paths.size === 0) {
2389
+ const pathStr = datasetPathToString(path2);
2390
+ const watchers = (poller.paths.get(pathStr) ?? 0) - 1;
2391
+ if (watchers > 0) {
2392
+ poller.paths.set(pathStr, watchers);
2393
+ return;
2394
+ }
2395
+ poller.paths.delete(pathStr);
2396
+ this.stopIdlePoller(workspace);
2397
+ }
2398
+ /**
2399
+ * Follow a dataset's content hash without fetching its content; see the
2400
+ * interface doc.
2401
+ */
2402
+ watchHash(workspace, path2, onHash, intervalMs = HASH_POLL_MS) {
2403
+ var _a2;
2404
+ const pathStr = datasetPathToString(path2);
2405
+ let byPath = this.hashWatches.get(workspace);
2406
+ if (!byPath) {
2407
+ byPath = /* @__PURE__ */ new Map();
2408
+ this.hashWatches.set(workspace, byPath);
2409
+ }
2410
+ let callbacks = byPath.get(pathStr);
2411
+ if (!callbacks) {
2412
+ callbacks = /* @__PURE__ */ new Map();
2413
+ byPath.set(pathStr, callbacks);
2414
+ }
2415
+ callbacks.set(onHash, void 0);
2416
+ const poller = this.workspacePollers.get(workspace);
2417
+ if (!poller) {
2418
+ this.workspacePollers.set(workspace, {
2419
+ intervalMs,
2420
+ paths: /* @__PURE__ */ new Map(),
2421
+ handle: this.clock.setInterval(() => this.pollWorkspaceStatus(workspace), intervalMs)
2422
+ });
2423
+ void this.pollWorkspaceStatus(workspace);
2424
+ } else if (intervalMs < poller.intervalMs) {
2387
2425
  (_a2 = poller.handle) == null ? void 0 : _a2.clear();
2388
- this.workspacePollers.delete(workspace);
2426
+ poller.intervalMs = intervalMs;
2427
+ poller.handle = this.clock.setInterval(() => this.pollWorkspaceStatus(workspace), intervalMs);
2389
2428
  }
2429
+ return () => {
2430
+ const watched = this.hashWatches.get(workspace);
2431
+ const set3 = watched == null ? void 0 : watched.get(pathStr);
2432
+ if (!watched || !set3) return;
2433
+ set3.delete(onHash);
2434
+ if (set3.size === 0) watched.delete(pathStr);
2435
+ if (watched.size === 0) this.hashWatches.delete(workspace);
2436
+ this.stopIdlePoller(workspace);
2437
+ };
2438
+ }
2439
+ /** Stops a workspace's poller once it has no path to reconcile and no hash
2440
+ * to follow — without this, a long-lived session polls forever. */
2441
+ stopIdlePoller(workspace) {
2442
+ var _a2;
2443
+ const poller = this.workspacePollers.get(workspace);
2444
+ if (!poller || poller.paths.size > 0 || this.hashWatches.has(workspace)) return;
2445
+ (_a2 = poller.handle) == null ? void 0 : _a2.clear();
2446
+ this.workspacePollers.delete(workspace);
2390
2447
  }
2391
2448
  refresh(workspace) {
2392
2449
  if (this.destroyed) return Promise.resolve();
2393
2450
  if (!this.workspacePollers.has(workspace)) return Promise.resolve();
2394
- return this.pollWorkspaceStatus(workspace);
2451
+ const inFlight = this.inFlightPolls.get(workspace);
2452
+ if (inFlight === void 0) return this.pollWorkspaceStatus(workspace);
2453
+ const again = () => this.destroyed || !this.workspacePollers.has(workspace) ? void 0 : this.pollWorkspaceStatus(workspace);
2454
+ return inFlight.then(again, again);
2395
2455
  }
2396
2456
  /**
2397
2457
  * Poll workspace status and reconcile the cache with the server.
@@ -2407,11 +2467,12 @@ class ReactiveDatasetCache {
2407
2467
  return promise4;
2408
2468
  }
2409
2469
  async doPollWorkspaceStatus(workspace) {
2470
+ var _a2;
2410
2471
  if (this.destroyed) return;
2411
2472
  const poller = this.workspacePollers.get(workspace);
2412
- if (!poller || poller.paths.size === 0) return;
2473
+ if (!poller || poller.paths.size === 0 && !this.hashWatches.has(workspace)) return;
2413
2474
  const epochsAtStart = /* @__PURE__ */ new Map();
2414
- for (const pathStr of poller.paths) {
2475
+ for (const pathStr of poller.paths.keys()) {
2415
2476
  const key = pathStr ? `${workspace}.${pathStr}` : workspace;
2416
2477
  epochsAtStart.set(key, this.writeEpochs.get(key) ?? 0);
2417
2478
  }
@@ -2425,15 +2486,28 @@ class ReactiveDatasetCache {
2425
2486
  if (this.destroyed) return;
2426
2487
  const infoByPath = /* @__PURE__ */ new Map();
2427
2488
  for (const info of status.datasets) infoByPath.set(info.path, info);
2489
+ for (const [pathStr, callbacks] of this.hashWatches.get(workspace) ?? []) {
2490
+ const info = infoByPath.get(pathStr ? `.${pathStr}` : "");
2491
+ const hash2 = ((_a2 = info == null ? void 0 : info.hash) == null ? void 0 : _a2.type) === "some" ? info.hash.value : null;
2492
+ for (const [onHash, told] of [...callbacks]) {
2493
+ if (told === hash2 || !callbacks.has(onHash)) continue;
2494
+ callbacks.set(onHash, hash2);
2495
+ try {
2496
+ onHash(hash2);
2497
+ } catch (error3) {
2498
+ console.error(`A hash watch on ${workspace}.${pathStr} failed:`, error3);
2499
+ }
2500
+ }
2501
+ }
2428
2502
  const pending = [];
2429
2503
  this.batch(() => {
2430
- var _a2;
2431
- for (const pathStr of poller.paths) {
2504
+ var _a3;
2505
+ for (const pathStr of poller.paths.keys()) {
2432
2506
  const e3Path = pathStr ? `.${pathStr}` : "";
2433
2507
  const info = infoByPath.get(e3Path);
2434
2508
  const key = pathStr ? `${workspace}.${pathStr}` : workspace;
2435
2509
  const path2 = this.stringToPath(pathStr);
2436
- const currentHash = ((_a2 = info == null ? void 0 : info.hash) == null ? void 0 : _a2.type) === "some" ? info.hash.value : null;
2510
+ const currentHash = ((_a3 = info == null ? void 0 : info.hash) == null ? void 0 : _a3.type) === "some" ? info.hash.value : null;
2437
2511
  const knownHash = this.knownHashes.get(key);
2438
2512
  if (info == null ? void 0 : info.status) {
2439
2513
  const previous = this.statuses.get(key);
@@ -2599,6 +2673,7 @@ class ReactiveDatasetCache {
2599
2673
  }
2600
2674
  this.workspacePollers.clear();
2601
2675
  this.inFlightPolls.clear();
2676
+ this.hashWatches.clear();
2602
2677
  this.keySubscribers.clear();
2603
2678
  this.globalSubscribers.clear();
2604
2679
  this.knownHashes.clear();
@@ -2713,9 +2788,16 @@ class MemoryStagedAdapter {
2713
2788
  this.map.clear();
2714
2789
  }
2715
2790
  }
2791
+ const SCOPE_SEPARATOR = "\0";
2792
+ function cacheKeyOf(key) {
2793
+ return key.slice(key.indexOf(SCOPE_SEPARATOR) + 1);
2794
+ }
2716
2795
  class StagedStore {
2717
2796
  constructor(adapter) {
2797
+ // By scope and `datasetCacheKey`; subscribers and versions go by the
2798
+ // `datasetCacheKey` alone, which is what a reactive view tracks.
2718
2799
  __publicField(this, "entries", /* @__PURE__ */ new Map());
2800
+ __publicField(this, "scope", null);
2719
2801
  __publicField(this, "subscribers", /* @__PURE__ */ new Map());
2720
2802
  __publicField(this, "versions", /* @__PURE__ */ new Map());
2721
2803
  __publicField(this, "adapter");
@@ -2739,25 +2821,25 @@ class StagedStore {
2739
2821
  }
2740
2822
  }
2741
2823
  hasPending(workspace, path2) {
2742
- return this.entries.has(datasetCacheKey(workspace, path2));
2824
+ return this.entries.has(this.keyOf(workspace, path2));
2743
2825
  }
2744
2826
  getBuffered(workspace, path2) {
2745
2827
  var _a2;
2746
- return (_a2 = this.entries.get(datasetCacheKey(workspace, path2))) == null ? void 0 : _a2.buffered;
2828
+ return (_a2 = this.entries.get(this.keyOf(workspace, path2))) == null ? void 0 : _a2.buffered;
2747
2829
  }
2748
2830
  getSnapshot(workspace, path2) {
2749
2831
  var _a2;
2750
- return (_a2 = this.entries.get(datasetCacheKey(workspace, path2))) == null ? void 0 : _a2.snapshot;
2832
+ return (_a2 = this.entries.get(this.keyOf(workspace, path2))) == null ? void 0 : _a2.snapshot;
2751
2833
  }
2752
2834
  getEntry(workspace, path2) {
2753
- return this.entries.get(datasetCacheKey(workspace, path2));
2835
+ return this.entries.get(this.keyOf(workspace, path2));
2754
2836
  }
2755
2837
  write(workspace, path2, snapshotIfNew, buffered) {
2756
- const key = datasetCacheKey(workspace, path2);
2838
+ const key = this.keyOf(workspace, path2);
2757
2839
  const existing = this.entries.get(key);
2758
2840
  const next2 = existing ? { snapshot: existing.snapshot, buffered } : { snapshot: snapshotIfNew, buffered };
2759
2841
  this.entries.set(key, next2);
2760
- this.notify(key);
2842
+ this.notify(datasetCacheKey(workspace, path2));
2761
2843
  const persisted = {
2762
2844
  snapshot: next2.snapshot,
2763
2845
  buffered: next2.buffered
@@ -2765,15 +2847,26 @@ class StagedStore {
2765
2847
  this.persist(key, () => this.adapter.save(key, persisted));
2766
2848
  }
2767
2849
  discard(workspace, path2) {
2768
- const key = datasetCacheKey(workspace, path2);
2850
+ const key = this.keyOf(workspace, path2);
2769
2851
  if (!this.entries.has(key)) return false;
2770
2852
  this.entries.delete(key);
2771
- this.notify(key);
2853
+ this.notify(datasetCacheKey(workspace, path2));
2772
2854
  this.persist(key, () => this.adapter.remove(key));
2773
2855
  return true;
2774
2856
  }
2775
2857
  listKeys() {
2776
- return [...this.entries.keys()];
2858
+ const prefix2 = `${this.scope ?? ""}${SCOPE_SEPARATOR}`;
2859
+ return [...this.entries.keys()].filter((key) => key.startsWith(prefix2)).map(cacheKeyOf);
2860
+ }
2861
+ setScope(scope) {
2862
+ const before = this.scope;
2863
+ this.scope = scope;
2864
+ this.notifyScopes(before, scope);
2865
+ return () => {
2866
+ if (this.scope !== scope) return;
2867
+ this.scope = null;
2868
+ this.notifyScopes(scope, null);
2869
+ };
2777
2870
  }
2778
2871
  subscribe(key, callback) {
2779
2872
  let set3 = this.subscribers.get(key);
@@ -2796,15 +2889,29 @@ class StagedStore {
2796
2889
  this.errorListeners.delete(cb);
2797
2890
  };
2798
2891
  }
2799
- /** Test-only: clear in-memory + persisted state. */
2892
+ /** Test-only: clear in-memory + persisted state, in every scope. */
2800
2893
  async clear() {
2801
2894
  const keys = [...this.entries.keys()];
2802
2895
  this.entries.clear();
2803
- for (const key of keys) this.notify(key);
2896
+ for (const key of new Set(keys.map(cacheKeyOf))) this.notify(key);
2804
2897
  await this.flushPending();
2805
2898
  await this.adapter.clear();
2806
2899
  }
2807
2900
  // ----- internals -------------------------------------------------------
2901
+ /** The key a path's entry is held under in the current scope. */
2902
+ keyOf(workspace, path2) {
2903
+ return `${this.scope ?? ""}${SCOPE_SEPARATOR}${datasetCacheKey(workspace, path2)}`;
2904
+ }
2905
+ /** Tells every path with an entry in either scope that what it reads has
2906
+ * changed: the scope's move shows one's entries and hides the other's. */
2907
+ notifyScopes(from2, to) {
2908
+ const keys = /* @__PURE__ */ new Set();
2909
+ for (const key of this.entries.keys()) {
2910
+ const scope = key.slice(0, key.indexOf(SCOPE_SEPARATOR));
2911
+ if (scope === (from2 ?? "") || scope === (to ?? "")) keys.add(cacheKeyOf(key));
2912
+ }
2913
+ for (const key of keys) this.notify(key);
2914
+ }
2808
2915
  notify(key) {
2809
2916
  this.versions.set(key, (this.versions.get(key) ?? 0) + 1);
2810
2917
  const subs = this.subscribers.get(key);
@@ -2821,7 +2928,7 @@ class StagedStore {
2821
2928
  const settled = prev2.then(op, op).then(
2822
2929
  () => void 0,
2823
2930
  (err) => {
2824
- this.emitPersistError(key, err);
2931
+ this.emitPersistError(cacheKeyOf(key), err);
2825
2932
  }
2826
2933
  ).then(() => {
2827
2934
  if (this.saveChains.get(key) === settled) this.saveChains.delete(key);
@@ -2852,11 +2959,18 @@ class StagedStore {
2852
2959
  return;
2853
2960
  }
2854
2961
  for (const [key, p2] of persisted) {
2962
+ if (!key.includes(SCOPE_SEPARATOR)) {
2963
+ this.persist(key, () => this.adapter.remove(key));
2964
+ this.emitPersistError(key, new Error(
2965
+ `a staged edit of ${key} that an older build left names no server or repository, and was dropped`
2966
+ ));
2967
+ continue;
2968
+ }
2855
2969
  this.entries.set(key, {
2856
2970
  snapshot: p2.snapshot,
2857
2971
  buffered: p2.buffered
2858
2972
  });
2859
- this.notify(key);
2973
+ this.notify(cacheKeyOf(key));
2860
2974
  }
2861
2975
  }
2862
2976
  }
@@ -2973,8 +3087,14 @@ class BindRuntime {
2973
3087
  }
2974
3088
  this.cache = cache3;
2975
3089
  }
2976
- /** Clear the active cache and drop any queued writes. */
2977
- clearCache() {
3090
+ /**
3091
+ * Clear the active cache and drop any queued writes.
3092
+ *
3093
+ * @param cache - Clear only while this is the active cache, so a provider
3094
+ * tearing down after another installed its own leaves that one in place
3095
+ */
3096
+ clearCache(cache3) {
3097
+ if (cache3 !== void 0 && this.cache !== cache3) return;
2978
3098
  this.cache = null;
2979
3099
  this.clearPendingWrites();
2980
3100
  this.handleCache.clear();
@@ -3299,6 +3419,10 @@ class BindRuntime {
3299
3419
  this.queueWrite(() => cache3.launchDataflow(ws));
3300
3420
  return null;
3301
3421
  };
3422
+ const hasSource = () => {
3423
+ this.trackPath(ws, sourcePath);
3424
+ return cache3.has(ws, sourcePath);
3425
+ };
3302
3426
  if (mode === "direct" && !patchPath) {
3303
3427
  return {
3304
3428
  read: () => {
@@ -3321,7 +3445,7 @@ class BindRuntime {
3321
3445
  pending: () => false,
3322
3446
  commit: () => null,
3323
3447
  discard: () => null,
3324
- has: () => cache3.has(ws, sourcePath),
3448
+ has: hasSource,
3325
3449
  status: () => {
3326
3450
  this.trackPath(ws, sourcePath);
3327
3451
  return cache3.getStatus(ws, sourcePath);
@@ -3370,7 +3494,7 @@ class BindRuntime {
3370
3494
  this.queueWrite(() => cache3.write(ws, pPath2, encodePatch(UNCHANGED_VARIANT)));
3371
3495
  return null;
3372
3496
  },
3373
- has: () => cache3.has(ws, sourcePath),
3497
+ has: hasSource,
3374
3498
  status: () => {
3375
3499
  this.trackPath(ws, sourcePath);
3376
3500
  return cache3.getStatus(ws, sourcePath);
@@ -3414,7 +3538,7 @@ class BindRuntime {
3414
3538
  staged.discard(ws, sourcePath);
3415
3539
  return null;
3416
3540
  },
3417
- has: () => cache3.has(ws, sourcePath),
3541
+ has: hasSource,
3418
3542
  status: () => {
3419
3543
  this.trackPath(ws, sourcePath);
3420
3544
  return cache3.getStatus(ws, sourcePath);
@@ -3461,7 +3585,7 @@ class BindRuntime {
3461
3585
  staged.discard(ws, sourcePath);
3462
3586
  return null;
3463
3587
  },
3464
- has: () => cache3.has(ws, sourcePath),
3588
+ has: hasSource,
3465
3589
  status: () => {
3466
3590
  this.trackPath(ws, sourcePath);
3467
3591
  return cache3.getStatus(ws, sourcePath);
@@ -3519,8 +3643,8 @@ function getReactiveDatasetCache() {
3519
3643
  function initializeReactiveDatasetCache(cache3) {
3520
3644
  defaultBindRuntime.initializeCache(cache3);
3521
3645
  }
3522
- function clearReactiveDatasetCache() {
3523
- defaultBindRuntime.clearCache();
3646
+ function clearReactiveDatasetCache(cache3) {
3647
+ defaultBindRuntime.clearCache(cache3);
3524
3648
  }
3525
3649
  function enableBindingTracking() {
3526
3650
  return defaultBindRuntime.enableTracking();
@@ -3700,7 +3824,11 @@ class TrackedChannelStore {
3700
3824
  this.keyVersions.clear();
3701
3825
  }
3702
3826
  }
3703
- function createDefaultPagedApi(apiUrl, repo, getToken) {
3827
+ const NO_INDEX = { index: null, join: false };
3828
+ function selectorKey(selector2) {
3829
+ return selector2.index === null ? "" : `@${selector2.index}${selector2.join ? "+join" : ""}`;
3830
+ }
3831
+ function createDefaultPagedApi(apiUrl, repo, getToken, datasets) {
3704
3832
  const opts = () => ({ token: getToken() });
3705
3833
  return {
3706
3834
  async getPage(workspace, path2, window3) {
@@ -3708,58 +3836,115 @@ function createDefaultPagedApi(apiUrl, repo, getToken) {
3708
3836
  },
3709
3837
  async findKey(workspace, path2, query) {
3710
3838
  return e3ApiClient.datasetFindKey(apiUrl, repo, workspace, path2, query, opts());
3839
+ },
3840
+ async getRevision(workspace, path2) {
3841
+ const status = await e3ApiClient.datasetGetStatus(apiUrl, repo, workspace, path2, opts());
3842
+ return status.hash.type === "some" ? status.hash.value : null;
3843
+ },
3844
+ watchRevision(workspace, path2, onChange2) {
3845
+ return datasets.watchHash(workspace, path2, onChange2);
3711
3846
  }
3712
3847
  };
3713
3848
  }
3714
3849
  const RETRY_AFTER_MS = 2e3;
3715
3850
  const MAX_RETAINED_WINDOWS = 24;
3716
- const PERMANENT_PAGE_ERRORS = /* @__PURE__ */ new Set(["dataset_not_pageable"]);
3717
- function isPermanentPageError(err) {
3851
+ const PERMANENT_PAGE_ERRORS = /* @__PURE__ */ new Set([
3852
+ "bad_request",
3853
+ "dataset_not_canonical",
3854
+ "dataset_not_indexed",
3855
+ "dataset_not_pageable",
3856
+ "dataset_not_searchable",
3857
+ "dataset_not_segmented",
3858
+ "index_not_found",
3859
+ "key_parse_error"
3860
+ ]);
3861
+ const NO_VALUE_ERRORS = /* @__PURE__ */ new Set(["dataset_unassigned", "dataset_null"]);
3862
+ function toSelector(indexArg, joinArg) {
3863
+ const index2 = indexArg;
3864
+ return {
3865
+ index: index2 !== void 0 && index2.type === "some" ? index2.value : null,
3866
+ join: joinArg === true
3867
+ };
3868
+ }
3869
+ function checkDeclared(allowed, path2) {
3870
+ if (allowed === null) return;
3871
+ const pathStr = datasetPathToString(path2);
3872
+ if (!allowed.has(pathStr)) {
3873
+ throw new Error(
3874
+ `Data.bindPaged: source path "${pathStr}" not declared in manifest — bind it in the task body so derivation records it`
3875
+ );
3876
+ }
3877
+ }
3878
+ function errorCode(err) {
3718
3879
  const code = err == null ? void 0 : err.code;
3719
- return typeof code === "string" && PERMANENT_PAGE_ERRORS.has(code);
3880
+ return typeof code === "string" ? code : void 0;
3881
+ }
3882
+ function isPermanentPageError(err) {
3883
+ const code = errorCode(err);
3884
+ return code !== void 0 && PERMANENT_PAGE_ERRORS.has(code);
3885
+ }
3886
+ function failureOf(path2, err) {
3887
+ const code = errorCode(err);
3888
+ const details = err == null ? void 0 : err.details;
3889
+ const reason = code === void 0 ? err instanceof Error ? err.message : String(err) : typeof details === "string" && details !== "" ? `${code}: ${details}` : code;
3890
+ return `Data.bindPaged: ${datasetPathToString(path2)}: ${reason}`;
3720
3891
  }
3721
3892
  function keyTypeOf(sourceType) {
3722
3893
  if (sourceType.type === "Dict") return sourceType.value.key;
3723
3894
  if (sourceType.type === "Set") return sourceType.value;
3724
3895
  return null;
3725
3896
  }
3726
- function buildSeek(sourceType, T2, pathExpr, platform2) {
3727
- if (keyTypeOf(sourceType) === null) return east.none;
3897
+ function buildSeek(sourceType, T2, pathExpr, indexExpr, joinExpr, selector2, platform2) {
3898
+ if (selector2.index === null && keyTypeOf(sourceType) === null) return east.none;
3728
3899
  const { seek } = internal$1.DataPagedPrimitives;
3729
3900
  return east.some(east.East.compile(
3730
3901
  east.East.function([eastUi.SeekQueryType], east.OptionType(eastUi.SeekRangeType), ($3, query) => {
3731
- $3.return(seek([T2], pathExpr, query));
3902
+ $3.return(seek([T2], pathExpr, indexExpr, joinExpr, query));
3732
3903
  }),
3733
3904
  platform2
3734
3905
  ));
3735
3906
  }
3736
- function pagedWindowKey(workspace, path2, offset, limit2) {
3737
- return `paged:${workspace}:${datasetPathToString(path2)}#${offset}+${limit2}`;
3907
+ function pagedRevisionKey(workspace, path2) {
3908
+ return `paged:${workspace}:${datasetPathToString(path2)}#revision`;
3738
3909
  }
3739
- function pagedTotalKey(workspace, path2) {
3740
- return `paged:${workspace}:${datasetPathToString(path2)}#total`;
3910
+ function pagedWindowKey(workspace, path2, revision, offset, limit2, selector2 = NO_INDEX) {
3911
+ return `paged:${workspace}:${datasetPathToString(path2)}${selectorKey(selector2)}@${revision}#${offset}+${limit2}`;
3741
3912
  }
3742
- function pagedSeekKey(workspace, path2, query) {
3743
- const q = "key" in query ? `k=${query.key}` : "fields" in query ? `f=${query.fields.join("\0")}|p=${query.prefix ?? ""}` : `p=${query.prefix}`;
3744
- return `paged:${workspace}:${datasetPathToString(path2)}#seek:${q}`;
3913
+ function pagedTotalKey(workspace, path2, revision, selector2 = NO_INDEX) {
3914
+ return `paged:${workspace}:${datasetPathToString(path2)}${selectorKey(selector2)}@${revision}#total`;
3745
3915
  }
3746
- function toFindQuery(query) {
3916
+ function pagedSeekKey(workspace, path2, revision, query, selector2 = NO_INDEX) {
3917
+ const q = "key" in query ? `k=${query.key}` : "fields" in query ? `f=${query.fields.join("\0")}|p=${query.prefix ?? ""}` : "prefix" in query ? `p=${query.prefix}` : `r=${(query.from ?? []).join("\0")}|${(query.to ?? []).join("\0")}`;
3918
+ return `paged:${workspace}:${datasetPathToString(path2)}${selectorKey(selector2)}@${revision}#seek:${q}`;
3919
+ }
3920
+ function toFindQuery(query, selector2 = NO_INDEX) {
3747
3921
  const q = query;
3748
- if (q.type === "key") return { key: q.value };
3749
- if (q.type === "prefix") return { prefix: q.value };
3922
+ const scope = selector2.index === null ? {} : { index: selector2.index };
3923
+ if (q.type === "key") return { key: q.value, ...scope };
3924
+ if (q.type === "prefix") return { prefix: q.value, ...scope };
3925
+ if (q.type === "range") {
3926
+ const r2 = q.value;
3927
+ const from2 = [...r2.from];
3928
+ const to = [...r2.to];
3929
+ if (from2.length === 0 && to.length === 0) {
3930
+ throw new Error("Data.bindPaged: a range seek names no end — give it a `from`, a `to`, or both");
3931
+ }
3932
+ return from2.length === 0 ? { to, ...scope } : to.length === 0 ? { from: from2, ...scope } : { from: from2, to, ...scope };
3933
+ }
3750
3934
  const f2 = q.value;
3751
3935
  const fields = [...f2.values];
3752
- return f2.prefix.type === "some" ? { fields, prefix: f2.prefix.value } : { fields };
3936
+ return f2.prefix.type === "some" ? { fields, prefix: f2.prefix.value, ...scope } : { fields, ...scope };
3753
3937
  }
3754
3938
  class PagedRuntime extends TrackedChannelStore {
3755
3939
  constructor() {
3756
3940
  super(...arguments);
3757
3941
  __publicField(this, "api", null);
3758
3942
  __publicField(this, "workspace", null);
3759
- // Compiled-handle cache (issue #106 perf): buildHandle compiles 2
3943
+ // Compiled-handle cache (issue #106 perf): buildHandle compiles several
3760
3944
  // East.functions per bind, and binds re-run every reactive frame. The
3761
- // method IR is a pure function of (sourceType, sourcePath), and the methods
3762
- // resolve api/workspace LIVE, so a cached handle still re-binds.
3945
+ // method IR is a pure function of (sourceType, path, selector, shape), and
3946
+ // the methods resolve api/workspace LIVE, so a cached handle still
3947
+ // re-binds.
3763
3948
  //
3764
3949
  // Keyed by TYPE first, then path — never by path alone. The window decoder
3765
3950
  // is baked from `sourceType`, so a path re-bound at a different type (a
@@ -3774,6 +3959,10 @@ class PagedRuntime extends TrackedChannelStore {
3774
3959
  /** Loaded window keys in least-recently-READ order (a Map preserves
3775
3960
  * insertion order; a read re-inserts). Bounds the decoded-window cache. */
3776
3961
  __publicField(this, "loadedWindows", /* @__PURE__ */ new Map());
3962
+ /** Each dataset's snapshot, by workspace and path. */
3963
+ __publicField(this, "snapshots", /* @__PURE__ */ new Map());
3964
+ /** Fetches launched so far — the source of every channel's `launchSeq`. */
3965
+ __publicField(this, "launches", 0);
3777
3966
  }
3778
3967
  /** Monotonic clock seam so tests can drive the retry gate. */
3779
3968
  now() {
@@ -3784,17 +3973,33 @@ class PagedRuntime extends TrackedChannelStore {
3784
3973
  }
3785
3974
  // ----- wiring ----------------------------------------------------------
3786
3975
  /** Install the API adapter + workspace — called by the React provider
3787
- * (or a test/showcase harness) before any handle is used. */
3976
+ * (or a test/showcase harness) before any handle is used. Another adapter
3977
+ * or workspace starts from nothing: no snapshot of the old one's carries
3978
+ * over. */
3788
3979
  initialize(api3, workspace) {
3980
+ if (this.api !== api3 || this.workspace !== workspace) this.reset();
3789
3981
  this.api = api3;
3790
3982
  this.workspace = workspace;
3791
3983
  }
3792
- /** Tear down the adapter and all window state. */
3793
- clear() {
3984
+ /**
3985
+ * Tear down the adapter and all window state.
3986
+ *
3987
+ * @param api - Clear only while this is the installed adapter, so a
3988
+ * provider tearing down after another installed its own leaves that one
3989
+ * in place
3990
+ */
3991
+ clear(api3) {
3992
+ if (api3 !== void 0 && this.api !== api3) return;
3794
3993
  this.api = null;
3795
3994
  this.workspace = null;
3796
- this.clearChannels();
3995
+ this.reset();
3797
3996
  this.handleCache.clear();
3997
+ }
3998
+ /** Stop following every dataset, and drop what every snapshot delivered. */
3999
+ reset() {
4000
+ for (const snapshot of this.snapshots.values()) snapshot.unwatch();
4001
+ this.snapshots.clear();
4002
+ this.clearChannels();
3798
4003
  this.loadedWindows.clear();
3799
4004
  }
3800
4005
  resolveWorkspace() {
@@ -3805,7 +4010,178 @@ class PagedRuntime extends TrackedChannelStore {
3805
4010
  }
3806
4011
  return this.workspace;
3807
4012
  }
3808
- // ----- window loading --------------------------------------------------
4013
+ // ----- snapshots -------------------------------------------------------
4014
+ /** A dataset's snapshot, followed from its first read until the runtime
4015
+ * is cleared. */
4016
+ snapshotOf(workspace, path2) {
4017
+ const key = `${workspace}:${datasetPathToString(path2)}`;
4018
+ const existing = this.snapshots.get(key);
4019
+ if (existing !== void 0) return existing;
4020
+ const snapshot = {
4021
+ key,
4022
+ revision: void 0,
4023
+ moves: 0,
4024
+ findSeq: 0,
4025
+ finding: false,
4026
+ channels: /* @__PURE__ */ new Set(),
4027
+ unwatch: () => {
4028
+ }
4029
+ };
4030
+ this.snapshots.set(key, snapshot);
4031
+ if (this.api !== null) {
4032
+ snapshot.unwatch = this.api.watchRevision(workspace, path2, (hash2) => this.moveTo(workspace, path2, snapshot, hash2));
4033
+ }
4034
+ return snapshot;
4035
+ }
4036
+ /** Whether a snapshot is still the runtime's, rather than one a clear or a
4037
+ * re-initialization dropped. */
4038
+ isLive(snapshot) {
4039
+ return this.snapshots.get(snapshot.key) === snapshot;
4040
+ }
4041
+ /** Whether a live snapshot is still pinned to `revision`. */
4042
+ pinnedTo(snapshot, revision) {
4043
+ return this.isLive(snapshot) && snapshot.revision === revision;
4044
+ }
4045
+ /**
4046
+ * The revision a read of `path` is pinned to — a hash, `null` while the
4047
+ * dataset has no value, or `undefined` while it is being found — tracked so
4048
+ * the read re-fires when the source moves.
4049
+ *
4050
+ * @throws The failed lookup's reason, until a read after the retry gap
4051
+ * looks again
4052
+ */
4053
+ revisionFor(workspace, path2) {
4054
+ this.track(pagedRevisionKey(workspace, path2));
4055
+ const snapshot = this.snapshotOf(workspace, path2);
4056
+ if (snapshot.revision !== void 0 || snapshot.finding) return snapshot.revision;
4057
+ if (snapshot.findError !== void 0 && this.now() - (snapshot.findFailedAtMs ?? 0) < RETRY_AFTER_MS) {
4058
+ throw new Error(snapshot.findError);
4059
+ }
4060
+ this.find(workspace, path2, snapshot);
4061
+ return void 0;
4062
+ }
4063
+ /**
4064
+ * Ask the server which content the dataset holds, and move there — unless
4065
+ * a move lands first, which came from something no older than the answer.
4066
+ */
4067
+ find(workspace, path2, snapshot) {
4068
+ const seq = ++snapshot.findSeq;
4069
+ const moves = snapshot.moves;
4070
+ snapshot.finding = true;
4071
+ const api3 = this.api;
4072
+ void (async () => {
4073
+ let revision;
4074
+ try {
4075
+ if (api3 === null) throw new Error("no PagedApi installed");
4076
+ revision = await api3.getRevision(workspace, path2);
4077
+ } catch (err) {
4078
+ if (!this.isLive(snapshot) || snapshot.findSeq !== seq) return;
4079
+ snapshot.finding = false;
4080
+ if (snapshot.revision !== void 0) return;
4081
+ snapshot.findError = failureOf(path2, err);
4082
+ snapshot.findFailedAtMs = this.now();
4083
+ console.error(`Data.bindPaged: could not find which content ${datasetPathToString(path2)} holds:`, err);
4084
+ this.notify(pagedRevisionKey(workspace, path2));
4085
+ return;
4086
+ }
4087
+ if (!this.isLive(snapshot) || snapshot.findSeq !== seq) return;
4088
+ snapshot.finding = false;
4089
+ if (snapshot.moves !== moves) return;
4090
+ this.moveTo(workspace, path2, snapshot, revision);
4091
+ })();
4092
+ }
4093
+ /**
4094
+ * Move a dataset's source to `revision`: drop what the old snapshot
4095
+ * delivered and re-fire every read of the dataset, so the windows and
4096
+ * searches still in use are fetched again, pinned to the new one. A move to
4097
+ * the revision the source already has changes nothing.
4098
+ */
4099
+ moveTo(workspace, path2, snapshot, revision) {
4100
+ if (!this.isLive(snapshot) || snapshot.revision === revision) return;
4101
+ snapshot.revision = revision;
4102
+ snapshot.moves += 1;
4103
+ delete snapshot.findError;
4104
+ delete snapshot.findFailedAtMs;
4105
+ for (const key of snapshot.channels) {
4106
+ this.entries.delete(key);
4107
+ this.loadedWindows.delete(key);
4108
+ }
4109
+ snapshot.channels.clear();
4110
+ this.notify(pagedRevisionKey(workspace, path2));
4111
+ }
4112
+ /**
4113
+ * Handle what a pinned fetch threw when it is not the read's failure: a
4114
+ * refusal naming other content moves the source there, and "no value"
4115
+ * moves it to no snapshot. Returns whether the error was one of those, or
4116
+ * arrived after the source had already moved on.
4117
+ */
4118
+ followed(workspace, path2, snapshot, revision, err) {
4119
+ if (!this.pinnedTo(snapshot, revision)) return true;
4120
+ if (err instanceof e3ApiClient.DatasetHashMismatchError) {
4121
+ if (err.currentHash !== null && err.currentHash !== revision) {
4122
+ this.moveTo(workspace, path2, snapshot, err.currentHash);
4123
+ return true;
4124
+ }
4125
+ this.find(workspace, path2, snapshot);
4126
+ return false;
4127
+ }
4128
+ const code = errorCode(err);
4129
+ if (code !== void 0 && NO_VALUE_ERRORS.has(code)) {
4130
+ this.moveTo(workspace, path2, snapshot, null);
4131
+ return true;
4132
+ }
4133
+ return false;
4134
+ }
4135
+ // ----- channels ----------------------------------------------------------
4136
+ /** A channel of the snapshot's current revision, dropped when it moves. */
4137
+ channel(snapshot, key) {
4138
+ snapshot.channels.add(key);
4139
+ return this.entry(key);
4140
+ }
4141
+ /** Whether a channel is due a fetch: never fetched, evicted, or failed
4142
+ * for a reason a retry can fix, longer ago than the retry gap. */
4143
+ due(entry) {
4144
+ if (entry.status === "idle") return true;
4145
+ if (entry.status !== "failed" || entry.permanent === true) return false;
4146
+ return this.now() - (entry.failedAtMs ?? 0) >= RETRY_AFTER_MS;
4147
+ }
4148
+ /**
4149
+ * Mark a channel's fetch as started, returning the fetch's sequence number.
4150
+ *
4151
+ * Deliberately does NOT notify — the read that triggers it runs inside a
4152
+ * render pass, and notifying there would re-enter the renderer. Only the
4153
+ * settle notifies.
4154
+ */
4155
+ launch(entry) {
4156
+ entry.status = "running";
4157
+ entry.launchSeq = ++this.launches;
4158
+ return entry.launchSeq;
4159
+ }
4160
+ /** Apply a fetch's outcome if the channel still waits on it, re-firing the
4161
+ * channel's reads. Returns whether it applied. */
4162
+ settle(key, seq, mutate) {
4163
+ const entry = this.entries.get(key);
4164
+ if (entry === void 0 || entry.launchSeq !== seq) return false;
4165
+ mutate(entry);
4166
+ this.notify(key);
4167
+ return true;
4168
+ }
4169
+ /** Settle a fetch as failed, with the reason a read will throw. */
4170
+ fail(key, seq, reason, permanent) {
4171
+ this.settle(key, seq, (e3) => {
4172
+ e3.status = "failed";
4173
+ e3.error = reason;
4174
+ e3.failedAtMs = this.now();
4175
+ e3.permanent = permanent;
4176
+ });
4177
+ }
4178
+ /** What a read of a channel answers: its value once loaded, `none` while
4179
+ * in flight, or its failure, thrown. */
4180
+ answer(entry, value) {
4181
+ if (entry.status === "loaded" && value !== void 0) return east.some(value);
4182
+ if (entry.status === "failed") throw new Error(entry.error ?? "Data.bindPaged: the read failed");
4183
+ return east.none;
4184
+ }
3809
4185
  /** Note a window as most-recently-read, and drop the coldest decoded
3810
4186
  * windows once the cache exceeds its cap. An evicted window returns to
3811
4187
  * `idle`, so the next read refetches it rather than reading a hole. */
@@ -3823,131 +4199,83 @@ class PagedRuntime extends TrackedChannelStore {
3823
4199
  delete entry.window;
3824
4200
  }
3825
4201
  }
3826
- /**
3827
- * Start the fetch for a window if it isn't loaded or already in flight.
3828
- *
3829
- * Deliberately does NOT notify on launch — the read that triggers it runs
3830
- * inside a render pass, and notifying there would re-enter the renderer.
3831
- * Only the settle notifies.
3832
- */
3833
- ensureWindow(sourceType, workspace, path2, offset, limit2) {
3834
- const key = pagedWindowKey(workspace, path2, offset, limit2);
3835
- const entry = this.entry(key);
3836
- if (entry.status === "running" || entry.status === "loaded") return;
3837
- if (entry.status === "failed") {
3838
- if (entry.permanent) return;
3839
- const since = this.now() - (entry.failedAtMs ?? 0);
3840
- if (since < RETRY_AFTER_MS) return;
3841
- }
3842
- entry.status = "running";
3843
- entry.launchSeq += 1;
3844
- const mySeq = entry.launchSeq;
4202
+ // ----- window loading --------------------------------------------------
4203
+ /** Start the pinned fetch for a window if it is due one, returning its
4204
+ * channel. */
4205
+ ensureWindow(sourceType, workspace, path2, snapshot, revision, offset, limit2, selector2, key) {
4206
+ const entry = this.channel(snapshot, key);
4207
+ if (!this.due(entry)) return entry;
4208
+ const seq = this.launch(entry);
3845
4209
  const api3 = this.api;
3846
4210
  void (async () => {
3847
- const settle = (mutate) => {
3848
- const current = this.entries.get(key);
3849
- if (!current || current.launchSeq !== mySeq) return;
3850
- mutate(current);
3851
- this.notify(key);
3852
- };
3853
- if (!api3) {
3854
- settle((e3) => {
3855
- e3.status = "failed";
3856
- e3.failedAtMs = this.now();
3857
- });
3858
- console.error("Data.bindPaged: no PagedApi installed");
3859
- return;
3860
- }
3861
4211
  let page;
3862
4212
  try {
3863
- page = await api3.getPage(workspace, path2, { offset, limit: limit2 });
3864
- } catch (err) {
3865
- const permanent = isPermanentPageError(err);
3866
- settle((e3) => {
3867
- e3.status = "failed";
3868
- e3.failedAtMs = this.now();
3869
- e3.permanent = permanent;
4213
+ if (api3 === null) throw new Error("no PagedApi installed");
4214
+ page = await api3.getPage(workspace, path2, {
4215
+ offset,
4216
+ limit: limit2,
4217
+ hash: revision,
4218
+ ...selector2.index !== null && { index: selector2.index, join: selector2.join }
3870
4219
  });
3871
- console.error(
3872
- permanent ? `Data.bindPaged: ${datasetPathToString(path2)} is not a pageable dataset — bind a collection (Array / Set / Dict), or use Data.bind for a whole value:` : `Data.bindPaged: fetch failed for ${key}:`,
3873
- err
3874
- );
4220
+ } catch (err) {
4221
+ if (this.followed(workspace, path2, snapshot, revision, err)) return;
4222
+ this.fail(key, seq, failureOf(path2, err), isPermanentPageError(err));
4223
+ console.error(`Data.bindPaged: fetch failed for ${key}:`, err);
3875
4224
  return;
3876
4225
  }
4226
+ if (!this.pinnedTo(snapshot, revision)) return;
3877
4227
  let decoded;
3878
4228
  try {
3879
4229
  decoded = east.decodeBeast2For(sourceType)(page.data);
3880
4230
  } catch (err) {
3881
- settle((e3) => {
3882
- e3.status = "failed";
3883
- e3.failedAtMs = this.now();
3884
- });
4231
+ this.fail(key, seq, `${failureOf(path2, err)} (the window does not decode as the source's type)`, true);
3885
4232
  console.error(`Data.bindPaged: decode failed for ${key}:`, err);
3886
4233
  return;
3887
4234
  }
3888
- settle((e3) => {
4235
+ const landed = this.settle(key, seq, (e3) => {
3889
4236
  e3.status = "loaded";
3890
4237
  e3.window = decoded;
3891
4238
  e3.total = page.totalElements;
4239
+ delete e3.error;
3892
4240
  });
4241
+ if (!landed) return;
3893
4242
  this.touchWindow(key);
3894
- const totalKey = pagedTotalKey(workspace, path2);
3895
- const totalEntry = this.entry(totalKey);
4243
+ const totalKey = pagedTotalKey(workspace, path2, revision, selector2);
4244
+ const totalEntry = this.channel(snapshot, totalKey);
3896
4245
  if (totalEntry.total !== page.totalElements) {
3897
4246
  totalEntry.total = page.totalElements;
3898
4247
  totalEntry.status = "loaded";
3899
4248
  this.notify(totalKey);
3900
4249
  }
3901
4250
  })();
4251
+ return entry;
3902
4252
  }
3903
- /**
3904
- * Start the fence search for one key query if it isn't answered or already
3905
- * in flight — the seek sibling of {@link ensureWindow}, with the same
3906
- * launch-does-not-notify rule (the read runs inside a render pass).
3907
- */
3908
- ensureSeek(workspace, path2, query, key) {
3909
- const entry = this.entry(key);
3910
- if (entry.status === "running" || entry.status === "loaded") return;
3911
- if (entry.status === "failed") {
3912
- if (entry.permanent) return;
3913
- const since = this.now() - (entry.failedAtMs ?? 0);
3914
- if (since < RETRY_AFTER_MS) return;
3915
- }
3916
- entry.status = "running";
3917
- entry.launchSeq += 1;
3918
- const mySeq = entry.launchSeq;
4253
+ /** Start the pinned fence search for one key query if it is due one,
4254
+ * returning its channel — the seek sibling of {@link ensureWindow}. */
4255
+ ensureSeek(workspace, path2, snapshot, revision, query, key) {
4256
+ const entry = this.channel(snapshot, key);
4257
+ if (!this.due(entry)) return entry;
4258
+ const seq = this.launch(entry);
3919
4259
  const api3 = this.api;
3920
4260
  void (async () => {
3921
- const settle = (mutate) => {
3922
- const current = this.entries.get(key);
3923
- if (!current || current.launchSeq !== mySeq) return;
3924
- mutate(current);
3925
- this.notify(key);
3926
- };
3927
- if (!api3) {
3928
- settle((e3) => {
3929
- e3.status = "failed";
3930
- e3.failedAtMs = this.now();
3931
- });
3932
- console.error("Data.bindPaged: no PagedApi installed");
3933
- return;
3934
- }
4261
+ let range;
3935
4262
  try {
3936
- const range = await api3.findKey(workspace, path2, query);
3937
- settle((e3) => {
3938
- e3.status = "loaded";
3939
- e3.range = range;
3940
- });
4263
+ if (api3 === null) throw new Error("no PagedApi installed");
4264
+ range = await api3.findKey(workspace, path2, { ...query, hash: revision });
3941
4265
  } catch (err) {
3942
- const permanent = isPermanentPageError(err);
3943
- settle((e3) => {
3944
- e3.status = "failed";
3945
- e3.failedAtMs = this.now();
3946
- e3.permanent = permanent;
3947
- });
4266
+ if (this.followed(workspace, path2, snapshot, revision, err)) return;
4267
+ this.fail(key, seq, failureOf(path2, err), isPermanentPageError(err));
3948
4268
  console.error(`Data.bindPaged: key search failed for ${key}:`, err);
4269
+ return;
3949
4270
  }
4271
+ if (!this.pinnedTo(snapshot, revision)) return;
4272
+ this.settle(key, seq, (e3) => {
4273
+ e3.status = "loaded";
4274
+ e3.range = range;
4275
+ delete e3.error;
4276
+ });
3950
4277
  })();
4278
+ return entry;
3951
4279
  }
3952
4280
  /**
3953
4281
  * The low-level primitives backing handle methods, bound to THIS runtime.
@@ -3957,53 +4285,92 @@ class PagedRuntime extends TrackedChannelStore {
3957
4285
  */
3958
4286
  buildPrimitives() {
3959
4287
  return [
3960
- internal$1.DataPagedPrimitives.page.implement((sourceType) => (pathArg, offsetArg, limitArg) => {
4288
+ internal$1.DataPagedPrimitives.page.implement((sourceType) => (pathArg, indexArg, joinArg, offsetArg, limitArg) => {
3961
4289
  const workspace = this.resolveWorkspace();
3962
4290
  const path2 = pathArg;
4291
+ const revision = this.revisionFor(workspace, path2);
4292
+ if (typeof revision !== "string") return east.none;
4293
+ const selector2 = toSelector(indexArg, joinArg);
3963
4294
  const offset = Number(offsetArg);
3964
4295
  const limit2 = Number(limitArg);
3965
- const key = pagedWindowKey(workspace, path2, offset, limit2);
4296
+ const key = pagedWindowKey(workspace, path2, revision, offset, limit2, selector2);
3966
4297
  this.track(key);
3967
- this.ensureWindow(sourceType, workspace, path2, offset, limit2);
3968
- const entry = this.entry(key);
3969
- if (entry.status === "loaded" && entry.window !== void 0) {
3970
- this.touchWindow(key);
3971
- return east.variant("some", entry.window);
3972
- }
3973
- return east.variant("none", null);
4298
+ const entry = this.ensureWindow(
4299
+ sourceType,
4300
+ workspace,
4301
+ path2,
4302
+ this.snapshotOf(workspace, path2),
4303
+ revision,
4304
+ offset,
4305
+ limit2,
4306
+ selector2,
4307
+ key
4308
+ );
4309
+ if (entry.status === "loaded" && entry.window !== void 0) this.touchWindow(key);
4310
+ return this.answer(entry, entry.window);
3974
4311
  }),
3975
- internal$1.DataPagedPrimitives.total.implement((_sourceType) => (pathArg) => {
4312
+ internal$1.DataPagedPrimitives.total.implement((_sourceType) => (pathArg, indexArg, joinArg) => {
3976
4313
  const workspace = this.resolveWorkspace();
3977
- const key = pagedTotalKey(workspace, pathArg);
4314
+ const path2 = pathArg;
4315
+ const revision = this.revisionFor(workspace, path2);
4316
+ if (typeof revision !== "string") return east.none;
4317
+ const key = pagedTotalKey(workspace, path2, revision, toSelector(indexArg, joinArg));
3978
4318
  this.track(key);
3979
- const entry = this.entry(key);
3980
- return entry.total !== void 0 ? east.variant("some", BigInt(entry.total)) : east.variant("none", null);
4319
+ const entry = this.channel(this.snapshotOf(workspace, path2), key);
4320
+ return entry.total !== void 0 ? east.some(BigInt(entry.total)) : east.none;
3981
4321
  }),
3982
- internal$1.DataPagedPrimitives.seek.implement((_sourceType) => (pathArg, queryArg) => {
4322
+ internal$1.DataPagedPrimitives.seek.implement((_sourceType) => (pathArg, indexArg, joinArg, queryArg) => {
3983
4323
  const workspace = this.resolveWorkspace();
3984
4324
  const path2 = pathArg;
3985
- const query = toFindQuery(queryArg);
3986
- const key = pagedSeekKey(workspace, path2, query);
4325
+ const revision = this.revisionFor(workspace, path2);
4326
+ if (typeof revision !== "string") return east.none;
4327
+ const selector2 = toSelector(indexArg, joinArg);
4328
+ const query = toFindQuery(queryArg, selector2);
4329
+ const key = pagedSeekKey(workspace, path2, revision, query, selector2);
3987
4330
  this.track(key);
3988
- this.ensureSeek(workspace, path2, query, key);
3989
- const entry = this.entry(key);
3990
- if (entry.status !== "loaded" || entry.range === void 0) return east.none;
3991
- const r2 = entry.range;
3992
- return east.some({ found: r2.found, row: BigInt(r2.row), count: BigInt(r2.count) });
4331
+ const entry = this.ensureSeek(workspace, path2, this.snapshotOf(workspace, path2), revision, query, key);
4332
+ const range = entry.range;
4333
+ return this.answer(entry, range === void 0 ? void 0 : { found: range.found, row: BigInt(range.row), count: BigInt(range.count) });
4334
+ }),
4335
+ internal$1.DataPagedPrimitives.revision.implement((_sourceType) => (pathArg, _indexArg, _joinArg) => {
4336
+ const revision = this.revisionFor(this.resolveWorkspace(), pathArg);
4337
+ return typeof revision === "string" ? east.some(revision) : east.none;
4338
+ }),
4339
+ internal$1.DataPagedPrimitives.refresh.implement((_sourceType) => (pathArg, _indexArg, _joinArg, targetArg) => {
4340
+ const workspace = this.resolveWorkspace();
4341
+ const path2 = pathArg;
4342
+ const target = targetArg;
4343
+ queueMicrotask(() => {
4344
+ if (this.workspace !== workspace) return;
4345
+ const snapshot = this.snapshotOf(workspace, path2);
4346
+ if (target.type === "some") this.moveTo(workspace, path2, snapshot, target.value);
4347
+ else this.find(workspace, path2, snapshot);
4348
+ });
4349
+ return null;
3993
4350
  })
3994
4351
  ];
3995
4352
  }
3996
4353
  /**
3997
- * Build the handle value for one `Data.bindPaged` evaluation. Both methods
3998
- * are thin IR-bearing `East.function`s over {@link buildPrimitives},
3999
- * capturing only the plain-data source path (the value type rides as a
4000
- * type-arg) — so the handle is ordinary serializable East data (issue #106).
4354
+ * Build the handle value for one paged bind. Every method is a thin
4355
+ * IR-bearing `East.function` over {@link buildPrimitives}, capturing only
4356
+ * the plain-data source path and selector (the value type rides as a
4357
+ * type-arg) — so the handle is ordinary serializable East data (issue
4358
+ * #106).
4359
+ *
4360
+ * @param sourceType - The dataset's type, or an index's window type
4361
+ * @param path - The dataset's path
4362
+ * @param selector - The rows the bind serves
4363
+ * @param shape - `released`: the handle `data_bind_paged` returns, without
4364
+ * `revision` or `refresh`; `pinned`: the handle `data_bind_paged_pinned`
4365
+ * returns. Both read through the same snapshot.
4366
+ * @returns The handle
4001
4367
  */
4002
- buildHandle(sourceType, path2) {
4003
- const pathKey = datasetPathToString(path2);
4368
+ buildHandle(sourceType, path2, selector2, shape) {
4369
+ const id3 = `${datasetPathToString(path2)}${selectorKey(selector2)}`;
4370
+ const cacheKey = `${id3}#${shape}`;
4004
4371
  let byPath = this.handleCache.get(sourceType);
4005
4372
  if (byPath) {
4006
- const hit = byPath.get(pathKey);
4373
+ const hit = byPath.get(cacheKey);
4007
4374
  if (hit) return hit;
4008
4375
  } else {
4009
4376
  byPath = /* @__PURE__ */ new Map();
@@ -4011,24 +4378,27 @@ class PagedRuntime extends TrackedChannelStore {
4011
4378
  }
4012
4379
  const T2 = east.fromEastTypeValue(sourceType);
4013
4380
  const pathExpr = east.East.value(path2, e3Types.TreePathType);
4381
+ const indexExpr = east.East.value(selector2.index === null ? east.none : east.some(selector2.index), east.OptionType(east.StringType));
4382
+ const joinExpr = east.East.value(selector2.join, east.BooleanType);
4014
4383
  const platform2 = this.buildPrimitives();
4015
- const { page, total } = internal$1.DataPagedPrimitives;
4384
+ const { page, total, revision, refresh } = internal$1.DataPagedPrimitives;
4016
4385
  const handle = {
4017
4386
  // The comparable identity east-ui's `PagedSourceType` requires:
4018
4387
  // East compares every function as EQUAL, so a struct of nothing but
4019
4388
  // closures is indistinguishable from any other and a memoized
4020
4389
  // component would never re-render on a source swap (#567 D4). The
4021
- // dataset path is the natural identity — same path, same rows.
4022
- id: pathKey,
4390
+ // dataset path and selector are the natural identity — same path,
4391
+ // same rows. The snapshot they serve is `revision`'s, not the id's.
4392
+ id: id3,
4023
4393
  page: east.East.compile(
4024
4394
  east.East.function([east.IntegerType, east.IntegerType], east.OptionType(T2), ($3, offset, limit2) => {
4025
- $3.return(page([T2], pathExpr, offset, limit2));
4395
+ $3.return(page([T2], pathExpr, indexExpr, joinExpr, offset, limit2));
4026
4396
  }),
4027
4397
  platform2
4028
4398
  ),
4029
4399
  total: east.East.compile(
4030
4400
  east.East.function([], east.OptionType(east.IntegerType), ($3) => {
4031
- $3.return(total([T2], pathExpr));
4401
+ $3.return(total([T2], pathExpr, indexExpr, joinExpr));
4032
4402
  }),
4033
4403
  platform2
4034
4404
  ),
@@ -4037,28 +4407,50 @@ class PagedRuntime extends TrackedChannelStore {
4037
4407
  // segments) against the stored fences; an Array's stream order has
4038
4408
  // nothing to search. Resolved at bind time from the dataset's own
4039
4409
  // type, so a component renders the affordance only when it works.
4040
- seek: buildSeek(sourceType, T2, pathExpr, platform2)
4410
+ seek: buildSeek(sourceType, T2, pathExpr, indexExpr, joinExpr, selector2, platform2)
4041
4411
  };
4042
- byPath.set(pathKey, handle);
4412
+ if (shape === "pinned") {
4413
+ handle.revision = east.East.compile(
4414
+ east.East.function([], east.OptionType(east.StringType), ($3) => {
4415
+ $3.return(revision([T2], pathExpr, indexExpr, joinExpr));
4416
+ }),
4417
+ platform2
4418
+ );
4419
+ handle.refresh = east.East.compile(
4420
+ east.East.function([east.OptionType(east.StringType)], east.NullType, ($3, target) => {
4421
+ $3.return(refresh([T2], pathExpr, indexExpr, joinExpr, target));
4422
+ }),
4423
+ platform2
4424
+ );
4425
+ }
4426
+ byPath.set(cacheKey, handle);
4043
4427
  return handle;
4044
4428
  }
4045
4429
  // ----- platform building -------------------------------------------------
4046
- /** Build a `Data.bindPaged` PlatformFunction bound to this runtime. Pass
4047
- * `allowed=null` for an unscoped impl; pass a Set of path strings for
4048
- * manifest scoping. */
4430
+ /** Build the PlatformFunction behind `data_bind_paged` — the bind a UI
4431
+ * exported before pinned reads calls — bound to this runtime. Its handle
4432
+ * has no `revision` or `refresh`, but its reads are pinned, follow the
4433
+ * dataset and fail visibly all the same. Pass `allowed=null` for an
4434
+ * unscoped impl, or a Set of path strings for manifest scoping. */
4049
4435
  buildPlatform(allowed) {
4050
4436
  return internal$1.bindPagedPlatformFn.implement(
4051
4437
  (sourceType) => (pathArg) => {
4052
4438
  const path2 = pathArg;
4053
- if (allowed) {
4054
- const pathStr = datasetPathToString(path2);
4055
- if (!allowed.has(pathStr)) {
4056
- throw new Error(
4057
- `Data.bindPaged: source path "${pathStr}" not declared in manifest — bind it in the task body so derivation records it`
4058
- );
4059
- }
4060
- }
4061
- return this.buildHandle(sourceType, path2);
4439
+ checkDeclared(allowed, path2);
4440
+ return this.buildHandle(sourceType, path2, NO_INDEX, "released");
4441
+ }
4442
+ );
4443
+ }
4444
+ /** Build the PlatformFunction behind `data_bind_paged_pinned` — what
4445
+ * `Data.bindPaged` emits, for a dataset's own rows or a record's index —
4446
+ * bound to this runtime and scoped by `allowed` exactly as
4447
+ * {@link buildPlatform} is: an index read is a read of the record's path. */
4448
+ buildPinnedPlatform(allowed) {
4449
+ return internal$1.bindPagedPinnedPlatformFn.implement(
4450
+ (sourceType) => (pathArg, indexArg, joinArg) => {
4451
+ const path2 = pathArg;
4452
+ checkDeclared(allowed, path2);
4453
+ return this.buildHandle(sourceType, path2, toSelector(indexArg, joinArg), "pinned");
4062
4454
  }
4063
4455
  );
4064
4456
  }
@@ -4067,17 +4459,19 @@ const defaultPagedRuntime = new PagedRuntime();
4067
4459
  function initializePagedApi(api3, workspace) {
4068
4460
  defaultPagedRuntime.initialize(api3, workspace);
4069
4461
  }
4070
- function clearPagedApi() {
4071
- defaultPagedRuntime.clear();
4462
+ function clearPagedApi(api3) {
4463
+ defaultPagedRuntime.clear(api3);
4072
4464
  }
4073
4465
  const PagedPlatform = [
4074
4466
  defaultPagedRuntime.buildPlatform(null),
4467
+ defaultPagedRuntime.buildPinnedPlatform(null),
4075
4468
  ...defaultPagedRuntime.buildPrimitives()
4076
4469
  ];
4077
4470
  function createScopedPagedPlatform(pages) {
4078
4471
  const allowed = new Set(pages.map((p2) => datasetPathToString(p2)));
4079
4472
  return [
4080
4473
  defaultPagedRuntime.buildPlatform(allowed),
4474
+ defaultPagedRuntime.buildPinnedPlatform(allowed),
4081
4475
  ...defaultPagedRuntime.buildPrimitives()
4082
4476
  ];
4083
4477
  }
@@ -4203,8 +4597,15 @@ class FuncRuntime extends TrackedChannelStore {
4203
4597
  this.api = api3;
4204
4598
  this.workspace = workspace;
4205
4599
  }
4206
- /** Tear down the adapter and all call state. */
4207
- clear() {
4600
+ /**
4601
+ * Tear down the adapter and all call state.
4602
+ *
4603
+ * @param api - Clear only while this is the installed adapter, so a
4604
+ * provider tearing down after another installed its own leaves that one
4605
+ * in place
4606
+ */
4607
+ clear(api3) {
4608
+ if (api3 !== void 0 && this.api !== api3) return;
4208
4609
  this.api = null;
4209
4610
  this.workspace = null;
4210
4611
  this.clearChannels();
@@ -4437,8 +4838,8 @@ const defaultFuncRuntime = new FuncRuntime();
4437
4838
  function initializeFunctionApi(api3, workspace) {
4438
4839
  defaultFuncRuntime.initialize(api3, workspace);
4439
4840
  }
4440
- function clearFunctionApi() {
4441
- defaultFuncRuntime.clear();
4841
+ function clearFunctionApi(api3) {
4842
+ defaultFuncRuntime.clear(api3);
4442
4843
  }
4443
4844
  const FuncPlatform = [
4444
4845
  defaultFuncRuntime.buildPlatform(null),
@@ -8211,12 +8612,6 @@ function errorOfMutationResult(result) {
8211
8612
  message: `reducer exited with code ${outcome.value.exitCode}`,
8212
8613
  stderr: outcome.value.stderr
8213
8614
  };
8214
- case "too_large":
8215
- return {
8216
- kind: east.variant("too_large", { bytes: outcome.value.bytes, limit: outcome.value.limit }),
8217
- message: `new state too large (${outcome.value.bytes} bytes, limit ${outcome.value.limit})`,
8218
- stderr: outcome.value.stderr
8219
- };
8220
8615
  case "timed_out":
8221
8616
  return {
8222
8617
  kind: east.variant("timed_out", { ms: outcome.value.ms }),
@@ -8276,8 +8671,15 @@ class RecordRuntime extends TrackedChannelStore {
8276
8671
  this.cache = cache3;
8277
8672
  this.workspace = workspace;
8278
8673
  }
8279
- /** Tear down the adapter and all record state. */
8280
- clear() {
8674
+ /**
8675
+ * Tear down the adapter and all record state.
8676
+ *
8677
+ * @param api - Clear only while this is the installed adapter, so a
8678
+ * provider tearing down after another installed its own leaves that one
8679
+ * in place
8680
+ */
8681
+ clear(api3) {
8682
+ if (api3 !== void 0 && this.api !== api3) return;
8281
8683
  this.api = null;
8282
8684
  this.cache = null;
8283
8685
  this.workspace = null;
@@ -8585,8 +8987,8 @@ const defaultRecordRuntime = new RecordRuntime();
8585
8987
  function initializeRecordApi(api3, cache3, workspace) {
8586
8988
  defaultRecordRuntime.initialize(api3, cache3, workspace);
8587
8989
  }
8588
- function clearRecordApi() {
8589
- defaultRecordRuntime.clear();
8990
+ function clearRecordApi(api3) {
8991
+ defaultRecordRuntime.clear(api3);
8590
8992
  }
8591
8993
  const RecordPlatform = [
8592
8994
  defaultRecordRuntime.buildPlatform(null),
@@ -8612,7 +9014,8 @@ function createInMemoryRecordApi(cache3, workspace, defs2) {
8612
9014
  state: `${def.name}-state-0`.padEnd(64, "0"),
8613
9015
  mutation: "$init",
8614
9016
  actor: "memory",
8615
- at: /* @__PURE__ */ new Date(0)
9017
+ at: /* @__PURE__ */ new Date(0),
9018
+ delta: east.none
8616
9019
  };
8617
9020
  compiled.set(def.name, { stateType, mutations, commits: [genesis], seq: 0 });
8618
9021
  void cache3.write(workspace, recordPath(def.name), east.encodeBeast2For(stateType)(def.initial));
@@ -8644,7 +9047,8 @@ function createInMemoryRecordApi(cache3, workspace, defs2) {
8644
9047
  state: `${record}-state-${c2.seq}`.padEnd(64, "0"),
8645
9048
  mutation,
8646
9049
  actor: "memory",
8647
- at: /* @__PURE__ */ new Date(0)
9050
+ at: /* @__PURE__ */ new Date(0),
9051
+ delta: east.none
8648
9052
  });
8649
9053
  return { outcome: east.variant("committed", { commitHash: hash2, stateHash: `${record}-state-${c2.seq}`.padEnd(64, "0") }) };
8650
9054
  },
@@ -8693,68 +9097,49 @@ function ReactiveDatasetProvider({
8693
9097
  children: children2
8694
9098
  }) {
8695
9099
  const e3 = useE3Config();
9100
+ const { apiUrl, workspace } = e3;
9101
+ const repo = e3.repo ?? "default";
8696
9102
  const tokenRef = React.useRef(e3.token ?? null);
8697
9103
  tokenRef.current = e3.token ?? null;
8698
- const api3 = React.useMemo(
8699
- () => createDefaultDatasetApi(e3.apiUrl, e3.repo ?? "default", () => tokenRef.current),
8700
- [e3.apiUrl, e3.repo]
8701
- );
8702
- const cache3 = React.useMemo(() => {
8703
- const cfg = {};
8704
- if (e3.workspace !== void 0) cfg.workspace = e3.workspace;
8705
- return new ReactiveDatasetCache(cfg, api3);
8706
- }, [api3, e3.workspace]);
8707
- React.useMemo(() => {
8708
- initializeReactiveDatasetCache(cache3);
8709
- cache3.setScheduler((notify3) => queueMicrotask(notify3));
8710
- }, [cache3]);
8711
- React.useMemo(() => {
8712
- if (e3.workspace !== void 0) {
8713
- initializeFunctionApi(
8714
- createDefaultFunctionApi(e3.apiUrl, e3.repo ?? "default", () => tokenRef.current),
8715
- e3.workspace
8716
- );
8717
- }
8718
- }, [e3.apiUrl, e3.repo, e3.workspace]);
8719
- React.useMemo(() => {
8720
- if (e3.workspace !== void 0) {
8721
- initializePagedApi(
8722
- createDefaultPagedApi(e3.apiUrl, e3.repo ?? "default", () => tokenRef.current),
8723
- e3.workspace
8724
- );
8725
- }
8726
- }, [e3.apiUrl, e3.repo, e3.workspace]);
8727
- React.useMemo(() => {
8728
- if (e3.workspace !== void 0) {
8729
- initializeRecordApi(
8730
- createDefaultRecordApi(e3.apiUrl, e3.repo ?? "default", () => tokenRef.current),
8731
- cache3,
8732
- e3.workspace
8733
- );
8734
- }
8735
- }, [e3.apiUrl, e3.repo, e3.workspace, cache3]);
9104
+ const [installed, setInstalled] = React.useState(null);
8736
9105
  React.useEffect(() => {
9106
+ const getToken = () => tokenRef.current;
9107
+ const cache22 = new ReactiveDatasetCache(
9108
+ workspace !== void 0 ? { workspace } : {},
9109
+ createDefaultDatasetApi(apiUrl, repo, getToken)
9110
+ );
9111
+ cache22.setScheduler((notify3) => queueMicrotask(notify3));
9112
+ initializeReactiveDatasetCache(cache22);
9113
+ const functionApi = createDefaultFunctionApi(apiUrl, repo, getToken);
9114
+ const pagedApi = createDefaultPagedApi(apiUrl, repo, getToken, cache22);
9115
+ const recordApi = createDefaultRecordApi(apiUrl, repo, getToken);
9116
+ if (workspace !== void 0) {
9117
+ initializeFunctionApi(functionApi, workspace);
9118
+ initializePagedApi(pagedApi, workspace);
9119
+ initializeRecordApi(recordApi, cache22, workspace);
9120
+ }
9121
+ const unscopeStaged = getStagedStore().setScope(JSON.stringify([apiUrl, repo]));
9122
+ setInstalled({ apiUrl, repo, workspace, cache: cache22 });
8737
9123
  return () => {
8738
- const ws = cache3.getConfig().workspace;
8739
- clearReactiveDatasetCache();
8740
- clearFunctionApi();
8741
- clearPagedApi();
8742
- clearRecordApi();
8743
- cache3.destroy();
8744
- if (ws) clearBindingRegistry(ws);
9124
+ clearReactiveDatasetCache(cache22);
9125
+ clearFunctionApi(functionApi);
9126
+ clearPagedApi(pagedApi);
9127
+ clearRecordApi(recordApi);
9128
+ unscopeStaged();
9129
+ cache22.destroy();
9130
+ if (workspace) clearBindingRegistry(workspace);
8745
9131
  else clearBindingRegistry();
8746
9132
  };
8747
- }, [cache3]);
9133
+ }, [apiUrl, repo, workspace]);
9134
+ const cache3 = installed !== null && installed.apiUrl === apiUrl && installed.repo === repo && installed.workspace === workspace ? installed.cache : null;
8748
9135
  React.useEffect(() => {
8749
- if (typeof window !== "undefined") {
8750
- window.__EAST_REACTIVE_DATASET_CACHE__ = cache3;
8751
- }
9136
+ if (cache3 === null || typeof window === "undefined") return;
9137
+ window.__EAST_REACTIVE_DATASET_CACHE__ = cache3;
8752
9138
  return () => {
8753
- if (typeof window !== "undefined") {
8754
- delete window.__EAST_REACTIVE_DATASET_CACHE__;
8755
- }
9139
+ delete window.__EAST_REACTIVE_DATASET_CACHE__;
8756
9140
  };
8757
9141
  }, [cache3]);
9142
+ if (cache3 === null) return null;
8758
9143
  return /* @__PURE__ */ jsxRuntime.jsx(ReactiveDatasetCacheContext.Provider, { value: cache3, children: children2 });
8759
9144
  }
8760
9145
  function useReactiveDatasetCache() {
@@ -64206,6 +64591,7 @@ function useDatasetValue(apiUrl, repo, workspace, datasetPath, options2) {
64206
64591
  const { requestOptions, queryOverrides, enabled = true, hash: hash2, platforms, type } = options2;
64207
64592
  const platformImpls = platforms ?? defaultPlatformImplementations;
64208
64593
  const reqOpts = requestOptions ?? { token: null };
64594
+ const queryClient = reactQuery.useQueryClient();
64209
64595
  const pathParts = React.useMemo(
64210
64596
  () => (datasetPath == null ? void 0 : datasetPath.split(".").filter(Boolean).map((v3) => east.variant("field", v3))) ?? [],
64211
64597
  [datasetPath]
@@ -64215,8 +64601,14 @@ function useDatasetValue(apiUrl, repo, workspace, datasetPath, options2) {
64215
64601
  queryFn: async () => {
64216
64602
  const result = await e3ApiClient.datasetGet(apiUrl, repo, workspace, pathParts, reqOpts);
64217
64603
  const decoded = east.decodeBeast2For(type, { platform: platformImpls })(result.data);
64218
- return { decoded, sizeBytes: result.data.length };
64604
+ const value = { decoded, sizeBytes: result.data.length, hash: result.hash ?? hash2 ?? null };
64605
+ if (value.hash !== hash2) {
64606
+ queryClient.setQueryData(["datasetValue", apiUrl, repo, workspace, datasetPath, value.hash], value);
64607
+ void queryClient.invalidateQueries({ queryKey: ["datasetStatus", apiUrl, repo, workspace, datasetPath] });
64608
+ }
64609
+ return value;
64219
64610
  },
64611
+ select: (value) => value.hash === (hash2 ?? null) ? value : void 0,
64220
64612
  enabled: enabled && !!workspace && !!datasetPath && hash2 != null,
64221
64613
  ...queryOverrides
64222
64614
  });
@@ -64282,14 +64674,6 @@ function useTaskDetails(apiUrl, repo, workspace, taskName, options2) {
64282
64674
  ...options2 == null ? void 0 : options2.queryOverrides
64283
64675
  });
64284
64676
  }
64285
- function getTaskKind(details) {
64286
- var _a2;
64287
- return ((_a2 = details.kind) == null ? void 0 : _a2.type) === "some" ? details.kind.value : null;
64288
- }
64289
- function getTaskMetadata(details) {
64290
- var _a2;
64291
- return ((_a2 = details.metadata) == null ? void 0 : _a2.type) === "some" ? details.metadata.value : null;
64292
- }
64293
64677
  const variantStyles = {
64294
64678
  // Mode-aware semantic tokens (the raw `red.50` / `gray.50` washes rendered
64295
64679
  // as bright light boxes in dark mode — #362).
@@ -64473,17 +64857,21 @@ const PagedDatasetPreview = React.memo(function PagedDatasetPreview2({
64473
64857
  const [error3, setError] = React.useState(null);
64474
64858
  const [jumpRow, setJumpRow] = React.useState(void 0);
64475
64859
  const inflightRef = React.useRef(/* @__PURE__ */ new Set());
64860
+ const shownRef = React.useRef({ path: path2, hash: hash2 });
64476
64861
  React.useEffect(() => {
64477
64862
  pagingDebug(`reset (path=${path2}, hash=${hash2.slice(0, 8)})`);
64863
+ shownRef.current = { path: path2, hash: hash2 };
64478
64864
  setPages(/* @__PURE__ */ new Map());
64479
64865
  setTotals(null);
64480
64866
  setError(null);
64481
64867
  setJumpRow(void 0);
64482
- inflightRef.current.clear();
64868
+ inflightRef.current = /* @__PURE__ */ new Set();
64483
64869
  }, [path2, hash2]);
64484
64870
  const loadPage = React.useCallback((pageIdx) => {
64485
- if (inflightRef.current.has(pageIdx)) return;
64486
- inflightRef.current.add(pageIdx);
64871
+ const inflight = inflightRef.current;
64872
+ if (inflight.has(pageIdx)) return;
64873
+ inflight.add(pageIdx);
64874
+ const current = () => shownRef.current.path === path2 && shownRef.current.hash === hash2;
64487
64875
  setLoadingCount((n2) => n2 + 1);
64488
64876
  pagingDebug(`fetch p${pageIdx} (offset=${pageIdx * PAGE_SIZE}, limit=${PAGE_SIZE}, hash=${hash2.slice(0, 8)})`);
64489
64877
  const pathParts2 = path2.split(".").filter(Boolean).map((v3) => east.variant("field", v3));
@@ -64496,6 +64884,7 @@ const PagedDatasetPreview = React.memo(function PagedDatasetPreview2({
64496
64884
  staleTime: Infinity
64497
64885
  // pages of one content hash are immutable
64498
64886
  }).then((page) => {
64887
+ if (!current()) return;
64499
64888
  const decoded = east.decodeBeast2For(type)(page.data);
64500
64889
  const rows = pageRows(type, decoded, page.offset);
64501
64890
  pagingDebug(
@@ -64509,12 +64898,17 @@ const PagedDatasetPreview = React.memo(function PagedDatasetPreview2({
64509
64898
  setPages((prev2) => new Map(prev2).set(pageIdx, rows));
64510
64899
  }).catch((err) => {
64511
64900
  pagingDebug(`p${pageIdx} FAILED:`, err);
64901
+ if (!current()) return;
64902
+ if (err instanceof e3ApiClient.DatasetHashMismatchError) {
64903
+ void queryClient.invalidateQueries({ queryKey: ["datasetStatus", apiUrl, repo, workspace, path2] });
64904
+ return;
64905
+ }
64512
64906
  if (err instanceof e3ApiClient.ApiError && err.code === "dataset_not_indexed") {
64513
64907
  onNotIndexed == null ? void 0 : onNotIndexed();
64514
64908
  }
64515
64909
  setError(err instanceof Error ? err : new Error(String(err)));
64516
64910
  }).finally(() => {
64517
- inflightRef.current.delete(pageIdx);
64911
+ inflight.delete(pageIdx);
64518
64912
  setLoadingCount((n2) => n2 - 1);
64519
64913
  });
64520
64914
  }, [queryClient, apiUrl, repo, workspace, path2, hash2, requestOptions, type, onNotIndexed]);
@@ -64611,7 +65005,10 @@ const PagedDatasetPreview = React.memo(function PagedDatasetPreview2({
64611
65005
  ] }),
64612
65006
  /* @__PURE__ */ jsxRuntime.jsx(react.Box, { flex: 1, minHeight: 0, overflow: "hidden", children: /* @__PURE__ */ jsxRuntime.jsx(eastUiComponents.EastChakraValueTree, { value: treeValue, storageKey: `${path2}:page`, paging }) })
64613
65007
  ] });
64614
- }, (prev2, next2) => prev2.path === next2.path && prev2.workspace === next2.workspace && prev2.hash === next2.hash && prev2.sizeBytes === next2.sizeBytes);
65008
+ }, (prev2, next2) => {
65009
+ var _a2, _b2;
65010
+ return prev2.apiUrl === next2.apiUrl && prev2.repo === next2.repo && prev2.workspace === next2.workspace && prev2.path === next2.path && prev2.hash === next2.hash && prev2.sizeBytes === next2.sizeBytes && ((_a2 = prev2.requestOptions) == null ? void 0 : _a2.token) === ((_b2 = next2.requestOptions) == null ? void 0 : _b2.token);
65011
+ });
64615
65012
  const DEFAULT_SIZE_LIMIT = 200 * 1024;
64616
65013
  function formatSize(bytes) {
64617
65014
  if (bytes < 1024) return `${bytes} B`;
@@ -64671,12 +65068,29 @@ const DatasetPreview = React.memo(function DatasetPreview2({
64671
65068
  const setMutation = useDatasetSet(apiUrl, repo, workspace, requestOptions);
64672
65069
  const queryClient = reactQuery.useQueryClient();
64673
65070
  const decoded = (_a2 = valueQuery.data) == null ? void 0 : _a2.decoded;
64674
- const write = React.useCallback(async (next2) => {
64675
- if (workspace == null || path2 == null || type === void 0) return;
65071
+ const edited = React.useRef(null);
65072
+ const writing = React.useRef(0);
65073
+ const writes = React.useRef(Promise.resolve());
65074
+ React.useEffect(() => {
65075
+ if (writing.current === 0) edited.current = null;
65076
+ }, [decoded]);
65077
+ const write = React.useCallback((next2) => {
65078
+ if (workspace == null || path2 == null || type === void 0) return Promise.resolve();
64676
65079
  const treePath = path2.split(".").filter(Boolean).map((p2) => east.variant("field", p2));
64677
65080
  const data4 = east.encodeBeast2For(type)(next2);
64678
- await setMutation.mutateAsync({ path: treePath, data: data4 });
64679
- await queryClient.invalidateQueries({ queryKey: ["datasetStatus", apiUrl, repo, workspace, path2] });
65081
+ edited.current = { value: next2 };
65082
+ writing.current++;
65083
+ const run = writes.current.then(async () => {
65084
+ await setMutation.mutateAsync({ path: treePath, data: data4 });
65085
+ await queryClient.invalidateQueries({ queryKey: ["datasetStatus", apiUrl, repo, workspace, path2] });
65086
+ });
65087
+ writes.current = run.catch(() => void 0);
65088
+ return run.catch((err) => {
65089
+ edited.current = null;
65090
+ throw err;
65091
+ }).finally(() => {
65092
+ writing.current--;
65093
+ });
64680
65094
  }, [apiUrl, repo, workspace, path2, type, setMutation, queryClient]);
64681
65095
  const keyType = React.useMemo(() => type === void 0 ? null : type.type === "Dict" ? type.value.key : type.type === "Set" ? type.value : null, [type]);
64682
65096
  const inlineKeys = React.useMemo(() => decoded === void 0 || type === void 0 ? null : type.type === "Dict" ? [...decoded.keys()] : type.type === "Set" ? [...decoded.values()] : null, [type, decoded]);
@@ -64712,11 +65126,12 @@ const DatasetPreview = React.memo(function DatasetPreview2({
64712
65126
  const treeValue = React.useMemo(() => {
64713
65127
  if (type === void 0 || decoded === void 0) return null;
64714
65128
  const root2 = eastUi.ValueTree.materialize(type, decoded);
65129
+ const base = () => edited.current !== null ? edited.current.value : decoded;
64715
65130
  const wire = editable ? {
64716
- onEdit: east.some((p2, leaf) => write(eastUi.ValueTree.applyEdit(type, decoded, p2, { kind: "edit", leaf }))),
64717
- onInsert: east.some((p2) => write(eastUi.ValueTree.applyEdit(type, decoded, p2, { kind: "insert" }))),
64718
- onRemove: east.some((p2) => write(eastUi.ValueTree.applyEdit(type, decoded, p2, { kind: "remove" }))),
64719
- onTag: east.some((p2, tag) => write(eastUi.ValueTree.applyEdit(type, decoded, p2, { kind: "tag", tag })))
65131
+ onEdit: east.some((p2, leaf) => write(eastUi.ValueTree.applyEdit(type, base(), p2, { kind: "edit", leaf }))),
65132
+ onInsert: east.some((p2) => write(eastUi.ValueTree.applyEdit(type, base(), p2, { kind: "insert" }))),
65133
+ onRemove: east.some((p2) => write(eastUi.ValueTree.applyEdit(type, base(), p2, { kind: "remove" }))),
65134
+ onTag: east.some((p2, tag) => write(eastUi.ValueTree.applyEdit(type, base(), p2, { kind: "tag", tag })))
64720
65135
  } : { onEdit: east.none, onInsert: east.none, onRemove: east.none, onTag: east.none };
64721
65136
  return { root: root2, ...wire, style: east.some({ height: east.some("100%"), maxHeight: east.none, openDepth: east.none, toolbar: east.some(true) }) };
64722
65137
  }, [type, decoded, editable, write]);
@@ -64758,8 +65173,11 @@ const DatasetPreview = React.memo(function DatasetPreview2({
64758
65173
  /* @__PURE__ */ jsxRuntime.jsx(DownloadButton, { onClick: download, label: "Download value" })
64759
65174
  ] });
64760
65175
  }
65176
+ if (valueQuery.error) {
65177
+ const { message, details } = formatApiError(valueQuery.error);
65178
+ return /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "error", title: "Load failed", message, details: details ?? formatError(valueQuery.error) });
65179
+ }
64761
65180
  if (valueQuery.isLoading || !valueQuery.data) return /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "loading", title: "Loading..." });
64762
- if (valueQuery.error) return /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "error", title: "Decode failed", message: valueQuery.error.message });
64763
65181
  const count = type === void 0 || decoded === void 0 ? null : type.type === "Array" ? decoded.length : type.type === "Set" || type.type === "Dict" ? decoded.size : null;
64764
65182
  const countText = count === null ? "" : `${count.toLocaleString()} ${(type == null ? void 0 : type.type) === "Dict" ? "entries" : "items"} · `;
64765
65183
  return /* @__PURE__ */ jsxRuntime.jsxs(react.Flex, { direction: "column", height: "100%", overflow: "hidden", children: [
@@ -64830,19 +65248,15 @@ const UITaskPreview = React.memo(function UITaskPreview2({
64830
65248
  const workspace = (config2 == null ? void 0 : config2.workspace) ?? (e3 == null ? void 0 : e3.workspace) ?? null;
64831
65249
  const token2 = (config2 == null ? void 0 : config2.token) ?? (e3 == null ? void 0 : e3.token) ?? null;
64832
65250
  const requestOptions = { token: token2 };
65251
+ const bound3 = cache3 == null ? void 0 : cache3.getConfig().workspace;
65252
+ const foreign = (config2 == null ? void 0 : config2.workspace) !== void 0 && bound3 !== void 0 && config2.workspace !== bound3;
64833
65253
  const detailsQuery = useTaskDetails(apiUrl ?? "", repo, workspace, task, { requestOptions });
64834
65254
  const details = detailsQuery.data;
64835
- const kind = details ? getTaskKind(details) : null;
64836
- const isUI = kind === "ui";
64837
- const manifest = React.useMemo(() => {
64838
- if (!details || !isUI) return null;
64839
- const meta3 = getTaskMetadata(details);
64840
- return meta3 ? internal$1.decodeManifest(meta3) : { paths: [], functions: [], records: [], pages: [] };
64841
- }, [details, isUI]);
64842
- const outputPath = details ? treePathToString$1(details.output) : null;
65255
+ const manifest = React.useMemo(() => (details == null ? void 0 : details.role.type) === "ui" ? details.role.value : null, [details]);
65256
+ const outputPath = details ? treePathToString$1(details.output.path) : null;
64843
65257
  const preloads = React.useMemo(
64844
- () => manifest && workspace ? manifest.paths.map((path2) => ({ workspace, path: path2 })) : [],
64845
- [manifest, workspace]
65258
+ () => manifest && workspace && !foreign ? manifest.paths.map((path2) => ({ workspace, path: path2 })) : [],
65259
+ [manifest, workspace, foreign]
64846
65260
  );
64847
65261
  const { loading: preloading, error: preloadError } = usePreloadReactiveDatasets(preloads);
64848
65262
  const scopedPlatforms = React.useMemo(
@@ -64868,7 +65282,7 @@ const UITaskPreview = React.memo(function UITaskPreview2({
64868
65282
  [manifest]
64869
65283
  );
64870
65284
  React.useEffect(() => {
64871
- if (!cache3 || !manifest || !workspace) return;
65285
+ if (!cache3 || !manifest || !workspace || foreign) return;
64872
65286
  for (const path2 of manifest.paths) {
64873
65287
  cache3.setRefetchInterval(workspace, path2, pollInterval);
64874
65288
  }
@@ -64877,7 +65291,7 @@ const UITaskPreview = React.memo(function UITaskPreview2({
64877
65291
  cache3.clearRefetchInterval(workspace, path2);
64878
65292
  }
64879
65293
  };
64880
- }, [cache3, manifest, workspace, pollInterval]);
65294
+ }, [cache3, manifest, workspace, pollInterval, foreign]);
64881
65295
  const statusQuery = useDatasetStatus(apiUrl ?? "", repo, workspace, outputPath, { requestOptions });
64882
65296
  const valueQuery = useDatasetValue(apiUrl ?? "", repo, workspace, outputPath, {
64883
65297
  requestOptions,
@@ -64887,16 +65301,20 @@ const UITaskPreview = React.memo(function UITaskPreview2({
64887
65301
  });
64888
65302
  const store = React.useMemo(() => eastUiComponents.createUIStore(), [task]);
64889
65303
  if (!apiUrl) return /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "error", title: "Configuration error", message: "No apiUrl available — render UITaskPreview inside an <E3Provider> or pass a config prop." });
65304
+ if (foreign) {
65305
+ return /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "error", title: "Workspace mismatch", message: `config names workspace "${config2 == null ? void 0 : config2.workspace}", and the data bindings read workspace "${bound3}", the one the surrounding <ReactiveDatasetProvider> serves: a page previews one workspace.` });
65306
+ }
64890
65307
  if (detailsQuery.isLoading) return /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "loading", title: "Loading task..." });
64891
65308
  if (detailsQuery.error) return /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "error", title: "Error", message: detailsQuery.error.message });
64892
65309
  if (!details) return /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "info", title: "No task", message: `Task "${task}" not found` });
64893
- if (!isUI) return /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "error", title: "Not a UI task", message: `Task "${task}" has kind "${kind ?? "(none)"}"` });
65310
+ if (!manifest) return /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "error", title: "Not a UI task", message: `Task "${task}" is a ${details.role.type} task` });
64894
65311
  if (preloadError) return /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "error", title: "Preload failed", message: preloadError.message });
64895
65312
  if (preloading) return /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "loading", title: "Loading datasets..." });
65313
+ if (statusQuery.error) return /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "error", title: "Error", message: statusQuery.error.message });
64896
65314
  if (statusQuery.isLoading || !statusQuery.data) return /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "loading", title: "Loading..." });
64897
65315
  if (statusQuery.data.refType !== "value") return /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "info", title: "No output yet", message: "Task has not produced a value" });
65316
+ if (valueQuery.error) return /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "error", title: "Load failed", message: valueQuery.error.message });
64898
65317
  if (valueQuery.isLoading || !valueQuery.data) return /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "loading", title: "Loading..." });
64899
- if (valueQuery.error) return /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "error", title: "Decode failed", message: valueQuery.error.message });
64900
65318
  return /* @__PURE__ */ jsxRuntime.jsx(eastUiComponents.UIStoreProvider, { store, children: /* @__PURE__ */ jsxRuntime.jsx(ErrorBoundary, { children: /* @__PURE__ */ jsxRuntime.jsx(react.Box, { height: "100%", overflow: bare ? void 0 : "auto", p: bare ? void 0 : "4", minH: bare ? 0 : void 0, children: /* @__PURE__ */ jsxRuntime.jsx(
64901
65319
  eastUiComponents.EastChakraComponent,
64902
65320
  {
@@ -65353,7 +65771,7 @@ const DataTaskPreview = React.memo(function DataTaskPreview2({
65353
65771
  const detailsQuery = useTaskDetails(apiUrl, repo, workspace, task, {
65354
65772
  ...requestOptions != null && { requestOptions }
65355
65773
  });
65356
- const outputPath = detailsQuery.data ? treePathToString(detailsQuery.data.output) : null;
65774
+ const outputPath = detailsQuery.data ? treePathToString(detailsQuery.data.output.path) : null;
65357
65775
  return /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { height: "100%", display: "flex", flexDirection: "column", overflow: "hidden", children: [
65358
65776
  /* @__PURE__ */ jsxRuntime.jsx(
65359
65777
  react.Flex,
@@ -65417,13 +65835,14 @@ const TaskPreview = React.memo(function TaskPreview2({
65417
65835
  requestOptions,
65418
65836
  bare = false
65419
65837
  }) {
65838
+ var _a2;
65420
65839
  const detailsQuery = useTaskDetails(apiUrl, repo, workspace, task, {
65421
65840
  ...requestOptions != null && { requestOptions }
65422
65841
  });
65423
- const kind = detailsQuery.data ? getTaskKind(detailsQuery.data) : null;
65842
+ const isUI = ((_a2 = detailsQuery.data) == null ? void 0 : _a2.role.type) === "ui";
65424
65843
  return /* @__PURE__ */ jsxRuntime.jsxs(react.Box, { height: "100%", display: "flex", flexDirection: "column", overflow: "hidden", children: [
65425
65844
  !bare && /* @__PURE__ */ jsxRuntime.jsx(react.Flex, { px: 4, py: 2, borderBottom: "1px solid", borderColor: "border.subtle", bg: "bg.surface", align: "center", flexShrink: 0, children: /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "sm", fontWeight: "medium", color: "fg", children: task }) }),
65426
- /* @__PURE__ */ jsxRuntime.jsx(react.Box, { flex: 1, overflow: "hidden", minHeight: 0, children: detailsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "loading", title: "Loading task..." }) : detailsQuery.error ? /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "error", title: "Error", message: detailsQuery.error.message }) : kind === "ui" ? /* @__PURE__ */ jsxRuntime.jsx(
65845
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { flex: 1, overflow: "hidden", minHeight: 0, children: detailsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "loading", title: "Loading task..." }) : detailsQuery.error ? /* @__PURE__ */ jsxRuntime.jsx(StatusDisplay, { variant: "error", title: "Error", message: detailsQuery.error.message }) : isUI ? /* @__PURE__ */ jsxRuntime.jsx(
65427
65846
  UITaskPreview,
65428
65847
  {
65429
65848
  task,
@@ -65441,7 +65860,10 @@ const TaskPreview = React.memo(function TaskPreview2({
65441
65860
  }
65442
65861
  ) })
65443
65862
  ] });
65444
- }, (prev2, next2) => prev2.task === next2.task && prev2.repo === next2.repo && prev2.workspace === next2.workspace && prev2.bare === next2.bare);
65863
+ }, (prev2, next2) => {
65864
+ var _a2, _b2;
65865
+ return prev2.task === next2.task && prev2.apiUrl === next2.apiUrl && prev2.repo === next2.repo && prev2.workspace === next2.workspace && prev2.bare === next2.bare && ((_a2 = prev2.requestOptions) == null ? void 0 : _a2.token) === ((_b2 = next2.requestOptions) == null ? void 0 : _b2.token);
65866
+ });
65445
65867
  function formatPrimitive(type, value) {
65446
65868
  switch (type.type) {
65447
65869
  case "Null":
@@ -65838,8 +66260,6 @@ exports.funcChannelKey = funcChannelKey;
65838
66260
  exports.getBindingTypes = getBindingTypes;
65839
66261
  exports.getReactiveDatasetCache = getReactiveDatasetCache;
65840
66262
  exports.getStagedStore = getStagedStore;
65841
- exports.getTaskKind = getTaskKind;
65842
- exports.getTaskMetadata = getTaskMetadata;
65843
66263
  exports.initializeFunctionApi = initializeFunctionApi;
65844
66264
  exports.initializePagedApi = initializePagedApi;
65845
66265
  exports.initializeReactiveDatasetCache = initializeReactiveDatasetCache;
@@ -65847,6 +66267,7 @@ exports.initializeRecordApi = initializeRecordApi;
65847
66267
  exports.initializeStagedStore = initializeStagedStore;
65848
66268
  exports.isBindingTracking = isBindingTracking;
65849
66269
  exports.onWriteError = onWriteError;
66270
+ exports.pagedRevisionKey = pagedRevisionKey;
65850
66271
  exports.pagedTotalKey = pagedTotalKey;
65851
66272
  exports.pagedWindowKey = pagedWindowKey;
65852
66273
  exports.preloadReactiveDatasetList = preloadReactiveDatasetList;