@elaraai/e3-ui-components 1.0.79 → 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.js CHANGED
@@ -18,22 +18,22 @@ var __privateWrapper = (obj, member, setter, getter) => ({
18
18
  }
19
19
  });
20
20
  var _focused, _cleanup, _setup, _a, _provider, _providerCalled, _b, _online, _cleanup2, _setup2, _c, _gcTimeout, _d, _initialState, _revertState, _cache, _client, _retryer, _defaultOptions, _abortSignalConsumed, _Query_instances, isInitialPausedFetch_fn, dispatch_fn, _e, _client2, _observers, _mutationCache, _retryer2, _Mutation_instances, dispatch_fn2, _f, _mutations, _scopes, _mutationId, _g, _queries, _h, _queryCache, _mutationCache2, _defaultOptions2, _queryDefaults, _mutationDefaults, _mountCount, _unsubscribeFocus, _unsubscribeOnline, _i, _j;
21
- import { variant, SortedMap, compareFor, EastError, fromEastTypeValue, East, none as none$1, some, OptionType, BooleanType, NullType, ConflictError, equalFor, toEastTypeValue, PatchType, diffFor, applyFor, encodeBeast2For, decodeBeast2For, BlobType, IntegerType, StructType, StringType, walkPatch, pathToString, pathDisplay, printFor, isVariant, prunePatchFor, detectConflictsFor, mergeWithResolutionsFor, validatePatchFor, isEastSet, isEastDict, ArrayType, parseFor } from "@elaraai/east";
21
+ import { variant, SortedMap, compareFor, EastError, fromEastTypeValue, East, none as none$1, some, OptionType, BooleanType, NullType, ConflictError, equalFor, toEastTypeValue, PatchType, diffFor, applyFor, encodeBeast2For, decodeBeast2For, BlobType, StringType, IntegerType, StructType, walkPatch, pathToString, pathDisplay, printFor, isVariant, prunePatchFor, detectConflictsFor, mergeWithResolutionsFor, validatePatchFor, isEastSet, isEastDict, ArrayType, parseFor } from "@elaraai/east";
22
22
  import { EastTypeType } from "@elaraai/east/internal";
23
- import { DataBindModeType, DataBindPrimitives, bindPlatformFn, DataPagedPrimitives, bindPagedPlatformFn, FuncBindPrimitives, FuncErrorType, FuncStatusType, funcBindPlatformFn, RecordBindPrimitives, recordBindPlatformFn, Diff, Ontology, FuncBindHandleType, Experiment, decisionBindPlatformFn, DecisionBindPrimitives, DecisionConstraintType, DiffBindingType, CommitStateType, VerdictType, AnswerType, judgementInputType, DecisionType, DecisionQueue, DecisionJournal, decodeManifest } from "@elaraai/e3-ui/internal";
23
+ import { DataBindModeType, DataBindPrimitives, bindPlatformFn, DataPagedPrimitives, bindPagedPlatformFn, bindPagedPinnedPlatformFn, FuncBindPrimitives, FuncErrorType, FuncStatusType, funcBindPlatformFn, RecordBindPrimitives, recordBindPlatformFn, Diff, Ontology, FuncBindHandleType, Experiment, decisionBindPlatformFn, DecisionBindPrimitives, DecisionConstraintType, DiffBindingType, CommitStateType, VerdictType, AnswerType, judgementInputType, DecisionType, DecisionQueue, DecisionJournal } from "@elaraai/e3-ui/internal";
24
24
  import { registerReactiveTracker, registerPlatformImplementation, StateRuntime, getRegisteredPlatformImplementations, boundSliceConfig } from "@elaraai/east-ui-components/platform";
25
25
  import { TreePathType, DatasetStatusType } from "@elaraai/e3-types";
26
- import { workspaceStatus, datasetListAt, datasetList, dataflowExecuteLaunch, datasetSet, datasetGet, ApiError, datasetFindKey, datasetGetPage, workspaceFunctionCall, workspaceFunctionList, workspaceRecordHistory, workspaceRecordMutate, workspaceRecordDescribe, repoCreate, repoGc, repoGcStart, repoGcStatus, repoList, repoRemove, repoStatus, packageExport, packageGet, packageImport, packageList, packageRemove, workspaceCreate, workspaceDeploy, workspaceExport, workspaceGet, workspaceList, workspaceRemove, datasetGetStatus, datasetListRecursive, datasetListRecursivePaths, datasetListWithStatus, taskExecutionList, taskGet, taskList, dataflowCancel, dataflowExecute, dataflowExecutePoll, dataflowGraph, taskLogs } from "@elaraai/e3-api-client";
26
+ import { workspaceStatus, datasetListAt, datasetList, dataflowExecuteLaunch, datasetSet, datasetGet, ApiError, DatasetHashMismatchError, datasetGetStatus, datasetFindKey, datasetGetPage, workspaceFunctionCall, workspaceFunctionList, workspaceRecordHistory, workspaceRecordMutate, workspaceRecordDescribe, repoCreate, repoGc, repoGcStart, repoGcStatus, repoList, repoRemove, repoStatus, packageExport, packageGet, packageImport, packageList, packageRemove, workspaceCreate, workspaceDeploy, workspaceExport, workspaceGet, workspaceList, workspaceRemove, datasetListRecursive, datasetListRecursivePaths, datasetListWithStatus, taskExecutionList, taskGet, taskList, dataflowCancel, dataflowExecute, dataflowExecutePoll, dataflowGraph, taskLogs } from "@elaraai/e3-api-client";
27
27
  import { SeekQueryType, SeekRangeType, ValueTree } from "@elaraai/east-ui";
28
28
  import { jsx as jsx$1, Fragment, jsxs } from "react/jsx-runtime";
29
29
  import * as React from "react";
30
- import React__default, { useId, useMemo, useContext, createContext, useRef, useEffect, useState, useCallback, useSyncExternalStore, memo, forwardRef, useLayoutEffect, Fragment as Fragment$1, Component } from "react";
30
+ import React__default, { useId, useMemo, useContext, createContext, useRef, useState, useEffect, useCallback, useSyncExternalStore, memo, forwardRef, useLayoutEffect, Fragment as Fragment$1, Component } from "react";
31
31
  import { Box, Flex, Text, HStack, Input, useSlotRecipe, useRecipe, VStack, useToken, Drawer, Portal, Field, NativeSelect, Textarea, chakra, Tooltip, SegmentGroup, Menu, HoverCard, useChakraContext, Spinner, Code, Clipboard, IconButton, Button, Tabs, Badge, useTabs } from "@chakra-ui/react";
32
32
  import { implementUIComponent, getSomeorUndefined, formatPredicate, SliceEditPopover, SlicePredicateBuilder, EastChakraSelect, EastChakraStringInput, EastChakraDateTimeInput, EastChakraFloatInput, EastChakraIntegerInput, formatTick, useSliceReactivity, useContainerBelow, usePersistedState, SliceRailCluster, EastChakraComponent, ClauseChip, StateImpl, NavImpl, SliceImpl, SliceApplyImpl, OverlayImpl, ClipboardImpl, DownloadImpl, ShareImpl, LoadingIcon, DatasetKeySearch, EastChakraValueTree, keyRangePredicates, createUIStore, UIStoreProvider } from "@elaraai/east-ui-components";
33
33
  import { DatasetKeySearch as DatasetKeySearch2 } from "@elaraai/east-ui-components";
34
34
  import { createPortal } from "react-dom";
35
35
  import { sliceMatches, pruneRetainedPages } from "@elaraai/east-ui/internal";
36
- import { QueryClient as QueryClient$1, QueryClientProvider, useMutation, useQuery, keepPreviousData, useQueryClient } from "@tanstack/react-query";
36
+ import { QueryClient as QueryClient$1, QueryClientProvider, useMutation, useQuery, useQueryClient, keepPreviousData } from "@tanstack/react-query";
37
37
  import { useVirtualizer } from "@tanstack/react-virtual";
38
38
  var Subscribable = class {
39
39
  constructor() {
@@ -2060,6 +2060,7 @@ const realClock = {
2060
2060
  return { clear: () => clearInterval(id3) };
2061
2061
  }
2062
2062
  };
2063
+ const HASH_POLL_MS = 1e3;
2063
2064
  function createDefaultDatasetApi(apiUrl, repo, getToken) {
2064
2065
  const opts = () => ({ token: getToken() });
2065
2066
  return {
@@ -2125,12 +2126,17 @@ class ReactiveDatasetCache {
2125
2126
  __publicField(this, "keyVersions", /* @__PURE__ */ new Map());
2126
2127
  // Workspace status polling — groups subscriptions by workspace for
2127
2128
  // efficiency. Each entry holds the active interval handle from the
2128
- // injected {@link Clock} so tests can drive ticking deterministically.
2129
+ // injected {@link Clock} so tests can drive ticking deterministically,
2130
+ // and each polled path with how many watchers poll it.
2129
2131
  __publicField(this, "workspacePollers", /* @__PURE__ */ new Map());
2130
2132
  __publicField(this, "clock");
2131
2133
  // In-flight poll dedup — concurrent callers (interval tick + a
2132
2134
  // post-write trigger) share a single round-trip rather than racing.
2133
2135
  __publicField(this, "inFlightPolls", /* @__PURE__ */ new Map());
2136
+ // Hash-only watches, by workspace and path: each callback with the hash it
2137
+ // was last told. A watched path rides its workspace's poll, but its
2138
+ // content is never fetched — a paged source reads windows, never the value.
2139
+ __publicField(this, "hashWatches", /* @__PURE__ */ new Map());
2134
2140
  // Batching
2135
2141
  __publicField(this, "batchDepth", 0);
2136
2142
  __publicField(this, "changedKeys", /* @__PURE__ */ new Set());
@@ -2219,9 +2225,7 @@ class ReactiveDatasetCache {
2219
2225
  hash: void 0,
2220
2226
  status: variant("stale", null)
2221
2227
  });
2222
- if (this.workspacePollers.has(workspace)) {
2223
- void this.pollWorkspaceStatus(workspace);
2224
- }
2228
+ void this.refresh(workspace);
2225
2229
  } catch (error3) {
2226
2230
  if (!this.destroyed && this.writeEpochs.get(key) === myEpoch) {
2227
2231
  const baseline = this.writeBaselines.get(key);
@@ -2327,15 +2331,15 @@ class ReactiveDatasetCache {
2327
2331
  * 3. Only fetches full content when hash changes
2328
2332
  *
2329
2333
  * Multiple subscriptions to the same workspace share a single poller.
2330
- * Pair with {@link clearRefetchInterval} on unmount so the poller can
2331
- * stop when nothing is watching.
2334
+ * Pair each call with one {@link clearRefetchInterval} on unmount so the
2335
+ * poller can stop when nothing is watching.
2332
2336
  */
2333
2337
  setRefetchInterval(workspace, path2, intervalMs) {
2334
2338
  var _a2;
2335
2339
  const pathStr = datasetPathToString(path2);
2336
2340
  let poller = this.workspacePollers.get(workspace);
2337
2341
  if (poller) {
2338
- poller.paths.add(pathStr);
2342
+ poller.paths.set(pathStr, (poller.paths.get(pathStr) ?? 0) + 1);
2339
2343
  if (intervalMs < poller.intervalMs) {
2340
2344
  (_a2 = poller.handle) == null ? void 0 : _a2.clear();
2341
2345
  poller.intervalMs = intervalMs;
@@ -2347,7 +2351,7 @@ class ReactiveDatasetCache {
2347
2351
  } else {
2348
2352
  poller = {
2349
2353
  intervalMs,
2350
- paths: /* @__PURE__ */ new Set([pathStr]),
2354
+ paths: /* @__PURE__ */ new Map([[pathStr, 1]]),
2351
2355
  handle: this.clock.setInterval(
2352
2356
  () => this.pollWorkspaceStatus(workspace),
2353
2357
  intervalMs
@@ -2358,23 +2362,79 @@ class ReactiveDatasetCache {
2358
2362
  }
2359
2363
  }
2360
2364
  /**
2361
- * Stop polling a dataset; the workspace poller is torn down when its
2362
- * last watched path is cleared.
2365
+ * Stop polling a dataset for one watcher; the path goes when its last
2366
+ * watcher clears it, and the workspace poller is torn down when its last
2367
+ * path goes.
2363
2368
  */
2364
2369
  clearRefetchInterval(workspace, path2) {
2365
- var _a2;
2366
2370
  const poller = this.workspacePollers.get(workspace);
2367
2371
  if (!poller) return;
2368
- poller.paths.delete(datasetPathToString(path2));
2369
- if (poller.paths.size === 0) {
2372
+ const pathStr = datasetPathToString(path2);
2373
+ const watchers = (poller.paths.get(pathStr) ?? 0) - 1;
2374
+ if (watchers > 0) {
2375
+ poller.paths.set(pathStr, watchers);
2376
+ return;
2377
+ }
2378
+ poller.paths.delete(pathStr);
2379
+ this.stopIdlePoller(workspace);
2380
+ }
2381
+ /**
2382
+ * Follow a dataset's content hash without fetching its content; see the
2383
+ * interface doc.
2384
+ */
2385
+ watchHash(workspace, path2, onHash, intervalMs = HASH_POLL_MS) {
2386
+ var _a2;
2387
+ const pathStr = datasetPathToString(path2);
2388
+ let byPath = this.hashWatches.get(workspace);
2389
+ if (!byPath) {
2390
+ byPath = /* @__PURE__ */ new Map();
2391
+ this.hashWatches.set(workspace, byPath);
2392
+ }
2393
+ let callbacks = byPath.get(pathStr);
2394
+ if (!callbacks) {
2395
+ callbacks = /* @__PURE__ */ new Map();
2396
+ byPath.set(pathStr, callbacks);
2397
+ }
2398
+ callbacks.set(onHash, void 0);
2399
+ const poller = this.workspacePollers.get(workspace);
2400
+ if (!poller) {
2401
+ this.workspacePollers.set(workspace, {
2402
+ intervalMs,
2403
+ paths: /* @__PURE__ */ new Map(),
2404
+ handle: this.clock.setInterval(() => this.pollWorkspaceStatus(workspace), intervalMs)
2405
+ });
2406
+ void this.pollWorkspaceStatus(workspace);
2407
+ } else if (intervalMs < poller.intervalMs) {
2370
2408
  (_a2 = poller.handle) == null ? void 0 : _a2.clear();
2371
- this.workspacePollers.delete(workspace);
2409
+ poller.intervalMs = intervalMs;
2410
+ poller.handle = this.clock.setInterval(() => this.pollWorkspaceStatus(workspace), intervalMs);
2372
2411
  }
2412
+ return () => {
2413
+ const watched = this.hashWatches.get(workspace);
2414
+ const set3 = watched == null ? void 0 : watched.get(pathStr);
2415
+ if (!watched || !set3) return;
2416
+ set3.delete(onHash);
2417
+ if (set3.size === 0) watched.delete(pathStr);
2418
+ if (watched.size === 0) this.hashWatches.delete(workspace);
2419
+ this.stopIdlePoller(workspace);
2420
+ };
2421
+ }
2422
+ /** Stops a workspace's poller once it has no path to reconcile and no hash
2423
+ * to follow — without this, a long-lived session polls forever. */
2424
+ stopIdlePoller(workspace) {
2425
+ var _a2;
2426
+ const poller = this.workspacePollers.get(workspace);
2427
+ if (!poller || poller.paths.size > 0 || this.hashWatches.has(workspace)) return;
2428
+ (_a2 = poller.handle) == null ? void 0 : _a2.clear();
2429
+ this.workspacePollers.delete(workspace);
2373
2430
  }
2374
2431
  refresh(workspace) {
2375
2432
  if (this.destroyed) return Promise.resolve();
2376
2433
  if (!this.workspacePollers.has(workspace)) return Promise.resolve();
2377
- return this.pollWorkspaceStatus(workspace);
2434
+ const inFlight = this.inFlightPolls.get(workspace);
2435
+ if (inFlight === void 0) return this.pollWorkspaceStatus(workspace);
2436
+ const again = () => this.destroyed || !this.workspacePollers.has(workspace) ? void 0 : this.pollWorkspaceStatus(workspace);
2437
+ return inFlight.then(again, again);
2378
2438
  }
2379
2439
  /**
2380
2440
  * Poll workspace status and reconcile the cache with the server.
@@ -2390,11 +2450,12 @@ class ReactiveDatasetCache {
2390
2450
  return promise4;
2391
2451
  }
2392
2452
  async doPollWorkspaceStatus(workspace) {
2453
+ var _a2;
2393
2454
  if (this.destroyed) return;
2394
2455
  const poller = this.workspacePollers.get(workspace);
2395
- if (!poller || poller.paths.size === 0) return;
2456
+ if (!poller || poller.paths.size === 0 && !this.hashWatches.has(workspace)) return;
2396
2457
  const epochsAtStart = /* @__PURE__ */ new Map();
2397
- for (const pathStr of poller.paths) {
2458
+ for (const pathStr of poller.paths.keys()) {
2398
2459
  const key = pathStr ? `${workspace}.${pathStr}` : workspace;
2399
2460
  epochsAtStart.set(key, this.writeEpochs.get(key) ?? 0);
2400
2461
  }
@@ -2408,15 +2469,28 @@ class ReactiveDatasetCache {
2408
2469
  if (this.destroyed) return;
2409
2470
  const infoByPath = /* @__PURE__ */ new Map();
2410
2471
  for (const info of status.datasets) infoByPath.set(info.path, info);
2472
+ for (const [pathStr, callbacks] of this.hashWatches.get(workspace) ?? []) {
2473
+ const info = infoByPath.get(pathStr ? `.${pathStr}` : "");
2474
+ const hash2 = ((_a2 = info == null ? void 0 : info.hash) == null ? void 0 : _a2.type) === "some" ? info.hash.value : null;
2475
+ for (const [onHash, told] of [...callbacks]) {
2476
+ if (told === hash2 || !callbacks.has(onHash)) continue;
2477
+ callbacks.set(onHash, hash2);
2478
+ try {
2479
+ onHash(hash2);
2480
+ } catch (error3) {
2481
+ console.error(`A hash watch on ${workspace}.${pathStr} failed:`, error3);
2482
+ }
2483
+ }
2484
+ }
2411
2485
  const pending = [];
2412
2486
  this.batch(() => {
2413
- var _a2;
2414
- for (const pathStr of poller.paths) {
2487
+ var _a3;
2488
+ for (const pathStr of poller.paths.keys()) {
2415
2489
  const e3Path = pathStr ? `.${pathStr}` : "";
2416
2490
  const info = infoByPath.get(e3Path);
2417
2491
  const key = pathStr ? `${workspace}.${pathStr}` : workspace;
2418
2492
  const path2 = this.stringToPath(pathStr);
2419
- const currentHash = ((_a2 = info == null ? void 0 : info.hash) == null ? void 0 : _a2.type) === "some" ? info.hash.value : null;
2493
+ const currentHash = ((_a3 = info == null ? void 0 : info.hash) == null ? void 0 : _a3.type) === "some" ? info.hash.value : null;
2420
2494
  const knownHash = this.knownHashes.get(key);
2421
2495
  if (info == null ? void 0 : info.status) {
2422
2496
  const previous = this.statuses.get(key);
@@ -2582,6 +2656,7 @@ class ReactiveDatasetCache {
2582
2656
  }
2583
2657
  this.workspacePollers.clear();
2584
2658
  this.inFlightPolls.clear();
2659
+ this.hashWatches.clear();
2585
2660
  this.keySubscribers.clear();
2586
2661
  this.globalSubscribers.clear();
2587
2662
  this.knownHashes.clear();
@@ -2696,9 +2771,16 @@ class MemoryStagedAdapter {
2696
2771
  this.map.clear();
2697
2772
  }
2698
2773
  }
2774
+ const SCOPE_SEPARATOR = "\0";
2775
+ function cacheKeyOf(key) {
2776
+ return key.slice(key.indexOf(SCOPE_SEPARATOR) + 1);
2777
+ }
2699
2778
  class StagedStore {
2700
2779
  constructor(adapter) {
2780
+ // By scope and `datasetCacheKey`; subscribers and versions go by the
2781
+ // `datasetCacheKey` alone, which is what a reactive view tracks.
2701
2782
  __publicField(this, "entries", /* @__PURE__ */ new Map());
2783
+ __publicField(this, "scope", null);
2702
2784
  __publicField(this, "subscribers", /* @__PURE__ */ new Map());
2703
2785
  __publicField(this, "versions", /* @__PURE__ */ new Map());
2704
2786
  __publicField(this, "adapter");
@@ -2722,25 +2804,25 @@ class StagedStore {
2722
2804
  }
2723
2805
  }
2724
2806
  hasPending(workspace, path2) {
2725
- return this.entries.has(datasetCacheKey(workspace, path2));
2807
+ return this.entries.has(this.keyOf(workspace, path2));
2726
2808
  }
2727
2809
  getBuffered(workspace, path2) {
2728
2810
  var _a2;
2729
- return (_a2 = this.entries.get(datasetCacheKey(workspace, path2))) == null ? void 0 : _a2.buffered;
2811
+ return (_a2 = this.entries.get(this.keyOf(workspace, path2))) == null ? void 0 : _a2.buffered;
2730
2812
  }
2731
2813
  getSnapshot(workspace, path2) {
2732
2814
  var _a2;
2733
- return (_a2 = this.entries.get(datasetCacheKey(workspace, path2))) == null ? void 0 : _a2.snapshot;
2815
+ return (_a2 = this.entries.get(this.keyOf(workspace, path2))) == null ? void 0 : _a2.snapshot;
2734
2816
  }
2735
2817
  getEntry(workspace, path2) {
2736
- return this.entries.get(datasetCacheKey(workspace, path2));
2818
+ return this.entries.get(this.keyOf(workspace, path2));
2737
2819
  }
2738
2820
  write(workspace, path2, snapshotIfNew, buffered) {
2739
- const key = datasetCacheKey(workspace, path2);
2821
+ const key = this.keyOf(workspace, path2);
2740
2822
  const existing = this.entries.get(key);
2741
2823
  const next2 = existing ? { snapshot: existing.snapshot, buffered } : { snapshot: snapshotIfNew, buffered };
2742
2824
  this.entries.set(key, next2);
2743
- this.notify(key);
2825
+ this.notify(datasetCacheKey(workspace, path2));
2744
2826
  const persisted = {
2745
2827
  snapshot: next2.snapshot,
2746
2828
  buffered: next2.buffered
@@ -2748,15 +2830,26 @@ class StagedStore {
2748
2830
  this.persist(key, () => this.adapter.save(key, persisted));
2749
2831
  }
2750
2832
  discard(workspace, path2) {
2751
- const key = datasetCacheKey(workspace, path2);
2833
+ const key = this.keyOf(workspace, path2);
2752
2834
  if (!this.entries.has(key)) return false;
2753
2835
  this.entries.delete(key);
2754
- this.notify(key);
2836
+ this.notify(datasetCacheKey(workspace, path2));
2755
2837
  this.persist(key, () => this.adapter.remove(key));
2756
2838
  return true;
2757
2839
  }
2758
2840
  listKeys() {
2759
- return [...this.entries.keys()];
2841
+ const prefix2 = `${this.scope ?? ""}${SCOPE_SEPARATOR}`;
2842
+ return [...this.entries.keys()].filter((key) => key.startsWith(prefix2)).map(cacheKeyOf);
2843
+ }
2844
+ setScope(scope) {
2845
+ const before = this.scope;
2846
+ this.scope = scope;
2847
+ this.notifyScopes(before, scope);
2848
+ return () => {
2849
+ if (this.scope !== scope) return;
2850
+ this.scope = null;
2851
+ this.notifyScopes(scope, null);
2852
+ };
2760
2853
  }
2761
2854
  subscribe(key, callback) {
2762
2855
  let set3 = this.subscribers.get(key);
@@ -2779,15 +2872,29 @@ class StagedStore {
2779
2872
  this.errorListeners.delete(cb);
2780
2873
  };
2781
2874
  }
2782
- /** Test-only: clear in-memory + persisted state. */
2875
+ /** Test-only: clear in-memory + persisted state, in every scope. */
2783
2876
  async clear() {
2784
2877
  const keys = [...this.entries.keys()];
2785
2878
  this.entries.clear();
2786
- for (const key of keys) this.notify(key);
2879
+ for (const key of new Set(keys.map(cacheKeyOf))) this.notify(key);
2787
2880
  await this.flushPending();
2788
2881
  await this.adapter.clear();
2789
2882
  }
2790
2883
  // ----- internals -------------------------------------------------------
2884
+ /** The key a path's entry is held under in the current scope. */
2885
+ keyOf(workspace, path2) {
2886
+ return `${this.scope ?? ""}${SCOPE_SEPARATOR}${datasetCacheKey(workspace, path2)}`;
2887
+ }
2888
+ /** Tells every path with an entry in either scope that what it reads has
2889
+ * changed: the scope's move shows one's entries and hides the other's. */
2890
+ notifyScopes(from2, to) {
2891
+ const keys = /* @__PURE__ */ new Set();
2892
+ for (const key of this.entries.keys()) {
2893
+ const scope = key.slice(0, key.indexOf(SCOPE_SEPARATOR));
2894
+ if (scope === (from2 ?? "") || scope === (to ?? "")) keys.add(cacheKeyOf(key));
2895
+ }
2896
+ for (const key of keys) this.notify(key);
2897
+ }
2791
2898
  notify(key) {
2792
2899
  this.versions.set(key, (this.versions.get(key) ?? 0) + 1);
2793
2900
  const subs = this.subscribers.get(key);
@@ -2804,7 +2911,7 @@ class StagedStore {
2804
2911
  const settled = prev2.then(op, op).then(
2805
2912
  () => void 0,
2806
2913
  (err) => {
2807
- this.emitPersistError(key, err);
2914
+ this.emitPersistError(cacheKeyOf(key), err);
2808
2915
  }
2809
2916
  ).then(() => {
2810
2917
  if (this.saveChains.get(key) === settled) this.saveChains.delete(key);
@@ -2835,11 +2942,18 @@ class StagedStore {
2835
2942
  return;
2836
2943
  }
2837
2944
  for (const [key, p2] of persisted) {
2945
+ if (!key.includes(SCOPE_SEPARATOR)) {
2946
+ this.persist(key, () => this.adapter.remove(key));
2947
+ this.emitPersistError(key, new Error(
2948
+ `a staged edit of ${key} that an older build left names no server or repository, and was dropped`
2949
+ ));
2950
+ continue;
2951
+ }
2838
2952
  this.entries.set(key, {
2839
2953
  snapshot: p2.snapshot,
2840
2954
  buffered: p2.buffered
2841
2955
  });
2842
- this.notify(key);
2956
+ this.notify(cacheKeyOf(key));
2843
2957
  }
2844
2958
  }
2845
2959
  }
@@ -2956,8 +3070,14 @@ class BindRuntime {
2956
3070
  }
2957
3071
  this.cache = cache3;
2958
3072
  }
2959
- /** Clear the active cache and drop any queued writes. */
2960
- clearCache() {
3073
+ /**
3074
+ * Clear the active cache and drop any queued writes.
3075
+ *
3076
+ * @param cache - Clear only while this is the active cache, so a provider
3077
+ * tearing down after another installed its own leaves that one in place
3078
+ */
3079
+ clearCache(cache3) {
3080
+ if (cache3 !== void 0 && this.cache !== cache3) return;
2961
3081
  this.cache = null;
2962
3082
  this.clearPendingWrites();
2963
3083
  this.handleCache.clear();
@@ -3282,6 +3402,10 @@ class BindRuntime {
3282
3402
  this.queueWrite(() => cache3.launchDataflow(ws));
3283
3403
  return null;
3284
3404
  };
3405
+ const hasSource = () => {
3406
+ this.trackPath(ws, sourcePath);
3407
+ return cache3.has(ws, sourcePath);
3408
+ };
3285
3409
  if (mode === "direct" && !patchPath) {
3286
3410
  return {
3287
3411
  read: () => {
@@ -3304,7 +3428,7 @@ class BindRuntime {
3304
3428
  pending: () => false,
3305
3429
  commit: () => null,
3306
3430
  discard: () => null,
3307
- has: () => cache3.has(ws, sourcePath),
3431
+ has: hasSource,
3308
3432
  status: () => {
3309
3433
  this.trackPath(ws, sourcePath);
3310
3434
  return cache3.getStatus(ws, sourcePath);
@@ -3353,7 +3477,7 @@ class BindRuntime {
3353
3477
  this.queueWrite(() => cache3.write(ws, pPath2, encodePatch(UNCHANGED_VARIANT)));
3354
3478
  return null;
3355
3479
  },
3356
- has: () => cache3.has(ws, sourcePath),
3480
+ has: hasSource,
3357
3481
  status: () => {
3358
3482
  this.trackPath(ws, sourcePath);
3359
3483
  return cache3.getStatus(ws, sourcePath);
@@ -3397,7 +3521,7 @@ class BindRuntime {
3397
3521
  staged.discard(ws, sourcePath);
3398
3522
  return null;
3399
3523
  },
3400
- has: () => cache3.has(ws, sourcePath),
3524
+ has: hasSource,
3401
3525
  status: () => {
3402
3526
  this.trackPath(ws, sourcePath);
3403
3527
  return cache3.getStatus(ws, sourcePath);
@@ -3444,7 +3568,7 @@ class BindRuntime {
3444
3568
  staged.discard(ws, sourcePath);
3445
3569
  return null;
3446
3570
  },
3447
- has: () => cache3.has(ws, sourcePath),
3571
+ has: hasSource,
3448
3572
  status: () => {
3449
3573
  this.trackPath(ws, sourcePath);
3450
3574
  return cache3.getStatus(ws, sourcePath);
@@ -3502,8 +3626,8 @@ function getReactiveDatasetCache() {
3502
3626
  function initializeReactiveDatasetCache(cache3) {
3503
3627
  defaultBindRuntime.initializeCache(cache3);
3504
3628
  }
3505
- function clearReactiveDatasetCache() {
3506
- defaultBindRuntime.clearCache();
3629
+ function clearReactiveDatasetCache(cache3) {
3630
+ defaultBindRuntime.clearCache(cache3);
3507
3631
  }
3508
3632
  function enableBindingTracking() {
3509
3633
  return defaultBindRuntime.enableTracking();
@@ -3683,7 +3807,11 @@ class TrackedChannelStore {
3683
3807
  this.keyVersions.clear();
3684
3808
  }
3685
3809
  }
3686
- function createDefaultPagedApi(apiUrl, repo, getToken) {
3810
+ const NO_INDEX = { index: null, join: false };
3811
+ function selectorKey(selector2) {
3812
+ return selector2.index === null ? "" : `@${selector2.index}${selector2.join ? "+join" : ""}`;
3813
+ }
3814
+ function createDefaultPagedApi(apiUrl, repo, getToken, datasets) {
3687
3815
  const opts = () => ({ token: getToken() });
3688
3816
  return {
3689
3817
  async getPage(workspace, path2, window3) {
@@ -3691,58 +3819,115 @@ function createDefaultPagedApi(apiUrl, repo, getToken) {
3691
3819
  },
3692
3820
  async findKey(workspace, path2, query) {
3693
3821
  return datasetFindKey(apiUrl, repo, workspace, path2, query, opts());
3822
+ },
3823
+ async getRevision(workspace, path2) {
3824
+ const status = await datasetGetStatus(apiUrl, repo, workspace, path2, opts());
3825
+ return status.hash.type === "some" ? status.hash.value : null;
3826
+ },
3827
+ watchRevision(workspace, path2, onChange2) {
3828
+ return datasets.watchHash(workspace, path2, onChange2);
3694
3829
  }
3695
3830
  };
3696
3831
  }
3697
3832
  const RETRY_AFTER_MS = 2e3;
3698
3833
  const MAX_RETAINED_WINDOWS = 24;
3699
- const PERMANENT_PAGE_ERRORS = /* @__PURE__ */ new Set(["dataset_not_pageable"]);
3700
- function isPermanentPageError(err) {
3834
+ const PERMANENT_PAGE_ERRORS = /* @__PURE__ */ new Set([
3835
+ "bad_request",
3836
+ "dataset_not_canonical",
3837
+ "dataset_not_indexed",
3838
+ "dataset_not_pageable",
3839
+ "dataset_not_searchable",
3840
+ "dataset_not_segmented",
3841
+ "index_not_found",
3842
+ "key_parse_error"
3843
+ ]);
3844
+ const NO_VALUE_ERRORS = /* @__PURE__ */ new Set(["dataset_unassigned", "dataset_null"]);
3845
+ function toSelector(indexArg, joinArg) {
3846
+ const index2 = indexArg;
3847
+ return {
3848
+ index: index2 !== void 0 && index2.type === "some" ? index2.value : null,
3849
+ join: joinArg === true
3850
+ };
3851
+ }
3852
+ function checkDeclared(allowed, path2) {
3853
+ if (allowed === null) return;
3854
+ const pathStr = datasetPathToString(path2);
3855
+ if (!allowed.has(pathStr)) {
3856
+ throw new Error(
3857
+ `Data.bindPaged: source path "${pathStr}" not declared in manifest — bind it in the task body so derivation records it`
3858
+ );
3859
+ }
3860
+ }
3861
+ function errorCode(err) {
3701
3862
  const code = err == null ? void 0 : err.code;
3702
- return typeof code === "string" && PERMANENT_PAGE_ERRORS.has(code);
3863
+ return typeof code === "string" ? code : void 0;
3864
+ }
3865
+ function isPermanentPageError(err) {
3866
+ const code = errorCode(err);
3867
+ return code !== void 0 && PERMANENT_PAGE_ERRORS.has(code);
3868
+ }
3869
+ function failureOf(path2, err) {
3870
+ const code = errorCode(err);
3871
+ const details = err == null ? void 0 : err.details;
3872
+ const reason = code === void 0 ? err instanceof Error ? err.message : String(err) : typeof details === "string" && details !== "" ? `${code}: ${details}` : code;
3873
+ return `Data.bindPaged: ${datasetPathToString(path2)}: ${reason}`;
3703
3874
  }
3704
3875
  function keyTypeOf(sourceType) {
3705
3876
  if (sourceType.type === "Dict") return sourceType.value.key;
3706
3877
  if (sourceType.type === "Set") return sourceType.value;
3707
3878
  return null;
3708
3879
  }
3709
- function buildSeek(sourceType, T2, pathExpr, platform) {
3710
- if (keyTypeOf(sourceType) === null) return none$1;
3880
+ function buildSeek(sourceType, T2, pathExpr, indexExpr, joinExpr, selector2, platform) {
3881
+ if (selector2.index === null && keyTypeOf(sourceType) === null) return none$1;
3711
3882
  const { seek } = DataPagedPrimitives;
3712
3883
  return some(East.compile(
3713
3884
  East.function([SeekQueryType], OptionType(SeekRangeType), ($3, query) => {
3714
- $3.return(seek([T2], pathExpr, query));
3885
+ $3.return(seek([T2], pathExpr, indexExpr, joinExpr, query));
3715
3886
  }),
3716
3887
  platform
3717
3888
  ));
3718
3889
  }
3719
- function pagedWindowKey(workspace, path2, offset, limit2) {
3720
- return `paged:${workspace}:${datasetPathToString(path2)}#${offset}+${limit2}`;
3890
+ function pagedRevisionKey(workspace, path2) {
3891
+ return `paged:${workspace}:${datasetPathToString(path2)}#revision`;
3721
3892
  }
3722
- function pagedTotalKey(workspace, path2) {
3723
- return `paged:${workspace}:${datasetPathToString(path2)}#total`;
3893
+ function pagedWindowKey(workspace, path2, revision, offset, limit2, selector2 = NO_INDEX) {
3894
+ return `paged:${workspace}:${datasetPathToString(path2)}${selectorKey(selector2)}@${revision}#${offset}+${limit2}`;
3724
3895
  }
3725
- function pagedSeekKey(workspace, path2, query) {
3726
- const q = "key" in query ? `k=${query.key}` : "fields" in query ? `f=${query.fields.join("\0")}|p=${query.prefix ?? ""}` : `p=${query.prefix}`;
3727
- return `paged:${workspace}:${datasetPathToString(path2)}#seek:${q}`;
3896
+ function pagedTotalKey(workspace, path2, revision, selector2 = NO_INDEX) {
3897
+ return `paged:${workspace}:${datasetPathToString(path2)}${selectorKey(selector2)}@${revision}#total`;
3728
3898
  }
3729
- function toFindQuery(query) {
3899
+ function pagedSeekKey(workspace, path2, revision, query, selector2 = NO_INDEX) {
3900
+ 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")}`;
3901
+ return `paged:${workspace}:${datasetPathToString(path2)}${selectorKey(selector2)}@${revision}#seek:${q}`;
3902
+ }
3903
+ function toFindQuery(query, selector2 = NO_INDEX) {
3730
3904
  const q = query;
3731
- if (q.type === "key") return { key: q.value };
3732
- if (q.type === "prefix") return { prefix: q.value };
3905
+ const scope = selector2.index === null ? {} : { index: selector2.index };
3906
+ if (q.type === "key") return { key: q.value, ...scope };
3907
+ if (q.type === "prefix") return { prefix: q.value, ...scope };
3908
+ if (q.type === "range") {
3909
+ const r2 = q.value;
3910
+ const from2 = [...r2.from];
3911
+ const to = [...r2.to];
3912
+ if (from2.length === 0 && to.length === 0) {
3913
+ throw new Error("Data.bindPaged: a range seek names no end — give it a `from`, a `to`, or both");
3914
+ }
3915
+ return from2.length === 0 ? { to, ...scope } : to.length === 0 ? { from: from2, ...scope } : { from: from2, to, ...scope };
3916
+ }
3733
3917
  const f2 = q.value;
3734
3918
  const fields = [...f2.values];
3735
- return f2.prefix.type === "some" ? { fields, prefix: f2.prefix.value } : { fields };
3919
+ return f2.prefix.type === "some" ? { fields, prefix: f2.prefix.value, ...scope } : { fields, ...scope };
3736
3920
  }
3737
3921
  class PagedRuntime extends TrackedChannelStore {
3738
3922
  constructor() {
3739
3923
  super(...arguments);
3740
3924
  __publicField(this, "api", null);
3741
3925
  __publicField(this, "workspace", null);
3742
- // Compiled-handle cache (issue #106 perf): buildHandle compiles 2
3926
+ // Compiled-handle cache (issue #106 perf): buildHandle compiles several
3743
3927
  // East.functions per bind, and binds re-run every reactive frame. The
3744
- // method IR is a pure function of (sourceType, sourcePath), and the methods
3745
- // resolve api/workspace LIVE, so a cached handle still re-binds.
3928
+ // method IR is a pure function of (sourceType, path, selector, shape), and
3929
+ // the methods resolve api/workspace LIVE, so a cached handle still
3930
+ // re-binds.
3746
3931
  //
3747
3932
  // Keyed by TYPE first, then path — never by path alone. The window decoder
3748
3933
  // is baked from `sourceType`, so a path re-bound at a different type (a
@@ -3757,6 +3942,10 @@ class PagedRuntime extends TrackedChannelStore {
3757
3942
  /** Loaded window keys in least-recently-READ order (a Map preserves
3758
3943
  * insertion order; a read re-inserts). Bounds the decoded-window cache. */
3759
3944
  __publicField(this, "loadedWindows", /* @__PURE__ */ new Map());
3945
+ /** Each dataset's snapshot, by workspace and path. */
3946
+ __publicField(this, "snapshots", /* @__PURE__ */ new Map());
3947
+ /** Fetches launched so far — the source of every channel's `launchSeq`. */
3948
+ __publicField(this, "launches", 0);
3760
3949
  }
3761
3950
  /** Monotonic clock seam so tests can drive the retry gate. */
3762
3951
  now() {
@@ -3767,17 +3956,33 @@ class PagedRuntime extends TrackedChannelStore {
3767
3956
  }
3768
3957
  // ----- wiring ----------------------------------------------------------
3769
3958
  /** Install the API adapter + workspace — called by the React provider
3770
- * (or a test/showcase harness) before any handle is used. */
3959
+ * (or a test/showcase harness) before any handle is used. Another adapter
3960
+ * or workspace starts from nothing: no snapshot of the old one's carries
3961
+ * over. */
3771
3962
  initialize(api3, workspace) {
3963
+ if (this.api !== api3 || this.workspace !== workspace) this.reset();
3772
3964
  this.api = api3;
3773
3965
  this.workspace = workspace;
3774
3966
  }
3775
- /** Tear down the adapter and all window state. */
3776
- clear() {
3967
+ /**
3968
+ * Tear down the adapter and all window state.
3969
+ *
3970
+ * @param api - Clear only while this is the installed adapter, so a
3971
+ * provider tearing down after another installed its own leaves that one
3972
+ * in place
3973
+ */
3974
+ clear(api3) {
3975
+ if (api3 !== void 0 && this.api !== api3) return;
3777
3976
  this.api = null;
3778
3977
  this.workspace = null;
3779
- this.clearChannels();
3978
+ this.reset();
3780
3979
  this.handleCache.clear();
3980
+ }
3981
+ /** Stop following every dataset, and drop what every snapshot delivered. */
3982
+ reset() {
3983
+ for (const snapshot of this.snapshots.values()) snapshot.unwatch();
3984
+ this.snapshots.clear();
3985
+ this.clearChannels();
3781
3986
  this.loadedWindows.clear();
3782
3987
  }
3783
3988
  resolveWorkspace() {
@@ -3788,7 +3993,178 @@ class PagedRuntime extends TrackedChannelStore {
3788
3993
  }
3789
3994
  return this.workspace;
3790
3995
  }
3791
- // ----- window loading --------------------------------------------------
3996
+ // ----- snapshots -------------------------------------------------------
3997
+ /** A dataset's snapshot, followed from its first read until the runtime
3998
+ * is cleared. */
3999
+ snapshotOf(workspace, path2) {
4000
+ const key = `${workspace}:${datasetPathToString(path2)}`;
4001
+ const existing = this.snapshots.get(key);
4002
+ if (existing !== void 0) return existing;
4003
+ const snapshot = {
4004
+ key,
4005
+ revision: void 0,
4006
+ moves: 0,
4007
+ findSeq: 0,
4008
+ finding: false,
4009
+ channels: /* @__PURE__ */ new Set(),
4010
+ unwatch: () => {
4011
+ }
4012
+ };
4013
+ this.snapshots.set(key, snapshot);
4014
+ if (this.api !== null) {
4015
+ snapshot.unwatch = this.api.watchRevision(workspace, path2, (hash2) => this.moveTo(workspace, path2, snapshot, hash2));
4016
+ }
4017
+ return snapshot;
4018
+ }
4019
+ /** Whether a snapshot is still the runtime's, rather than one a clear or a
4020
+ * re-initialization dropped. */
4021
+ isLive(snapshot) {
4022
+ return this.snapshots.get(snapshot.key) === snapshot;
4023
+ }
4024
+ /** Whether a live snapshot is still pinned to `revision`. */
4025
+ pinnedTo(snapshot, revision) {
4026
+ return this.isLive(snapshot) && snapshot.revision === revision;
4027
+ }
4028
+ /**
4029
+ * The revision a read of `path` is pinned to — a hash, `null` while the
4030
+ * dataset has no value, or `undefined` while it is being found — tracked so
4031
+ * the read re-fires when the source moves.
4032
+ *
4033
+ * @throws The failed lookup's reason, until a read after the retry gap
4034
+ * looks again
4035
+ */
4036
+ revisionFor(workspace, path2) {
4037
+ this.track(pagedRevisionKey(workspace, path2));
4038
+ const snapshot = this.snapshotOf(workspace, path2);
4039
+ if (snapshot.revision !== void 0 || snapshot.finding) return snapshot.revision;
4040
+ if (snapshot.findError !== void 0 && this.now() - (snapshot.findFailedAtMs ?? 0) < RETRY_AFTER_MS) {
4041
+ throw new Error(snapshot.findError);
4042
+ }
4043
+ this.find(workspace, path2, snapshot);
4044
+ return void 0;
4045
+ }
4046
+ /**
4047
+ * Ask the server which content the dataset holds, and move there — unless
4048
+ * a move lands first, which came from something no older than the answer.
4049
+ */
4050
+ find(workspace, path2, snapshot) {
4051
+ const seq = ++snapshot.findSeq;
4052
+ const moves = snapshot.moves;
4053
+ snapshot.finding = true;
4054
+ const api3 = this.api;
4055
+ void (async () => {
4056
+ let revision;
4057
+ try {
4058
+ if (api3 === null) throw new Error("no PagedApi installed");
4059
+ revision = await api3.getRevision(workspace, path2);
4060
+ } catch (err) {
4061
+ if (!this.isLive(snapshot) || snapshot.findSeq !== seq) return;
4062
+ snapshot.finding = false;
4063
+ if (snapshot.revision !== void 0) return;
4064
+ snapshot.findError = failureOf(path2, err);
4065
+ snapshot.findFailedAtMs = this.now();
4066
+ console.error(`Data.bindPaged: could not find which content ${datasetPathToString(path2)} holds:`, err);
4067
+ this.notify(pagedRevisionKey(workspace, path2));
4068
+ return;
4069
+ }
4070
+ if (!this.isLive(snapshot) || snapshot.findSeq !== seq) return;
4071
+ snapshot.finding = false;
4072
+ if (snapshot.moves !== moves) return;
4073
+ this.moveTo(workspace, path2, snapshot, revision);
4074
+ })();
4075
+ }
4076
+ /**
4077
+ * Move a dataset's source to `revision`: drop what the old snapshot
4078
+ * delivered and re-fire every read of the dataset, so the windows and
4079
+ * searches still in use are fetched again, pinned to the new one. A move to
4080
+ * the revision the source already has changes nothing.
4081
+ */
4082
+ moveTo(workspace, path2, snapshot, revision) {
4083
+ if (!this.isLive(snapshot) || snapshot.revision === revision) return;
4084
+ snapshot.revision = revision;
4085
+ snapshot.moves += 1;
4086
+ delete snapshot.findError;
4087
+ delete snapshot.findFailedAtMs;
4088
+ for (const key of snapshot.channels) {
4089
+ this.entries.delete(key);
4090
+ this.loadedWindows.delete(key);
4091
+ }
4092
+ snapshot.channels.clear();
4093
+ this.notify(pagedRevisionKey(workspace, path2));
4094
+ }
4095
+ /**
4096
+ * Handle what a pinned fetch threw when it is not the read's failure: a
4097
+ * refusal naming other content moves the source there, and "no value"
4098
+ * moves it to no snapshot. Returns whether the error was one of those, or
4099
+ * arrived after the source had already moved on.
4100
+ */
4101
+ followed(workspace, path2, snapshot, revision, err) {
4102
+ if (!this.pinnedTo(snapshot, revision)) return true;
4103
+ if (err instanceof DatasetHashMismatchError) {
4104
+ if (err.currentHash !== null && err.currentHash !== revision) {
4105
+ this.moveTo(workspace, path2, snapshot, err.currentHash);
4106
+ return true;
4107
+ }
4108
+ this.find(workspace, path2, snapshot);
4109
+ return false;
4110
+ }
4111
+ const code = errorCode(err);
4112
+ if (code !== void 0 && NO_VALUE_ERRORS.has(code)) {
4113
+ this.moveTo(workspace, path2, snapshot, null);
4114
+ return true;
4115
+ }
4116
+ return false;
4117
+ }
4118
+ // ----- channels ----------------------------------------------------------
4119
+ /** A channel of the snapshot's current revision, dropped when it moves. */
4120
+ channel(snapshot, key) {
4121
+ snapshot.channels.add(key);
4122
+ return this.entry(key);
4123
+ }
4124
+ /** Whether a channel is due a fetch: never fetched, evicted, or failed
4125
+ * for a reason a retry can fix, longer ago than the retry gap. */
4126
+ due(entry) {
4127
+ if (entry.status === "idle") return true;
4128
+ if (entry.status !== "failed" || entry.permanent === true) return false;
4129
+ return this.now() - (entry.failedAtMs ?? 0) >= RETRY_AFTER_MS;
4130
+ }
4131
+ /**
4132
+ * Mark a channel's fetch as started, returning the fetch's sequence number.
4133
+ *
4134
+ * Deliberately does NOT notify — the read that triggers it runs inside a
4135
+ * render pass, and notifying there would re-enter the renderer. Only the
4136
+ * settle notifies.
4137
+ */
4138
+ launch(entry) {
4139
+ entry.status = "running";
4140
+ entry.launchSeq = ++this.launches;
4141
+ return entry.launchSeq;
4142
+ }
4143
+ /** Apply a fetch's outcome if the channel still waits on it, re-firing the
4144
+ * channel's reads. Returns whether it applied. */
4145
+ settle(key, seq, mutate) {
4146
+ const entry = this.entries.get(key);
4147
+ if (entry === void 0 || entry.launchSeq !== seq) return false;
4148
+ mutate(entry);
4149
+ this.notify(key);
4150
+ return true;
4151
+ }
4152
+ /** Settle a fetch as failed, with the reason a read will throw. */
4153
+ fail(key, seq, reason, permanent) {
4154
+ this.settle(key, seq, (e3) => {
4155
+ e3.status = "failed";
4156
+ e3.error = reason;
4157
+ e3.failedAtMs = this.now();
4158
+ e3.permanent = permanent;
4159
+ });
4160
+ }
4161
+ /** What a read of a channel answers: its value once loaded, `none` while
4162
+ * in flight, or its failure, thrown. */
4163
+ answer(entry, value) {
4164
+ if (entry.status === "loaded" && value !== void 0) return some(value);
4165
+ if (entry.status === "failed") throw new Error(entry.error ?? "Data.bindPaged: the read failed");
4166
+ return none$1;
4167
+ }
3792
4168
  /** Note a window as most-recently-read, and drop the coldest decoded
3793
4169
  * windows once the cache exceeds its cap. An evicted window returns to
3794
4170
  * `idle`, so the next read refetches it rather than reading a hole. */
@@ -3806,131 +4182,83 @@ class PagedRuntime extends TrackedChannelStore {
3806
4182
  delete entry.window;
3807
4183
  }
3808
4184
  }
3809
- /**
3810
- * Start the fetch for a window if it isn't loaded or already in flight.
3811
- *
3812
- * Deliberately does NOT notify on launch — the read that triggers it runs
3813
- * inside a render pass, and notifying there would re-enter the renderer.
3814
- * Only the settle notifies.
3815
- */
3816
- ensureWindow(sourceType, workspace, path2, offset, limit2) {
3817
- const key = pagedWindowKey(workspace, path2, offset, limit2);
3818
- const entry = this.entry(key);
3819
- if (entry.status === "running" || entry.status === "loaded") return;
3820
- if (entry.status === "failed") {
3821
- if (entry.permanent) return;
3822
- const since = this.now() - (entry.failedAtMs ?? 0);
3823
- if (since < RETRY_AFTER_MS) return;
3824
- }
3825
- entry.status = "running";
3826
- entry.launchSeq += 1;
3827
- const mySeq = entry.launchSeq;
4185
+ // ----- window loading --------------------------------------------------
4186
+ /** Start the pinned fetch for a window if it is due one, returning its
4187
+ * channel. */
4188
+ ensureWindow(sourceType, workspace, path2, snapshot, revision, offset, limit2, selector2, key) {
4189
+ const entry = this.channel(snapshot, key);
4190
+ if (!this.due(entry)) return entry;
4191
+ const seq = this.launch(entry);
3828
4192
  const api3 = this.api;
3829
4193
  void (async () => {
3830
- const settle = (mutate) => {
3831
- const current = this.entries.get(key);
3832
- if (!current || current.launchSeq !== mySeq) return;
3833
- mutate(current);
3834
- this.notify(key);
3835
- };
3836
- if (!api3) {
3837
- settle((e3) => {
3838
- e3.status = "failed";
3839
- e3.failedAtMs = this.now();
3840
- });
3841
- console.error("Data.bindPaged: no PagedApi installed");
3842
- return;
3843
- }
3844
4194
  let page;
3845
4195
  try {
3846
- page = await api3.getPage(workspace, path2, { offset, limit: limit2 });
3847
- } catch (err) {
3848
- const permanent = isPermanentPageError(err);
3849
- settle((e3) => {
3850
- e3.status = "failed";
3851
- e3.failedAtMs = this.now();
3852
- e3.permanent = permanent;
4196
+ if (api3 === null) throw new Error("no PagedApi installed");
4197
+ page = await api3.getPage(workspace, path2, {
4198
+ offset,
4199
+ limit: limit2,
4200
+ hash: revision,
4201
+ ...selector2.index !== null && { index: selector2.index, join: selector2.join }
3853
4202
  });
3854
- console.error(
3855
- 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}:`,
3856
- err
3857
- );
4203
+ } catch (err) {
4204
+ if (this.followed(workspace, path2, snapshot, revision, err)) return;
4205
+ this.fail(key, seq, failureOf(path2, err), isPermanentPageError(err));
4206
+ console.error(`Data.bindPaged: fetch failed for ${key}:`, err);
3858
4207
  return;
3859
4208
  }
4209
+ if (!this.pinnedTo(snapshot, revision)) return;
3860
4210
  let decoded;
3861
4211
  try {
3862
4212
  decoded = decodeBeast2For(sourceType)(page.data);
3863
4213
  } catch (err) {
3864
- settle((e3) => {
3865
- e3.status = "failed";
3866
- e3.failedAtMs = this.now();
3867
- });
4214
+ this.fail(key, seq, `${failureOf(path2, err)} (the window does not decode as the source's type)`, true);
3868
4215
  console.error(`Data.bindPaged: decode failed for ${key}:`, err);
3869
4216
  return;
3870
4217
  }
3871
- settle((e3) => {
4218
+ const landed = this.settle(key, seq, (e3) => {
3872
4219
  e3.status = "loaded";
3873
4220
  e3.window = decoded;
3874
4221
  e3.total = page.totalElements;
4222
+ delete e3.error;
3875
4223
  });
4224
+ if (!landed) return;
3876
4225
  this.touchWindow(key);
3877
- const totalKey = pagedTotalKey(workspace, path2);
3878
- const totalEntry = this.entry(totalKey);
4226
+ const totalKey = pagedTotalKey(workspace, path2, revision, selector2);
4227
+ const totalEntry = this.channel(snapshot, totalKey);
3879
4228
  if (totalEntry.total !== page.totalElements) {
3880
4229
  totalEntry.total = page.totalElements;
3881
4230
  totalEntry.status = "loaded";
3882
4231
  this.notify(totalKey);
3883
4232
  }
3884
4233
  })();
4234
+ return entry;
3885
4235
  }
3886
- /**
3887
- * Start the fence search for one key query if it isn't answered or already
3888
- * in flight — the seek sibling of {@link ensureWindow}, with the same
3889
- * launch-does-not-notify rule (the read runs inside a render pass).
3890
- */
3891
- ensureSeek(workspace, path2, query, key) {
3892
- const entry = this.entry(key);
3893
- if (entry.status === "running" || entry.status === "loaded") return;
3894
- if (entry.status === "failed") {
3895
- if (entry.permanent) return;
3896
- const since = this.now() - (entry.failedAtMs ?? 0);
3897
- if (since < RETRY_AFTER_MS) return;
3898
- }
3899
- entry.status = "running";
3900
- entry.launchSeq += 1;
3901
- const mySeq = entry.launchSeq;
4236
+ /** Start the pinned fence search for one key query if it is due one,
4237
+ * returning its channel — the seek sibling of {@link ensureWindow}. */
4238
+ ensureSeek(workspace, path2, snapshot, revision, query, key) {
4239
+ const entry = this.channel(snapshot, key);
4240
+ if (!this.due(entry)) return entry;
4241
+ const seq = this.launch(entry);
3902
4242
  const api3 = this.api;
3903
4243
  void (async () => {
3904
- const settle = (mutate) => {
3905
- const current = this.entries.get(key);
3906
- if (!current || current.launchSeq !== mySeq) return;
3907
- mutate(current);
3908
- this.notify(key);
3909
- };
3910
- if (!api3) {
3911
- settle((e3) => {
3912
- e3.status = "failed";
3913
- e3.failedAtMs = this.now();
3914
- });
3915
- console.error("Data.bindPaged: no PagedApi installed");
3916
- return;
3917
- }
4244
+ let range;
3918
4245
  try {
3919
- const range = await api3.findKey(workspace, path2, query);
3920
- settle((e3) => {
3921
- e3.status = "loaded";
3922
- e3.range = range;
3923
- });
4246
+ if (api3 === null) throw new Error("no PagedApi installed");
4247
+ range = await api3.findKey(workspace, path2, { ...query, hash: revision });
3924
4248
  } catch (err) {
3925
- const permanent = isPermanentPageError(err);
3926
- settle((e3) => {
3927
- e3.status = "failed";
3928
- e3.failedAtMs = this.now();
3929
- e3.permanent = permanent;
3930
- });
4249
+ if (this.followed(workspace, path2, snapshot, revision, err)) return;
4250
+ this.fail(key, seq, failureOf(path2, err), isPermanentPageError(err));
3931
4251
  console.error(`Data.bindPaged: key search failed for ${key}:`, err);
4252
+ return;
3932
4253
  }
4254
+ if (!this.pinnedTo(snapshot, revision)) return;
4255
+ this.settle(key, seq, (e3) => {
4256
+ e3.status = "loaded";
4257
+ e3.range = range;
4258
+ delete e3.error;
4259
+ });
3933
4260
  })();
4261
+ return entry;
3934
4262
  }
3935
4263
  /**
3936
4264
  * The low-level primitives backing handle methods, bound to THIS runtime.
@@ -3940,53 +4268,92 @@ class PagedRuntime extends TrackedChannelStore {
3940
4268
  */
3941
4269
  buildPrimitives() {
3942
4270
  return [
3943
- DataPagedPrimitives.page.implement((sourceType) => (pathArg, offsetArg, limitArg) => {
4271
+ DataPagedPrimitives.page.implement((sourceType) => (pathArg, indexArg, joinArg, offsetArg, limitArg) => {
3944
4272
  const workspace = this.resolveWorkspace();
3945
4273
  const path2 = pathArg;
4274
+ const revision = this.revisionFor(workspace, path2);
4275
+ if (typeof revision !== "string") return none$1;
4276
+ const selector2 = toSelector(indexArg, joinArg);
3946
4277
  const offset = Number(offsetArg);
3947
4278
  const limit2 = Number(limitArg);
3948
- const key = pagedWindowKey(workspace, path2, offset, limit2);
4279
+ const key = pagedWindowKey(workspace, path2, revision, offset, limit2, selector2);
3949
4280
  this.track(key);
3950
- this.ensureWindow(sourceType, workspace, path2, offset, limit2);
3951
- const entry = this.entry(key);
3952
- if (entry.status === "loaded" && entry.window !== void 0) {
3953
- this.touchWindow(key);
3954
- return variant("some", entry.window);
3955
- }
3956
- return variant("none", null);
4281
+ const entry = this.ensureWindow(
4282
+ sourceType,
4283
+ workspace,
4284
+ path2,
4285
+ this.snapshotOf(workspace, path2),
4286
+ revision,
4287
+ offset,
4288
+ limit2,
4289
+ selector2,
4290
+ key
4291
+ );
4292
+ if (entry.status === "loaded" && entry.window !== void 0) this.touchWindow(key);
4293
+ return this.answer(entry, entry.window);
3957
4294
  }),
3958
- DataPagedPrimitives.total.implement((_sourceType) => (pathArg) => {
4295
+ DataPagedPrimitives.total.implement((_sourceType) => (pathArg, indexArg, joinArg) => {
3959
4296
  const workspace = this.resolveWorkspace();
3960
- const key = pagedTotalKey(workspace, pathArg);
4297
+ const path2 = pathArg;
4298
+ const revision = this.revisionFor(workspace, path2);
4299
+ if (typeof revision !== "string") return none$1;
4300
+ const key = pagedTotalKey(workspace, path2, revision, toSelector(indexArg, joinArg));
3961
4301
  this.track(key);
3962
- const entry = this.entry(key);
3963
- return entry.total !== void 0 ? variant("some", BigInt(entry.total)) : variant("none", null);
4302
+ const entry = this.channel(this.snapshotOf(workspace, path2), key);
4303
+ return entry.total !== void 0 ? some(BigInt(entry.total)) : none$1;
3964
4304
  }),
3965
- DataPagedPrimitives.seek.implement((_sourceType) => (pathArg, queryArg) => {
4305
+ DataPagedPrimitives.seek.implement((_sourceType) => (pathArg, indexArg, joinArg, queryArg) => {
3966
4306
  const workspace = this.resolveWorkspace();
3967
4307
  const path2 = pathArg;
3968
- const query = toFindQuery(queryArg);
3969
- const key = pagedSeekKey(workspace, path2, query);
4308
+ const revision = this.revisionFor(workspace, path2);
4309
+ if (typeof revision !== "string") return none$1;
4310
+ const selector2 = toSelector(indexArg, joinArg);
4311
+ const query = toFindQuery(queryArg, selector2);
4312
+ const key = pagedSeekKey(workspace, path2, revision, query, selector2);
3970
4313
  this.track(key);
3971
- this.ensureSeek(workspace, path2, query, key);
3972
- const entry = this.entry(key);
3973
- if (entry.status !== "loaded" || entry.range === void 0) return none$1;
3974
- const r2 = entry.range;
3975
- return some({ found: r2.found, row: BigInt(r2.row), count: BigInt(r2.count) });
4314
+ const entry = this.ensureSeek(workspace, path2, this.snapshotOf(workspace, path2), revision, query, key);
4315
+ const range = entry.range;
4316
+ return this.answer(entry, range === void 0 ? void 0 : { found: range.found, row: BigInt(range.row), count: BigInt(range.count) });
4317
+ }),
4318
+ DataPagedPrimitives.revision.implement((_sourceType) => (pathArg, _indexArg, _joinArg) => {
4319
+ const revision = this.revisionFor(this.resolveWorkspace(), pathArg);
4320
+ return typeof revision === "string" ? some(revision) : none$1;
4321
+ }),
4322
+ DataPagedPrimitives.refresh.implement((_sourceType) => (pathArg, _indexArg, _joinArg, targetArg) => {
4323
+ const workspace = this.resolveWorkspace();
4324
+ const path2 = pathArg;
4325
+ const target = targetArg;
4326
+ queueMicrotask(() => {
4327
+ if (this.workspace !== workspace) return;
4328
+ const snapshot = this.snapshotOf(workspace, path2);
4329
+ if (target.type === "some") this.moveTo(workspace, path2, snapshot, target.value);
4330
+ else this.find(workspace, path2, snapshot);
4331
+ });
4332
+ return null;
3976
4333
  })
3977
4334
  ];
3978
4335
  }
3979
4336
  /**
3980
- * Build the handle value for one `Data.bindPaged` evaluation. Both methods
3981
- * are thin IR-bearing `East.function`s over {@link buildPrimitives},
3982
- * capturing only the plain-data source path (the value type rides as a
3983
- * type-arg) — so the handle is ordinary serializable East data (issue #106).
4337
+ * Build the handle value for one paged bind. Every method is a thin
4338
+ * IR-bearing `East.function` over {@link buildPrimitives}, capturing only
4339
+ * the plain-data source path and selector (the value type rides as a
4340
+ * type-arg) — so the handle is ordinary serializable East data (issue
4341
+ * #106).
4342
+ *
4343
+ * @param sourceType - The dataset's type, or an index's window type
4344
+ * @param path - The dataset's path
4345
+ * @param selector - The rows the bind serves
4346
+ * @param shape - `released`: the handle `data_bind_paged` returns, without
4347
+ * `revision` or `refresh`; `pinned`: the handle `data_bind_paged_pinned`
4348
+ * returns. Both read through the same snapshot.
4349
+ * @returns The handle
3984
4350
  */
3985
- buildHandle(sourceType, path2) {
3986
- const pathKey = datasetPathToString(path2);
4351
+ buildHandle(sourceType, path2, selector2, shape) {
4352
+ const id3 = `${datasetPathToString(path2)}${selectorKey(selector2)}`;
4353
+ const cacheKey = `${id3}#${shape}`;
3987
4354
  let byPath = this.handleCache.get(sourceType);
3988
4355
  if (byPath) {
3989
- const hit = byPath.get(pathKey);
4356
+ const hit = byPath.get(cacheKey);
3990
4357
  if (hit) return hit;
3991
4358
  } else {
3992
4359
  byPath = /* @__PURE__ */ new Map();
@@ -3994,24 +4361,27 @@ class PagedRuntime extends TrackedChannelStore {
3994
4361
  }
3995
4362
  const T2 = fromEastTypeValue(sourceType);
3996
4363
  const pathExpr = East.value(path2, TreePathType);
4364
+ const indexExpr = East.value(selector2.index === null ? none$1 : some(selector2.index), OptionType(StringType));
4365
+ const joinExpr = East.value(selector2.join, BooleanType);
3997
4366
  const platform = this.buildPrimitives();
3998
- const { page, total } = DataPagedPrimitives;
4367
+ const { page, total, revision, refresh } = DataPagedPrimitives;
3999
4368
  const handle = {
4000
4369
  // The comparable identity east-ui's `PagedSourceType` requires:
4001
4370
  // East compares every function as EQUAL, so a struct of nothing but
4002
4371
  // closures is indistinguishable from any other and a memoized
4003
4372
  // component would never re-render on a source swap (#567 D4). The
4004
- // dataset path is the natural identity — same path, same rows.
4005
- id: pathKey,
4373
+ // dataset path and selector are the natural identity — same path,
4374
+ // same rows. The snapshot they serve is `revision`'s, not the id's.
4375
+ id: id3,
4006
4376
  page: East.compile(
4007
4377
  East.function([IntegerType, IntegerType], OptionType(T2), ($3, offset, limit2) => {
4008
- $3.return(page([T2], pathExpr, offset, limit2));
4378
+ $3.return(page([T2], pathExpr, indexExpr, joinExpr, offset, limit2));
4009
4379
  }),
4010
4380
  platform
4011
4381
  ),
4012
4382
  total: East.compile(
4013
4383
  East.function([], OptionType(IntegerType), ($3) => {
4014
- $3.return(total([T2], pathExpr));
4384
+ $3.return(total([T2], pathExpr, indexExpr, joinExpr));
4015
4385
  }),
4016
4386
  platform
4017
4387
  ),
@@ -4020,28 +4390,50 @@ class PagedRuntime extends TrackedChannelStore {
4020
4390
  // segments) against the stored fences; an Array's stream order has
4021
4391
  // nothing to search. Resolved at bind time from the dataset's own
4022
4392
  // type, so a component renders the affordance only when it works.
4023
- seek: buildSeek(sourceType, T2, pathExpr, platform)
4393
+ seek: buildSeek(sourceType, T2, pathExpr, indexExpr, joinExpr, selector2, platform)
4024
4394
  };
4025
- byPath.set(pathKey, handle);
4395
+ if (shape === "pinned") {
4396
+ handle.revision = East.compile(
4397
+ East.function([], OptionType(StringType), ($3) => {
4398
+ $3.return(revision([T2], pathExpr, indexExpr, joinExpr));
4399
+ }),
4400
+ platform
4401
+ );
4402
+ handle.refresh = East.compile(
4403
+ East.function([OptionType(StringType)], NullType, ($3, target) => {
4404
+ $3.return(refresh([T2], pathExpr, indexExpr, joinExpr, target));
4405
+ }),
4406
+ platform
4407
+ );
4408
+ }
4409
+ byPath.set(cacheKey, handle);
4026
4410
  return handle;
4027
4411
  }
4028
4412
  // ----- platform building -------------------------------------------------
4029
- /** Build a `Data.bindPaged` PlatformFunction bound to this runtime. Pass
4030
- * `allowed=null` for an unscoped impl; pass a Set of path strings for
4031
- * manifest scoping. */
4413
+ /** Build the PlatformFunction behind `data_bind_paged` — the bind a UI
4414
+ * exported before pinned reads calls — bound to this runtime. Its handle
4415
+ * has no `revision` or `refresh`, but its reads are pinned, follow the
4416
+ * dataset and fail visibly all the same. Pass `allowed=null` for an
4417
+ * unscoped impl, or a Set of path strings for manifest scoping. */
4032
4418
  buildPlatform(allowed) {
4033
4419
  return bindPagedPlatformFn.implement(
4034
4420
  (sourceType) => (pathArg) => {
4035
4421
  const path2 = pathArg;
4036
- if (allowed) {
4037
- const pathStr = datasetPathToString(path2);
4038
- if (!allowed.has(pathStr)) {
4039
- throw new Error(
4040
- `Data.bindPaged: source path "${pathStr}" not declared in manifest — bind it in the task body so derivation records it`
4041
- );
4042
- }
4043
- }
4044
- return this.buildHandle(sourceType, path2);
4422
+ checkDeclared(allowed, path2);
4423
+ return this.buildHandle(sourceType, path2, NO_INDEX, "released");
4424
+ }
4425
+ );
4426
+ }
4427
+ /** Build the PlatformFunction behind `data_bind_paged_pinned` — what
4428
+ * `Data.bindPaged` emits, for a dataset's own rows or a record's index —
4429
+ * bound to this runtime and scoped by `allowed` exactly as
4430
+ * {@link buildPlatform} is: an index read is a read of the record's path. */
4431
+ buildPinnedPlatform(allowed) {
4432
+ return bindPagedPinnedPlatformFn.implement(
4433
+ (sourceType) => (pathArg, indexArg, joinArg) => {
4434
+ const path2 = pathArg;
4435
+ checkDeclared(allowed, path2);
4436
+ return this.buildHandle(sourceType, path2, toSelector(indexArg, joinArg), "pinned");
4045
4437
  }
4046
4438
  );
4047
4439
  }
@@ -4050,17 +4442,19 @@ const defaultPagedRuntime = new PagedRuntime();
4050
4442
  function initializePagedApi(api3, workspace) {
4051
4443
  defaultPagedRuntime.initialize(api3, workspace);
4052
4444
  }
4053
- function clearPagedApi() {
4054
- defaultPagedRuntime.clear();
4445
+ function clearPagedApi(api3) {
4446
+ defaultPagedRuntime.clear(api3);
4055
4447
  }
4056
4448
  const PagedPlatform = [
4057
4449
  defaultPagedRuntime.buildPlatform(null),
4450
+ defaultPagedRuntime.buildPinnedPlatform(null),
4058
4451
  ...defaultPagedRuntime.buildPrimitives()
4059
4452
  ];
4060
4453
  function createScopedPagedPlatform(pages) {
4061
4454
  const allowed = new Set(pages.map((p2) => datasetPathToString(p2)));
4062
4455
  return [
4063
4456
  defaultPagedRuntime.buildPlatform(allowed),
4457
+ defaultPagedRuntime.buildPinnedPlatform(allowed),
4064
4458
  ...defaultPagedRuntime.buildPrimitives()
4065
4459
  ];
4066
4460
  }
@@ -4186,8 +4580,15 @@ class FuncRuntime extends TrackedChannelStore {
4186
4580
  this.api = api3;
4187
4581
  this.workspace = workspace;
4188
4582
  }
4189
- /** Tear down the adapter and all call state. */
4190
- clear() {
4583
+ /**
4584
+ * Tear down the adapter and all call state.
4585
+ *
4586
+ * @param api - Clear only while this is the installed adapter, so a
4587
+ * provider tearing down after another installed its own leaves that one
4588
+ * in place
4589
+ */
4590
+ clear(api3) {
4591
+ if (api3 !== void 0 && this.api !== api3) return;
4191
4592
  this.api = null;
4192
4593
  this.workspace = null;
4193
4594
  this.clearChannels();
@@ -4420,8 +4821,8 @@ const defaultFuncRuntime = new FuncRuntime();
4420
4821
  function initializeFunctionApi(api3, workspace) {
4421
4822
  defaultFuncRuntime.initialize(api3, workspace);
4422
4823
  }
4423
- function clearFunctionApi() {
4424
- defaultFuncRuntime.clear();
4824
+ function clearFunctionApi(api3) {
4825
+ defaultFuncRuntime.clear(api3);
4425
4826
  }
4426
4827
  const FuncPlatform = [
4427
4828
  defaultFuncRuntime.buildPlatform(null),
@@ -8194,12 +8595,6 @@ function errorOfMutationResult(result) {
8194
8595
  message: `reducer exited with code ${outcome.value.exitCode}`,
8195
8596
  stderr: outcome.value.stderr
8196
8597
  };
8197
- case "too_large":
8198
- return {
8199
- kind: variant("too_large", { bytes: outcome.value.bytes, limit: outcome.value.limit }),
8200
- message: `new state too large (${outcome.value.bytes} bytes, limit ${outcome.value.limit})`,
8201
- stderr: outcome.value.stderr
8202
- };
8203
8598
  case "timed_out":
8204
8599
  return {
8205
8600
  kind: variant("timed_out", { ms: outcome.value.ms }),
@@ -8259,8 +8654,15 @@ class RecordRuntime extends TrackedChannelStore {
8259
8654
  this.cache = cache3;
8260
8655
  this.workspace = workspace;
8261
8656
  }
8262
- /** Tear down the adapter and all record state. */
8263
- clear() {
8657
+ /**
8658
+ * Tear down the adapter and all record state.
8659
+ *
8660
+ * @param api - Clear only while this is the installed adapter, so a
8661
+ * provider tearing down after another installed its own leaves that one
8662
+ * in place
8663
+ */
8664
+ clear(api3) {
8665
+ if (api3 !== void 0 && this.api !== api3) return;
8264
8666
  this.api = null;
8265
8667
  this.cache = null;
8266
8668
  this.workspace = null;
@@ -8568,8 +8970,8 @@ const defaultRecordRuntime = new RecordRuntime();
8568
8970
  function initializeRecordApi(api3, cache3, workspace) {
8569
8971
  defaultRecordRuntime.initialize(api3, cache3, workspace);
8570
8972
  }
8571
- function clearRecordApi() {
8572
- defaultRecordRuntime.clear();
8973
+ function clearRecordApi(api3) {
8974
+ defaultRecordRuntime.clear(api3);
8573
8975
  }
8574
8976
  const RecordPlatform = [
8575
8977
  defaultRecordRuntime.buildPlatform(null),
@@ -8595,7 +8997,8 @@ function createInMemoryRecordApi(cache3, workspace, defs2) {
8595
8997
  state: `${def.name}-state-0`.padEnd(64, "0"),
8596
8998
  mutation: "$init",
8597
8999
  actor: "memory",
8598
- at: /* @__PURE__ */ new Date(0)
9000
+ at: /* @__PURE__ */ new Date(0),
9001
+ delta: none$1
8599
9002
  };
8600
9003
  compiled.set(def.name, { stateType, mutations, commits: [genesis], seq: 0 });
8601
9004
  void cache3.write(workspace, recordPath(def.name), encodeBeast2For(stateType)(def.initial));
@@ -8627,7 +9030,8 @@ function createInMemoryRecordApi(cache3, workspace, defs2) {
8627
9030
  state: `${record}-state-${c2.seq}`.padEnd(64, "0"),
8628
9031
  mutation,
8629
9032
  actor: "memory",
8630
- at: /* @__PURE__ */ new Date(0)
9033
+ at: /* @__PURE__ */ new Date(0),
9034
+ delta: none$1
8631
9035
  });
8632
9036
  return { outcome: variant("committed", { commitHash: hash2, stateHash: `${record}-state-${c2.seq}`.padEnd(64, "0") }) };
8633
9037
  },
@@ -8676,68 +9080,49 @@ function ReactiveDatasetProvider({
8676
9080
  children: children2
8677
9081
  }) {
8678
9082
  const e3 = useE3Config();
9083
+ const { apiUrl, workspace } = e3;
9084
+ const repo = e3.repo ?? "default";
8679
9085
  const tokenRef = useRef(e3.token ?? null);
8680
9086
  tokenRef.current = e3.token ?? null;
8681
- const api3 = useMemo(
8682
- () => createDefaultDatasetApi(e3.apiUrl, e3.repo ?? "default", () => tokenRef.current),
8683
- [e3.apiUrl, e3.repo]
8684
- );
8685
- const cache3 = useMemo(() => {
8686
- const cfg = {};
8687
- if (e3.workspace !== void 0) cfg.workspace = e3.workspace;
8688
- return new ReactiveDatasetCache(cfg, api3);
8689
- }, [api3, e3.workspace]);
8690
- useMemo(() => {
8691
- initializeReactiveDatasetCache(cache3);
8692
- cache3.setScheduler((notify3) => queueMicrotask(notify3));
8693
- }, [cache3]);
8694
- useMemo(() => {
8695
- if (e3.workspace !== void 0) {
8696
- initializeFunctionApi(
8697
- createDefaultFunctionApi(e3.apiUrl, e3.repo ?? "default", () => tokenRef.current),
8698
- e3.workspace
8699
- );
8700
- }
8701
- }, [e3.apiUrl, e3.repo, e3.workspace]);
8702
- useMemo(() => {
8703
- if (e3.workspace !== void 0) {
8704
- initializePagedApi(
8705
- createDefaultPagedApi(e3.apiUrl, e3.repo ?? "default", () => tokenRef.current),
8706
- e3.workspace
8707
- );
8708
- }
8709
- }, [e3.apiUrl, e3.repo, e3.workspace]);
8710
- useMemo(() => {
8711
- if (e3.workspace !== void 0) {
8712
- initializeRecordApi(
8713
- createDefaultRecordApi(e3.apiUrl, e3.repo ?? "default", () => tokenRef.current),
8714
- cache3,
8715
- e3.workspace
8716
- );
8717
- }
8718
- }, [e3.apiUrl, e3.repo, e3.workspace, cache3]);
9087
+ const [installed, setInstalled] = useState(null);
8719
9088
  useEffect(() => {
9089
+ const getToken = () => tokenRef.current;
9090
+ const cache22 = new ReactiveDatasetCache(
9091
+ workspace !== void 0 ? { workspace } : {},
9092
+ createDefaultDatasetApi(apiUrl, repo, getToken)
9093
+ );
9094
+ cache22.setScheduler((notify3) => queueMicrotask(notify3));
9095
+ initializeReactiveDatasetCache(cache22);
9096
+ const functionApi = createDefaultFunctionApi(apiUrl, repo, getToken);
9097
+ const pagedApi = createDefaultPagedApi(apiUrl, repo, getToken, cache22);
9098
+ const recordApi = createDefaultRecordApi(apiUrl, repo, getToken);
9099
+ if (workspace !== void 0) {
9100
+ initializeFunctionApi(functionApi, workspace);
9101
+ initializePagedApi(pagedApi, workspace);
9102
+ initializeRecordApi(recordApi, cache22, workspace);
9103
+ }
9104
+ const unscopeStaged = getStagedStore().setScope(JSON.stringify([apiUrl, repo]));
9105
+ setInstalled({ apiUrl, repo, workspace, cache: cache22 });
8720
9106
  return () => {
8721
- const ws = cache3.getConfig().workspace;
8722
- clearReactiveDatasetCache();
8723
- clearFunctionApi();
8724
- clearPagedApi();
8725
- clearRecordApi();
8726
- cache3.destroy();
8727
- if (ws) clearBindingRegistry(ws);
9107
+ clearReactiveDatasetCache(cache22);
9108
+ clearFunctionApi(functionApi);
9109
+ clearPagedApi(pagedApi);
9110
+ clearRecordApi(recordApi);
9111
+ unscopeStaged();
9112
+ cache22.destroy();
9113
+ if (workspace) clearBindingRegistry(workspace);
8728
9114
  else clearBindingRegistry();
8729
9115
  };
8730
- }, [cache3]);
9116
+ }, [apiUrl, repo, workspace]);
9117
+ const cache3 = installed !== null && installed.apiUrl === apiUrl && installed.repo === repo && installed.workspace === workspace ? installed.cache : null;
8731
9118
  useEffect(() => {
8732
- if (typeof window !== "undefined") {
8733
- window.__EAST_REACTIVE_DATASET_CACHE__ = cache3;
8734
- }
9119
+ if (cache3 === null || typeof window === "undefined") return;
9120
+ window.__EAST_REACTIVE_DATASET_CACHE__ = cache3;
8735
9121
  return () => {
8736
- if (typeof window !== "undefined") {
8737
- delete window.__EAST_REACTIVE_DATASET_CACHE__;
8738
- }
9122
+ delete window.__EAST_REACTIVE_DATASET_CACHE__;
8739
9123
  };
8740
9124
  }, [cache3]);
9125
+ if (cache3 === null) return null;
8741
9126
  return /* @__PURE__ */ jsx$1(ReactiveDatasetCacheContext.Provider, { value: cache3, children: children2 });
8742
9127
  }
8743
9128
  function useReactiveDatasetCache() {
@@ -64189,6 +64574,7 @@ function useDatasetValue(apiUrl, repo, workspace, datasetPath, options2) {
64189
64574
  const { requestOptions, queryOverrides, enabled = true, hash: hash2, platforms, type } = options2;
64190
64575
  const platformImpls = platforms ?? defaultPlatformImplementations;
64191
64576
  const reqOpts = requestOptions ?? { token: null };
64577
+ const queryClient = useQueryClient();
64192
64578
  const pathParts = useMemo(
64193
64579
  () => (datasetPath == null ? void 0 : datasetPath.split(".").filter(Boolean).map((v3) => variant("field", v3))) ?? [],
64194
64580
  [datasetPath]
@@ -64198,8 +64584,14 @@ function useDatasetValue(apiUrl, repo, workspace, datasetPath, options2) {
64198
64584
  queryFn: async () => {
64199
64585
  const result = await datasetGet(apiUrl, repo, workspace, pathParts, reqOpts);
64200
64586
  const decoded = decodeBeast2For(type, { platform: platformImpls })(result.data);
64201
- return { decoded, sizeBytes: result.data.length };
64587
+ const value = { decoded, sizeBytes: result.data.length, hash: result.hash ?? hash2 ?? null };
64588
+ if (value.hash !== hash2) {
64589
+ queryClient.setQueryData(["datasetValue", apiUrl, repo, workspace, datasetPath, value.hash], value);
64590
+ void queryClient.invalidateQueries({ queryKey: ["datasetStatus", apiUrl, repo, workspace, datasetPath] });
64591
+ }
64592
+ return value;
64202
64593
  },
64594
+ select: (value) => value.hash === (hash2 ?? null) ? value : void 0,
64203
64595
  enabled: enabled && !!workspace && !!datasetPath && hash2 != null,
64204
64596
  ...queryOverrides
64205
64597
  });
@@ -64265,14 +64657,6 @@ function useTaskDetails(apiUrl, repo, workspace, taskName, options2) {
64265
64657
  ...options2 == null ? void 0 : options2.queryOverrides
64266
64658
  });
64267
64659
  }
64268
- function getTaskKind(details) {
64269
- var _a2;
64270
- return ((_a2 = details.kind) == null ? void 0 : _a2.type) === "some" ? details.kind.value : null;
64271
- }
64272
- function getTaskMetadata(details) {
64273
- var _a2;
64274
- return ((_a2 = details.metadata) == null ? void 0 : _a2.type) === "some" ? details.metadata.value : null;
64275
- }
64276
64660
  const variantStyles = {
64277
64661
  // Mode-aware semantic tokens (the raw `red.50` / `gray.50` washes rendered
64278
64662
  // as bright light boxes in dark mode — #362).
@@ -64456,17 +64840,21 @@ const PagedDatasetPreview = memo(function PagedDatasetPreview2({
64456
64840
  const [error3, setError] = useState(null);
64457
64841
  const [jumpRow, setJumpRow] = useState(void 0);
64458
64842
  const inflightRef = useRef(/* @__PURE__ */ new Set());
64843
+ const shownRef = useRef({ path: path2, hash: hash2 });
64459
64844
  useEffect(() => {
64460
64845
  pagingDebug(`reset (path=${path2}, hash=${hash2.slice(0, 8)})`);
64846
+ shownRef.current = { path: path2, hash: hash2 };
64461
64847
  setPages(/* @__PURE__ */ new Map());
64462
64848
  setTotals(null);
64463
64849
  setError(null);
64464
64850
  setJumpRow(void 0);
64465
- inflightRef.current.clear();
64851
+ inflightRef.current = /* @__PURE__ */ new Set();
64466
64852
  }, [path2, hash2]);
64467
64853
  const loadPage = useCallback((pageIdx) => {
64468
- if (inflightRef.current.has(pageIdx)) return;
64469
- inflightRef.current.add(pageIdx);
64854
+ const inflight = inflightRef.current;
64855
+ if (inflight.has(pageIdx)) return;
64856
+ inflight.add(pageIdx);
64857
+ const current = () => shownRef.current.path === path2 && shownRef.current.hash === hash2;
64470
64858
  setLoadingCount((n2) => n2 + 1);
64471
64859
  pagingDebug(`fetch p${pageIdx} (offset=${pageIdx * PAGE_SIZE}, limit=${PAGE_SIZE}, hash=${hash2.slice(0, 8)})`);
64472
64860
  const pathParts2 = path2.split(".").filter(Boolean).map((v3) => variant("field", v3));
@@ -64479,6 +64867,7 @@ const PagedDatasetPreview = memo(function PagedDatasetPreview2({
64479
64867
  staleTime: Infinity
64480
64868
  // pages of one content hash are immutable
64481
64869
  }).then((page) => {
64870
+ if (!current()) return;
64482
64871
  const decoded = decodeBeast2For(type)(page.data);
64483
64872
  const rows = pageRows(type, decoded, page.offset);
64484
64873
  pagingDebug(
@@ -64492,12 +64881,17 @@ const PagedDatasetPreview = memo(function PagedDatasetPreview2({
64492
64881
  setPages((prev2) => new Map(prev2).set(pageIdx, rows));
64493
64882
  }).catch((err) => {
64494
64883
  pagingDebug(`p${pageIdx} FAILED:`, err);
64884
+ if (!current()) return;
64885
+ if (err instanceof DatasetHashMismatchError) {
64886
+ void queryClient.invalidateQueries({ queryKey: ["datasetStatus", apiUrl, repo, workspace, path2] });
64887
+ return;
64888
+ }
64495
64889
  if (err instanceof ApiError && err.code === "dataset_not_indexed") {
64496
64890
  onNotIndexed == null ? void 0 : onNotIndexed();
64497
64891
  }
64498
64892
  setError(err instanceof Error ? err : new Error(String(err)));
64499
64893
  }).finally(() => {
64500
- inflightRef.current.delete(pageIdx);
64894
+ inflight.delete(pageIdx);
64501
64895
  setLoadingCount((n2) => n2 - 1);
64502
64896
  });
64503
64897
  }, [queryClient, apiUrl, repo, workspace, path2, hash2, requestOptions, type, onNotIndexed]);
@@ -64594,7 +64988,10 @@ const PagedDatasetPreview = memo(function PagedDatasetPreview2({
64594
64988
  ] }),
64595
64989
  /* @__PURE__ */ jsx$1(Box, { flex: 1, minHeight: 0, overflow: "hidden", children: /* @__PURE__ */ jsx$1(EastChakraValueTree, { value: treeValue, storageKey: `${path2}:page`, paging }) })
64596
64990
  ] });
64597
- }, (prev2, next2) => prev2.path === next2.path && prev2.workspace === next2.workspace && prev2.hash === next2.hash && prev2.sizeBytes === next2.sizeBytes);
64991
+ }, (prev2, next2) => {
64992
+ var _a2, _b2;
64993
+ 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);
64994
+ });
64598
64995
  const DEFAULT_SIZE_LIMIT = 200 * 1024;
64599
64996
  function formatSize(bytes) {
64600
64997
  if (bytes < 1024) return `${bytes} B`;
@@ -64654,12 +65051,29 @@ const DatasetPreview = memo(function DatasetPreview2({
64654
65051
  const setMutation = useDatasetSet(apiUrl, repo, workspace, requestOptions);
64655
65052
  const queryClient = useQueryClient();
64656
65053
  const decoded = (_a2 = valueQuery.data) == null ? void 0 : _a2.decoded;
64657
- const write = useCallback(async (next2) => {
64658
- if (workspace == null || path2 == null || type === void 0) return;
65054
+ const edited = useRef(null);
65055
+ const writing = useRef(0);
65056
+ const writes = useRef(Promise.resolve());
65057
+ useEffect(() => {
65058
+ if (writing.current === 0) edited.current = null;
65059
+ }, [decoded]);
65060
+ const write = useCallback((next2) => {
65061
+ if (workspace == null || path2 == null || type === void 0) return Promise.resolve();
64659
65062
  const treePath = path2.split(".").filter(Boolean).map((p2) => variant("field", p2));
64660
65063
  const data4 = encodeBeast2For(type)(next2);
64661
- await setMutation.mutateAsync({ path: treePath, data: data4 });
64662
- await queryClient.invalidateQueries({ queryKey: ["datasetStatus", apiUrl, repo, workspace, path2] });
65064
+ edited.current = { value: next2 };
65065
+ writing.current++;
65066
+ const run = writes.current.then(async () => {
65067
+ await setMutation.mutateAsync({ path: treePath, data: data4 });
65068
+ await queryClient.invalidateQueries({ queryKey: ["datasetStatus", apiUrl, repo, workspace, path2] });
65069
+ });
65070
+ writes.current = run.catch(() => void 0);
65071
+ return run.catch((err) => {
65072
+ edited.current = null;
65073
+ throw err;
65074
+ }).finally(() => {
65075
+ writing.current--;
65076
+ });
64663
65077
  }, [apiUrl, repo, workspace, path2, type, setMutation, queryClient]);
64664
65078
  const keyType = useMemo(() => type === void 0 ? null : type.type === "Dict" ? type.value.key : type.type === "Set" ? type.value : null, [type]);
64665
65079
  const inlineKeys = useMemo(() => decoded === void 0 || type === void 0 ? null : type.type === "Dict" ? [...decoded.keys()] : type.type === "Set" ? [...decoded.values()] : null, [type, decoded]);
@@ -64695,11 +65109,12 @@ const DatasetPreview = memo(function DatasetPreview2({
64695
65109
  const treeValue = useMemo(() => {
64696
65110
  if (type === void 0 || decoded === void 0) return null;
64697
65111
  const root2 = ValueTree.materialize(type, decoded);
65112
+ const base = () => edited.current !== null ? edited.current.value : decoded;
64698
65113
  const wire = editable ? {
64699
- onEdit: some((p2, leaf) => write(ValueTree.applyEdit(type, decoded, p2, { kind: "edit", leaf }))),
64700
- onInsert: some((p2) => write(ValueTree.applyEdit(type, decoded, p2, { kind: "insert" }))),
64701
- onRemove: some((p2) => write(ValueTree.applyEdit(type, decoded, p2, { kind: "remove" }))),
64702
- onTag: some((p2, tag) => write(ValueTree.applyEdit(type, decoded, p2, { kind: "tag", tag })))
65114
+ onEdit: some((p2, leaf) => write(ValueTree.applyEdit(type, base(), p2, { kind: "edit", leaf }))),
65115
+ onInsert: some((p2) => write(ValueTree.applyEdit(type, base(), p2, { kind: "insert" }))),
65116
+ onRemove: some((p2) => write(ValueTree.applyEdit(type, base(), p2, { kind: "remove" }))),
65117
+ onTag: some((p2, tag) => write(ValueTree.applyEdit(type, base(), p2, { kind: "tag", tag })))
64703
65118
  } : { onEdit: none$1, onInsert: none$1, onRemove: none$1, onTag: none$1 };
64704
65119
  return { root: root2, ...wire, style: some({ height: some("100%"), maxHeight: none$1, openDepth: none$1, toolbar: some(true) }) };
64705
65120
  }, [type, decoded, editable, write]);
@@ -64741,8 +65156,11 @@ const DatasetPreview = memo(function DatasetPreview2({
64741
65156
  /* @__PURE__ */ jsx$1(DownloadButton, { onClick: download, label: "Download value" })
64742
65157
  ] });
64743
65158
  }
65159
+ if (valueQuery.error) {
65160
+ const { message, details } = formatApiError(valueQuery.error);
65161
+ return /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "error", title: "Load failed", message, details: details ?? formatError(valueQuery.error) });
65162
+ }
64744
65163
  if (valueQuery.isLoading || !valueQuery.data) return /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "loading", title: "Loading..." });
64745
- if (valueQuery.error) return /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "error", title: "Decode failed", message: valueQuery.error.message });
64746
65164
  const count = type === void 0 || decoded === void 0 ? null : type.type === "Array" ? decoded.length : type.type === "Set" || type.type === "Dict" ? decoded.size : null;
64747
65165
  const countText = count === null ? "" : `${count.toLocaleString()} ${(type == null ? void 0 : type.type) === "Dict" ? "entries" : "items"} · `;
64748
65166
  return /* @__PURE__ */ jsxs(Flex, { direction: "column", height: "100%", overflow: "hidden", children: [
@@ -64813,19 +65231,15 @@ const UITaskPreview = memo(function UITaskPreview2({
64813
65231
  const workspace = (config2 == null ? void 0 : config2.workspace) ?? (e3 == null ? void 0 : e3.workspace) ?? null;
64814
65232
  const token2 = (config2 == null ? void 0 : config2.token) ?? (e3 == null ? void 0 : e3.token) ?? null;
64815
65233
  const requestOptions = { token: token2 };
65234
+ const bound3 = cache3 == null ? void 0 : cache3.getConfig().workspace;
65235
+ const foreign = (config2 == null ? void 0 : config2.workspace) !== void 0 && bound3 !== void 0 && config2.workspace !== bound3;
64816
65236
  const detailsQuery = useTaskDetails(apiUrl ?? "", repo, workspace, task, { requestOptions });
64817
65237
  const details = detailsQuery.data;
64818
- const kind = details ? getTaskKind(details) : null;
64819
- const isUI = kind === "ui";
64820
- const manifest = useMemo(() => {
64821
- if (!details || !isUI) return null;
64822
- const meta3 = getTaskMetadata(details);
64823
- return meta3 ? decodeManifest(meta3) : { paths: [], functions: [], records: [], pages: [] };
64824
- }, [details, isUI]);
64825
- const outputPath = details ? treePathToString$1(details.output) : null;
65238
+ const manifest = useMemo(() => (details == null ? void 0 : details.role.type) === "ui" ? details.role.value : null, [details]);
65239
+ const outputPath = details ? treePathToString$1(details.output.path) : null;
64826
65240
  const preloads = useMemo(
64827
- () => manifest && workspace ? manifest.paths.map((path2) => ({ workspace, path: path2 })) : [],
64828
- [manifest, workspace]
65241
+ () => manifest && workspace && !foreign ? manifest.paths.map((path2) => ({ workspace, path: path2 })) : [],
65242
+ [manifest, workspace, foreign]
64829
65243
  );
64830
65244
  const { loading: preloading, error: preloadError } = usePreloadReactiveDatasets(preloads);
64831
65245
  const scopedPlatforms = useMemo(
@@ -64851,7 +65265,7 @@ const UITaskPreview = memo(function UITaskPreview2({
64851
65265
  [manifest]
64852
65266
  );
64853
65267
  useEffect(() => {
64854
- if (!cache3 || !manifest || !workspace) return;
65268
+ if (!cache3 || !manifest || !workspace || foreign) return;
64855
65269
  for (const path2 of manifest.paths) {
64856
65270
  cache3.setRefetchInterval(workspace, path2, pollInterval);
64857
65271
  }
@@ -64860,7 +65274,7 @@ const UITaskPreview = memo(function UITaskPreview2({
64860
65274
  cache3.clearRefetchInterval(workspace, path2);
64861
65275
  }
64862
65276
  };
64863
- }, [cache3, manifest, workspace, pollInterval]);
65277
+ }, [cache3, manifest, workspace, pollInterval, foreign]);
64864
65278
  const statusQuery = useDatasetStatus(apiUrl ?? "", repo, workspace, outputPath, { requestOptions });
64865
65279
  const valueQuery = useDatasetValue(apiUrl ?? "", repo, workspace, outputPath, {
64866
65280
  requestOptions,
@@ -64870,16 +65284,20 @@ const UITaskPreview = memo(function UITaskPreview2({
64870
65284
  });
64871
65285
  const store = useMemo(() => createUIStore(), [task]);
64872
65286
  if (!apiUrl) return /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "error", title: "Configuration error", message: "No apiUrl available — render UITaskPreview inside an <E3Provider> or pass a config prop." });
65287
+ if (foreign) {
65288
+ return /* @__PURE__ */ jsx$1(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.` });
65289
+ }
64873
65290
  if (detailsQuery.isLoading) return /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "loading", title: "Loading task..." });
64874
65291
  if (detailsQuery.error) return /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "error", title: "Error", message: detailsQuery.error.message });
64875
65292
  if (!details) return /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "info", title: "No task", message: `Task "${task}" not found` });
64876
- if (!isUI) return /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "error", title: "Not a UI task", message: `Task "${task}" has kind "${kind ?? "(none)"}"` });
65293
+ if (!manifest) return /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "error", title: "Not a UI task", message: `Task "${task}" is a ${details.role.type} task` });
64877
65294
  if (preloadError) return /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "error", title: "Preload failed", message: preloadError.message });
64878
65295
  if (preloading) return /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "loading", title: "Loading datasets..." });
65296
+ if (statusQuery.error) return /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "error", title: "Error", message: statusQuery.error.message });
64879
65297
  if (statusQuery.isLoading || !statusQuery.data) return /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "loading", title: "Loading..." });
64880
65298
  if (statusQuery.data.refType !== "value") return /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "info", title: "No output yet", message: "Task has not produced a value" });
65299
+ if (valueQuery.error) return /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "error", title: "Load failed", message: valueQuery.error.message });
64881
65300
  if (valueQuery.isLoading || !valueQuery.data) return /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "loading", title: "Loading..." });
64882
- if (valueQuery.error) return /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "error", title: "Decode failed", message: valueQuery.error.message });
64883
65301
  return /* @__PURE__ */ jsx$1(UIStoreProvider, { store, children: /* @__PURE__ */ jsx$1(ErrorBoundary, { children: /* @__PURE__ */ jsx$1(Box, { height: "100%", overflow: bare ? void 0 : "auto", p: bare ? void 0 : "4", minH: bare ? 0 : void 0, children: /* @__PURE__ */ jsx$1(
64884
65302
  EastChakraComponent,
64885
65303
  {
@@ -65336,7 +65754,7 @@ const DataTaskPreview = memo(function DataTaskPreview2({
65336
65754
  const detailsQuery = useTaskDetails(apiUrl, repo, workspace, task, {
65337
65755
  ...requestOptions != null && { requestOptions }
65338
65756
  });
65339
- const outputPath = detailsQuery.data ? treePathToString(detailsQuery.data.output) : null;
65757
+ const outputPath = detailsQuery.data ? treePathToString(detailsQuery.data.output.path) : null;
65340
65758
  return /* @__PURE__ */ jsxs(Box, { height: "100%", display: "flex", flexDirection: "column", overflow: "hidden", children: [
65341
65759
  /* @__PURE__ */ jsx$1(
65342
65760
  Flex,
@@ -65400,13 +65818,14 @@ const TaskPreview = memo(function TaskPreview2({
65400
65818
  requestOptions,
65401
65819
  bare = false
65402
65820
  }) {
65821
+ var _a2;
65403
65822
  const detailsQuery = useTaskDetails(apiUrl, repo, workspace, task, {
65404
65823
  ...requestOptions != null && { requestOptions }
65405
65824
  });
65406
- const kind = detailsQuery.data ? getTaskKind(detailsQuery.data) : null;
65825
+ const isUI = ((_a2 = detailsQuery.data) == null ? void 0 : _a2.role.type) === "ui";
65407
65826
  return /* @__PURE__ */ jsxs(Box, { height: "100%", display: "flex", flexDirection: "column", overflow: "hidden", children: [
65408
65827
  !bare && /* @__PURE__ */ jsx$1(Flex, { px: 4, py: 2, borderBottom: "1px solid", borderColor: "border.subtle", bg: "bg.surface", align: "center", flexShrink: 0, children: /* @__PURE__ */ jsx$1(Text, { fontSize: "sm", fontWeight: "medium", color: "fg", children: task }) }),
65409
- /* @__PURE__ */ jsx$1(Box, { flex: 1, overflow: "hidden", minHeight: 0, children: detailsQuery.isLoading ? /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "loading", title: "Loading task..." }) : detailsQuery.error ? /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "error", title: "Error", message: detailsQuery.error.message }) : kind === "ui" ? /* @__PURE__ */ jsx$1(
65828
+ /* @__PURE__ */ jsx$1(Box, { flex: 1, overflow: "hidden", minHeight: 0, children: detailsQuery.isLoading ? /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "loading", title: "Loading task..." }) : detailsQuery.error ? /* @__PURE__ */ jsx$1(StatusDisplay, { variant: "error", title: "Error", message: detailsQuery.error.message }) : isUI ? /* @__PURE__ */ jsx$1(
65410
65829
  UITaskPreview,
65411
65830
  {
65412
65831
  task,
@@ -65424,7 +65843,10 @@ const TaskPreview = memo(function TaskPreview2({
65424
65843
  }
65425
65844
  ) })
65426
65845
  ] });
65427
- }, (prev2, next2) => prev2.task === next2.task && prev2.repo === next2.repo && prev2.workspace === next2.workspace && prev2.bare === next2.bare);
65846
+ }, (prev2, next2) => {
65847
+ var _a2, _b2;
65848
+ 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);
65849
+ });
65428
65850
  function formatPrimitive(type, value) {
65429
65851
  switch (type.type) {
65430
65852
  case "Null":
@@ -65819,8 +66241,6 @@ export {
65819
66241
  getBindingTypes,
65820
66242
  getReactiveDatasetCache,
65821
66243
  getStagedStore,
65822
- getTaskKind,
65823
- getTaskMetadata,
65824
66244
  initializeFunctionApi,
65825
66245
  initializePagedApi,
65826
66246
  initializeReactiveDatasetCache,
@@ -65828,6 +66248,7 @@ export {
65828
66248
  initializeStagedStore,
65829
66249
  isBindingTracking,
65830
66250
  onWriteError,
66251
+ pagedRevisionKey,
65831
66252
  pagedTotalKey,
65832
66253
  pagedWindowKey,
65833
66254
  preloadReactiveDatasetList,