@farmslot/protocol 0.74.10 → 0.76.0

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 (152) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/client-cache/artifact-response.d.ts +15 -0
  3. package/dist/client-cache/artifact-response.d.ts.map +1 -0
  4. package/dist/client-cache/artifact-response.js +103 -0
  5. package/dist/client-cache/artifact-response.js.map +1 -0
  6. package/dist/client-cache/cache.d.ts +6 -1
  7. package/dist/client-cache/cache.d.ts.map +1 -1
  8. package/dist/client-cache/cache.js +231 -40
  9. package/dist/client-cache/cache.js.map +1 -1
  10. package/dist/client-cache/command-method.d.ts +17 -4
  11. package/dist/client-cache/command-method.d.ts.map +1 -1
  12. package/dist/client-cache/command-method.js +222 -1
  13. package/dist/client-cache/command-method.js.map +1 -1
  14. package/dist/client-cache/conformance.d.ts +2 -5
  15. package/dist/client-cache/conformance.d.ts.map +1 -1
  16. package/dist/client-cache/conformance.js +4 -0
  17. package/dist/client-cache/conformance.js.map +1 -1
  18. package/dist/client-cache/content.d.ts +11 -0
  19. package/dist/client-cache/content.d.ts.map +1 -1
  20. package/dist/client-cache/content.js +43 -0
  21. package/dist/client-cache/content.js.map +1 -1
  22. package/dist/client-cache/index.d.ts +5 -2
  23. package/dist/client-cache/index.d.ts.map +1 -1
  24. package/dist/client-cache/index.js +5 -2
  25. package/dist/client-cache/index.js.map +1 -1
  26. package/dist/client-cache/network-cost.d.ts +4 -0
  27. package/dist/client-cache/network-cost.d.ts.map +1 -0
  28. package/dist/client-cache/network-cost.js +24 -0
  29. package/dist/client-cache/network-cost.js.map +1 -0
  30. package/dist/client-cache/prefetch.d.ts +17 -0
  31. package/dist/client-cache/prefetch.d.ts.map +1 -1
  32. package/dist/client-cache/prefetch.js +110 -47
  33. package/dist/client-cache/prefetch.js.map +1 -1
  34. package/dist/client-cache/read-stored.d.ts +7 -0
  35. package/dist/client-cache/read-stored.d.ts.map +1 -0
  36. package/dist/client-cache/read-stored.js +18 -0
  37. package/dist/client-cache/read-stored.js.map +1 -0
  38. package/dist/client-cache/registry.d.ts +309 -16
  39. package/dist/client-cache/registry.d.ts.map +1 -1
  40. package/dist/client-cache/registry.js +422 -27
  41. package/dist/client-cache/registry.js.map +1 -1
  42. package/dist/client-cache/run-details.d.ts +29 -0
  43. package/dist/client-cache/run-details.d.ts.map +1 -0
  44. package/dist/client-cache/run-details.js +145 -0
  45. package/dist/client-cache/run-details.js.map +1 -0
  46. package/dist/client-cache/run-list-versions.d.ts +2 -0
  47. package/dist/client-cache/run-list-versions.d.ts.map +1 -1
  48. package/dist/client-cache/run-list-versions.js +9 -0
  49. package/dist/client-cache/run-list-versions.js.map +1 -1
  50. package/dist/client-cache/store.d.ts +8 -0
  51. package/dist/client-cache/store.d.ts.map +1 -1
  52. package/dist/client-cache/store.js +25 -0
  53. package/dist/client-cache/store.js.map +1 -1
  54. package/dist/client-cache/transport.d.ts +8 -1
  55. package/dist/client-cache/transport.d.ts.map +1 -1
  56. package/dist/client-cache/transport.js +8 -0
  57. package/dist/client-cache/transport.js.map +1 -1
  58. package/dist/client-cache/types.d.ts +11 -1
  59. package/dist/client-cache/types.d.ts.map +1 -1
  60. package/dist/contracts/agents.d.ts +2 -0
  61. package/dist/contracts/agents.d.ts.map +1 -1
  62. package/dist/contracts/agents.js.map +1 -1
  63. package/dist/contracts/config.d.ts +10 -0
  64. package/dist/contracts/config.d.ts.map +1 -1
  65. package/dist/contracts/config.js +7 -0
  66. package/dist/contracts/config.js.map +1 -1
  67. package/dist/contracts/dispatch.d.ts +18 -0
  68. package/dist/contracts/dispatch.d.ts.map +1 -1
  69. package/dist/contracts/run-lifecycle-view.d.ts +9 -0
  70. package/dist/contracts/run-lifecycle-view.d.ts.map +1 -1
  71. package/dist/contracts/run-lifecycle-view.js +8 -0
  72. package/dist/contracts/run-lifecycle-view.js.map +1 -1
  73. package/dist/contracts/runs.d.ts +78 -7
  74. package/dist/contracts/runs.d.ts.map +1 -1
  75. package/dist/contracts/runs.js +33 -2
  76. package/dist/contracts/runs.js.map +1 -1
  77. package/dist/contracts/slot-recovery.d.ts +11 -0
  78. package/dist/contracts/slot-recovery.d.ts.map +1 -1
  79. package/dist/contracts/slot-recovery.js +11 -1
  80. package/dist/contracts/slot-recovery.js.map +1 -1
  81. package/dist/contracts/slots.d.ts +17 -0
  82. package/dist/contracts/slots.d.ts.map +1 -1
  83. package/dist/index.d.ts +3 -0
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +3 -0
  86. package/dist/index.js.map +1 -1
  87. package/dist/recipe/recording.d.ts +1 -1
  88. package/dist/recipe/recording.d.ts.map +1 -1
  89. package/dist/recipe/recording.js +2 -2
  90. package/dist/recipe/recording.js.map +1 -1
  91. package/dist/rpc/dispatch.d.ts +8 -0
  92. package/dist/rpc/dispatch.d.ts.map +1 -1
  93. package/dist/rpc/dispatch.js +8 -0
  94. package/dist/rpc/dispatch.js.map +1 -1
  95. package/dist/rpc/fleet.d.ts +3 -1
  96. package/dist/rpc/fleet.d.ts.map +1 -1
  97. package/dist/rpc/fleet.js.map +1 -1
  98. package/dist/rpc/native-session.d.ts +29 -0
  99. package/dist/rpc/native-session.d.ts.map +1 -1
  100. package/dist/rpc/native-session.js +9 -0
  101. package/dist/rpc/native-session.js.map +1 -1
  102. package/dist/rpc/registry.d.ts +1 -0
  103. package/dist/rpc/registry.d.ts.map +1 -1
  104. package/dist/rpc/registry.js +1 -0
  105. package/dist/rpc/registry.js.map +1 -1
  106. package/dist/rpc/run.d.ts +4 -0
  107. package/dist/rpc/run.d.ts.map +1 -1
  108. package/dist/rpc/run.js.map +1 -1
  109. package/dist/rpc/runner-models.d.ts +14 -3
  110. package/dist/rpc/runner-models.d.ts.map +1 -1
  111. package/dist/rpc/runner-models.js +37 -13
  112. package/dist/rpc/runner-models.js.map +1 -1
  113. package/dist/rpc/slot.d.ts +3 -2
  114. package/dist/rpc/slot.d.ts.map +1 -1
  115. package/dist/rpc/slot.js.map +1 -1
  116. package/dist/rpc/tmux.d.ts +8 -0
  117. package/dist/rpc/tmux.d.ts.map +1 -1
  118. package/dist/runs/list-trim.d.ts +13 -0
  119. package/dist/runs/list-trim.d.ts.map +1 -0
  120. package/dist/runs/list-trim.js +71 -0
  121. package/dist/runs/list-trim.js.map +1 -0
  122. package/dist/runs/list-version.d.ts +4 -0
  123. package/dist/runs/list-version.d.ts.map +1 -0
  124. package/dist/runs/list-version.js +9 -0
  125. package/dist/runs/list-version.js.map +1 -0
  126. package/dist/runs/review-chain.d.ts.map +1 -1
  127. package/dist/runs/review-chain.js +2 -1
  128. package/dist/runs/review-chain.js.map +1 -1
  129. package/dist/runs/run-family-groups.d.ts.map +1 -1
  130. package/dist/runs/run-family-groups.js +5 -4
  131. package/dist/runs/run-family-groups.js.map +1 -1
  132. package/dist/runs/run-list-view.d.ts.map +1 -1
  133. package/dist/runs/run-list-view.js +2 -1
  134. package/dist/runs/run-list-view.js.map +1 -1
  135. package/dist/slots/tracking-branch.d.ts +7 -0
  136. package/dist/slots/tracking-branch.d.ts.map +1 -1
  137. package/dist/slots/tracking-branch.js.map +1 -1
  138. package/dist/surfaces/command-center.js +1 -1
  139. package/dist/surfaces/command-center.js.map +1 -1
  140. package/dist/surfaces/view-links.d.ts +1 -0
  141. package/dist/surfaces/view-links.d.ts.map +1 -1
  142. package/dist/surfaces/view-links.js +23 -8
  143. package/dist/surfaces/view-links.js.map +1 -1
  144. package/dist/text-order.d.ts +7 -0
  145. package/dist/text-order.d.ts.map +1 -0
  146. package/dist/text-order.js +7 -0
  147. package/dist/text-order.js.map +1 -0
  148. package/dist/version.d.ts +1 -1
  149. package/dist/version.d.ts.map +1 -1
  150. package/dist/version.js +1 -1
  151. package/dist/version.js.map +1 -1
  152. package/package.json +1 -1
@@ -1,11 +1,13 @@
1
1
  import { Methods } from '../rpc/registry.js';
2
+ import { runListVersion } from '../runs/list-version.js';
2
3
  import { ResourceCollectorAdmission } from './collector-admission.js';
3
- import { resourceBudgetEvictions, resourceContentWithinLimits, verifyResourceContent, } from './content.js';
4
+ import { resourceBudgetEvictions, resourceContentWithinLimits, verifyResourceContent, verifyResourceContentInSlices, } from './content.js';
4
5
  import { isKnownResourceFamily, RESOURCE_PUBLISH_INTERVAL_MS, RESOURCE_REREAD_INTERVAL_MS, RESOURCE_RETRY_MAX_WAIT_MS, resourceKeyId, } from './keys.js';
5
6
  import { nativeHistoryStorageId, readNativeHistory, storedNativeHistoryMatches, } from './native-history.js';
6
7
  import { resourceQuery } from './registry.js';
8
+ import { createRunDetailSync, runDetailTarget } from './run-details.js';
7
9
  import { isRunListCollectionRead, RUN_LIST_COLLECTION_ID, RunListCollection, runListSummaryFlags, } from './run-list-versions.js';
8
- import { createMemoryResourceStore, normalizeResourceStoreScope, resourceStoreScopeId, sameResourceAuthority, } from './store.js';
10
+ import { createMemoryResourceStore, normalizeResourceStoreScope, readStoredQueryBatch, resourceStoreScopeId, sameResourceAuthority, } from './store.js';
9
11
  import { isUpstreamStoredQuery, upstreamRetentionEvictions } from './upstream-retention.js';
10
12
  import { workspaceRetentionEvictions, workspaceRetentionMembership, } from './workspace-retention.js';
11
13
  const deletionReasons = new Set([
@@ -21,6 +23,13 @@ const evictionCodes = new Set([
21
23
  'RUN_NOT_FOUND',
22
24
  'RESOURCE_NOT_FOUND',
23
25
  ]);
26
+ /**
27
+ * The gateway refused the read: the resource is gone or not this principal's. The
28
+ * cache evicts saved data on these; a network failure or timeout is not one.
29
+ */
30
+ export function readRefused(error) {
31
+ return Boolean(error && evictionCodes.has(error.code));
32
+ }
24
33
  function queryError(error) {
25
34
  const code = error && typeof error === 'object' ? Reflect.get(error, 'code') : undefined;
26
35
  return {
@@ -44,6 +53,7 @@ function normalizeConnection(connection) {
44
53
  };
45
54
  }
46
55
  const defaultContentBudgetBytes = 500 * 1024 * 1024;
56
+ const startupQueryBytes = 8 * 1024 * 1024;
47
57
  // Only the identity crosses the store seam, never a caller's other fields.
48
58
  const contentKey = ({ kind, id }) => ({ kind, id });
49
59
  function profileId(connection) {
@@ -56,6 +66,9 @@ export function createGatewayResourceCache(options) {
56
66
  const transport = options.transport;
57
67
  let store = options.store ?? createMemoryResourceStore();
58
68
  let metadata;
69
+ let startupStore;
70
+ let startupOwnerVersion = -1;
71
+ const startupQueries = new Map();
59
72
  let connection = normalizeConnection(transport.getConnection());
60
73
  let usable = false;
61
74
  let disposed = false;
@@ -91,9 +104,33 @@ export function createGatewayResourceCache(options) {
91
104
  maxBodyBytes: resourceQuery(Methods.RUN_LIST).maxBytes,
92
105
  });
93
106
  let runListLoad;
107
+ const fallbackDetailTargets = new Map();
108
+ const detailSync = createRunDetailSync({
109
+ connection: () => connection,
110
+ list: async () => (await readStoredContent((target) => target.listQueries())) ?? [],
111
+ stored: (runId) => readStoredContent((target) => target.getQuery(resourceQuery(Methods.RUN_GET, { runId }).id)),
112
+ touch: async (runId) => {
113
+ const target = store;
114
+ const ownerVersion = hydrationOwnerVersion;
115
+ await updateStore(target, () => target === store && !target.deleted && ownerVersion === hydrationOwnerVersion, () => target.touchQuery?.(resourceQuery(Methods.RUN_GET, { runId }).id, Date.now()) ??
116
+ Promise.resolve());
117
+ },
118
+ fetch: (runId, maxStoredBytes, signal) => retryQuery(resourceQuery(Methods.RUN_GET, { runId }), { origin: 'prefetch', cohort: 'run', maxStoredBytes }, signal),
119
+ onError: (error) => options.onStorageNotice?.(`Background run details could not sync: ${queryError(error).message}`),
120
+ });
121
+ function refreshDetailTargets() {
122
+ const versioned = runList.detailTargets();
123
+ detailSync.setTargets(store.persistent && usable
124
+ ? runList.held
125
+ ? versioned
126
+ : [...fallbackDetailTargets.values()]
127
+ : []);
128
+ }
94
129
  function forgetRunList() {
95
130
  runList.clear();
96
131
  runListLoad = undefined;
132
+ fallbackDetailTargets.clear();
133
+ detailSync.reset();
97
134
  }
98
135
  function updateCollectors() {
99
136
  collectors.update({ ...connection, foreground: usable && connection.foreground !== false }, usable
@@ -116,12 +153,29 @@ export function createGatewayResourceCache(options) {
116
153
  unsubscribeStore();
117
154
  store = next;
118
155
  storeVersion++;
156
+ startupStore = undefined;
157
+ startupQueries.clear();
158
+ detailSync.reset();
119
159
  unsubscribeStore = store.onDeleted(storeDeleted);
120
160
  }
161
+ /** Observers whose failure was already logged, so a repeating one is reported once. */
162
+ const failedObservers = new WeakSet();
163
+ /** One observer's failure is its own: the other observers and the cache's reads go on. */
164
+ function deliver(entry, listener) {
165
+ try {
166
+ listener(entry.state);
167
+ }
168
+ catch (error) {
169
+ if (failedObservers.has(listener))
170
+ return;
171
+ failedObservers.add(listener);
172
+ console.error(`Resource cache observer of ${entry.query.id} failed:`, error);
173
+ }
174
+ }
121
175
  function notify(entry) {
122
176
  entry.state = { ...entry.state, storageNotice };
123
177
  for (const listener of entry.listeners)
124
- listener(entry.state);
178
+ deliver(entry, listener);
125
179
  }
126
180
  function cancelScheduledRead(entry) {
127
181
  if (entry.readTimer !== undefined)
@@ -178,13 +232,24 @@ export function createGatewayResourceCache(options) {
178
232
  for (const entry of entries.values())
179
233
  notify(entry);
180
234
  }
235
+ function withStoreWriteLock(target, write) {
236
+ return target.withWriteLock ? target.withWriteLock(write) : write();
237
+ }
181
238
  /** Retention, writes and lifecycle changes share one ordered store boundary. */
182
- function updateStore(target, current, update) {
239
+ function updateStore(target, current, update, lockWrites = true) {
183
240
  const updating = barrier.then(async () => {
184
241
  if (!current())
185
242
  return;
186
243
  try {
187
- await update();
244
+ const write = async () => {
245
+ // A queued cross-tab write must recheck the owner after acquiring its lock.
246
+ if (current())
247
+ await update();
248
+ };
249
+ if (lockWrites)
250
+ await withStoreWriteLock(target, write);
251
+ else
252
+ await write();
188
253
  }
189
254
  catch (error) {
190
255
  await recoverStore(error, target);
@@ -232,6 +297,46 @@ export function createGatewayResourceCache(options) {
232
297
  if (entry.state.status !== 'ready')
233
298
  hydrateEntry(entry);
234
299
  }
300
+ async function warmStoredQueries() {
301
+ if (!store.persistent ||
302
+ (startupStore === store && startupOwnerVersion === hydrationOwnerVersion))
303
+ return;
304
+ const readingStore = store;
305
+ const incarnation = storeVersion;
306
+ const ownerVersion = hydrationOwnerVersion;
307
+ const current = () => !disposed &&
308
+ store === readingStore &&
309
+ !readingStore.deleted &&
310
+ incarnation === storeVersion &&
311
+ ownerVersion === hydrationOwnerVersion;
312
+ try {
313
+ const batch = await readStoredQueryBatch(readingStore, startupQueryBytes);
314
+ if (!current())
315
+ return;
316
+ const collection = batch.find((saved) => saved.id === RUN_LIST_COLLECTION_ID);
317
+ const rows = collection && readingStore.getRows
318
+ ? await readingStore.getRows(RUN_LIST_COLLECTION_ID)
319
+ : undefined;
320
+ if (!current())
321
+ return;
322
+ startupQueries.clear();
323
+ let remaining = startupQueryBytes;
324
+ for (const saved of batch) {
325
+ if (saved.bytes > remaining)
326
+ continue;
327
+ remaining -= saved.bytes;
328
+ if (saved.id !== RUN_LIST_COLLECTION_ID)
329
+ startupQueries.set(saved.id, saved);
330
+ }
331
+ if (rows)
332
+ runList.hydrate(rows);
333
+ startupStore = readingStore;
334
+ startupOwnerVersion = ownerVersion;
335
+ }
336
+ catch (error) {
337
+ await recoverStore(error, readingStore);
338
+ }
339
+ }
235
340
  function schedule(entry) {
236
341
  if (entry.queued || disposed)
237
342
  return;
@@ -261,8 +366,13 @@ export function createGatewayResourceCache(options) {
261
366
  connection.id === next.id &&
262
367
  !scopeChanged &&
263
368
  !authorityChanged &&
264
- !revoke)
369
+ !revoke) {
370
+ if (connection.metered !== next.metered) {
371
+ connection = { ...connection, metered: next.metered };
372
+ refreshDetailTargets();
373
+ }
265
374
  return;
375
+ }
266
376
  const ticket = ++version;
267
377
  runList.abort();
268
378
  // Stored facts belong to an owner, not a socket attempt. Fence a revoked
@@ -273,6 +383,7 @@ export function createGatewayResourceCache(options) {
273
383
  }
274
384
  connection = next;
275
385
  usable = false;
386
+ refreshDetailTargets();
276
387
  updateCollectors();
277
388
  revisions.clear();
278
389
  for (const entry of entries.values()) {
@@ -353,17 +464,24 @@ export function createGatewayResourceCache(options) {
353
464
  if (store.persistent && retentionSweptStore !== store) {
354
465
  const target = store;
355
466
  try {
356
- for (const id of upstreamRetentionEvictions(await target.listQueries()))
357
- await target.deleteQuery(id);
358
- retentionSweptStore = target;
467
+ await withStoreWriteLock(target, async () => {
468
+ for (const id of upstreamRetentionEvictions(await target.listQueries()))
469
+ await target.deleteQuery(id);
470
+ retentionSweptStore = target;
471
+ });
359
472
  }
360
473
  catch (error) {
361
474
  await recoverStore(error, target);
362
475
  }
363
476
  }
477
+ if (disposed || ticket !== version)
478
+ return;
479
+ if (!revoke && canPersist(next))
480
+ await warmStoredQueries();
364
481
  if (disposed || ticket !== version)
365
482
  return;
366
483
  usable = next.connected && next.foreground !== false && !revoke;
484
+ refreshDetailTargets();
367
485
  updateCollectors();
368
486
  if (usable)
369
487
  for (const entry of entries.values())
@@ -409,7 +527,7 @@ export function createGatewayResourceCache(options) {
409
527
  if (change.revision <= (revisions.get(id) ?? -1))
410
528
  continue;
411
529
  revisions.set(id, change.revision);
412
- const affected = [...entries.values()].filter((entry) => entry.query.dependencies.some((key) => resourceKeyId(key) === id));
530
+ const affected = [...entries.values()].filter((entry) => entry.dependencyIds.has(id));
413
531
  // Coverage counts declared dependencies only, so a known key that no entry declares (another
414
532
  // slot, an unopened workspace) changes nothing held. Only a family this client cannot name might.
415
533
  if (!affected.length && isKnownResourceFamily(change.key?.family))
@@ -441,6 +559,7 @@ export function createGatewayResourceCache(options) {
441
559
  }
442
560
  const entry = {
443
561
  query,
562
+ dependencyIds: new Set(query.dependencies.map(resourceKeyId)),
444
563
  state: { status: 'loading' },
445
564
  listeners: new Set(),
446
565
  hydration: Promise.resolve(),
@@ -454,9 +573,40 @@ export function createGatewayResourceCache(options) {
454
573
  failures: 0,
455
574
  };
456
575
  entries.set(query.id, entry);
457
- hydrateEntry(entry);
576
+ const saved = isRunListView(query) ? heldRunListView(query) : takeStartupQuery(query);
577
+ if (applyStoredQuery(entry, saved)) {
578
+ entry.hydrated = true;
579
+ queueMicrotask(prune);
580
+ }
581
+ else
582
+ hydrateEntry(entry);
458
583
  return entry;
459
584
  }
585
+ function takeStartupQuery(query) {
586
+ if (startupStore !== store || startupOwnerVersion !== hydrationOwnerVersion)
587
+ return undefined;
588
+ const id = nativeHistoryStorageId(query);
589
+ const saved = startupQueries.get(id);
590
+ startupQueries.delete(id);
591
+ return saved;
592
+ }
593
+ function applyStoredQuery(entry, saved) {
594
+ const query = entry.query;
595
+ if (!saved ||
596
+ !query.persisted ||
597
+ saved.bytes > query.maxBytes ||
598
+ (query.method === Methods.NATIVE_SESSION_READ &&
599
+ !storedNativeHistoryMatches(query.params, saved.data)))
600
+ return false;
601
+ const sourceObservation = query.sourceObservation?.(saved.data);
602
+ entry.state = {
603
+ status: 'stale',
604
+ data: saved.data,
605
+ observedAt: saved.observedAt,
606
+ ...(sourceObservation ? { sourceObservation, error: sourceObservation.error } : {}),
607
+ };
608
+ return true;
609
+ }
460
610
  function hydrateEntry(entry) {
461
611
  const query = entry.query;
462
612
  const hydrationSerial = ++entry.hydrationSerial;
@@ -470,7 +620,8 @@ export function createGatewayResourceCache(options) {
470
620
  const readingStore = store;
471
621
  try {
472
622
  const saved = query.persisted
473
- ? ((isRunListView(query) ? await storedRunListView(query, readingStore) : undefined) ??
623
+ ? (takeStartupQuery(query) ??
624
+ (isRunListView(query) ? await storedRunListView(query, readingStore) : undefined) ??
474
625
  (await readingStore.getQuery(nativeHistoryStorageId(query))))
475
626
  : undefined;
476
627
  if (!disposed &&
@@ -480,17 +631,7 @@ export function createGatewayResourceCache(options) {
480
631
  ownerVersion === hydrationOwnerVersion &&
481
632
  entry.hydrationSerial === hydrationSerial &&
482
633
  entry.state.status !== 'ready' &&
483
- saved &&
484
- saved.bytes <= query.maxBytes &&
485
- (query.method !== Methods.NATIVE_SESSION_READ ||
486
- storedNativeHistoryMatches(query.params, saved.data))) {
487
- const sourceObservation = query.sourceObservation?.(saved.data);
488
- entry.state = {
489
- status: 'stale',
490
- data: saved.data,
491
- observedAt: saved.observedAt,
492
- ...(sourceObservation ? { sourceObservation, error: sourceObservation.error } : {}),
493
- };
634
+ applyStoredQuery(entry, saved)) {
494
635
  notify(entry);
495
636
  }
496
637
  }
@@ -521,6 +662,9 @@ export function createGatewayResourceCache(options) {
521
662
  };
522
663
  await runListLoad.promise;
523
664
  }
665
+ return heldRunListView(query);
666
+ }
667
+ function heldRunListView(query) {
524
668
  const data = runList.view(query.params);
525
669
  return data && runList.observedAt !== undefined
526
670
  ? { id: query.id, data, observedAt: runList.observedAt, bytes: 0 }
@@ -548,7 +692,9 @@ export function createGatewayResourceCache(options) {
548
692
  if (evictionCodes.has(queryError(error).code)) {
549
693
  const evictingStore = store;
550
694
  forgetRunList();
551
- void updateStore(evictingStore, () => evictingStore === store && !evictingStore.deleted, () => evictingStore.deleteQuery(RUN_LIST_COLLECTION_ID));
695
+ void updateStore(evictingStore, () => evictingStore === store && !evictingStore.deleted, () => evictingStore.deleteQuery(RUN_LIST_COLLECTION_ID),
696
+ // Queue one-statement refusal evictions before their callers settle.
697
+ false);
552
698
  }
553
699
  throw error;
554
700
  }
@@ -623,7 +769,11 @@ export function createGatewayResourceCache(options) {
623
769
  const shared = entry.pending.prefetch;
624
770
  entry.pending.prefetch =
625
771
  shared && prefetch
626
- ? { origin: 'prefetch', cohort: shared.cohort === 'gate' ? 'gate' : prefetch.cohort }
772
+ ? {
773
+ origin: 'prefetch',
774
+ cohort: shared.cohort === 'gate' ? 'gate' : prefetch.cohort,
775
+ maxStoredBytes: Math.min(shared.maxStoredBytes ?? Infinity, prefetch.maxStoredBytes ?? Infinity),
776
+ }
627
777
  : undefined;
628
778
  return entry.pending.promise;
629
779
  }
@@ -744,6 +894,7 @@ export function createGatewayResourceCache(options) {
744
894
  const retention = entry.query.retention;
745
895
  const saved = serialized !== undefined &&
746
896
  bytes <= entry.query.maxBytes &&
897
+ (entry.listeners.size > 0 || bytes <= (pending.prefetch?.maxStoredBytes ?? Infinity)) &&
747
898
  (!retention || bytes <= retention.maxBytes)
748
899
  ? {
749
900
  id: nativeHistoryStorageId(entry.query),
@@ -816,7 +967,9 @@ export function createGatewayResourceCache(options) {
816
967
  (retention.cohort === 'upstream'
817
968
  ? isUpstreamStoredQuery(value.id)
818
969
  : value.id.startsWith(`${entry.query.method}:`)))
819
- .sort((a, b) => a.observedAt - b.observedAt ||
970
+ .sort((a, b) => (entry.query.method === Methods.RUN_GET
971
+ ? (a.accessedAt ?? a.observedAt) - (b.accessedAt ?? b.observedAt)
972
+ : a.observedAt - b.observedAt) ||
820
973
  (entries.get(a.id)?.lastUsed ?? 0) - (entries.get(b.id)?.lastUsed ?? 0));
821
974
  if (!current())
822
975
  return;
@@ -836,6 +989,14 @@ export function createGatewayResourceCache(options) {
836
989
  await writingStore.putQuery(row);
837
990
  });
838
991
  }
992
+ if (store.persistent && entry.query.method === Methods.RUN_LIST) {
993
+ const listed = reply.data;
994
+ if (Array.isArray(listed.runs)) {
995
+ for (const run of listed.runs)
996
+ fallbackDetailTargets.set(run.id, runDetailTarget(run, runListVersion(run)));
997
+ }
998
+ refreshDetailTargets();
999
+ }
839
1000
  }
840
1001
  catch (error) {
841
1002
  if (disposed ||
@@ -866,7 +1027,9 @@ export function createGatewayResourceCache(options) {
866
1027
  // Commit an accepted refusal before a listener queues its profile's close.
867
1028
  const eviction = updateStore(evictingStore, () => evictingStore === store &&
868
1029
  evictingIncarnation === storeVersion &&
869
- !evictingStore.deleted, () => evictingStore.deleteQuery(nativeHistoryStorageId(entry.query)));
1030
+ !evictingStore.deleted, () => evictingStore.deleteQuery(nativeHistoryStorageId(entry.query)),
1031
+ // Refusals must not wait for another tab's admission lock before enqueueing deletion.
1032
+ false);
870
1033
  notify(entry);
871
1034
  // An authorization refusal settles its callers in the reply's own task, so a prefetch
872
1035
  // stops before a store callback can release another read. The eviction is queued.
@@ -962,14 +1125,25 @@ export function createGatewayResourceCache(options) {
962
1125
  });
963
1126
  return written;
964
1127
  }
1128
+ function contentWriteToken() {
1129
+ return { connectionId: connection.id, ownerVersion: hydrationOwnerVersion, storeVersion };
1130
+ }
965
1131
  /** Stores verified bytes within the caps and the shared budget. */
966
- function storeContent(input) {
1132
+ async function storeContent(input) {
967
1133
  const key = contentKey(input.key);
968
1134
  const { data, origin, cohort } = input;
969
- if (!resourceContentWithinLimits(key, data) ||
970
- !verifyResourceContent(key, data, input.untrackedFiles))
971
- return Promise.resolve(false);
972
- return writeContent(input.token, async (target, current) => {
1135
+ // Prefetch verifies between frames, so the write still answers to the owner it was asked under.
1136
+ const token = input.token ?? contentWriteToken();
1137
+ if (!resourceContentWithinLimits(key, data))
1138
+ return false;
1139
+ // Prefetch is background work; content a person opened is on their critical path and
1140
+ // verifies at once, as before.
1141
+ const verified = origin === 'prefetch'
1142
+ ? await verifyResourceContentInSlices(key, data, input.untrackedFiles)
1143
+ : verifyResourceContent(key, data, input.untrackedFiles);
1144
+ if (!verified)
1145
+ return false;
1146
+ return writeContent(token, async (target, current) => {
973
1147
  const accessedAt = Date.now();
974
1148
  const content = await target.listContent();
975
1149
  if (content.some((row) => row.kind === key.kind && row.id === key.id)) {
@@ -1019,6 +1193,13 @@ export function createGatewayResourceCache(options) {
1019
1193
  return true;
1020
1194
  });
1021
1195
  }
1196
+ async function retryQuery(query, placement, signal) {
1197
+ if (disposed)
1198
+ throw new Error('Resource cache is disposed');
1199
+ const entry = entryFor(query);
1200
+ await fetchEntry(entry, false, placement, signal);
1201
+ return entry.state;
1202
+ }
1022
1203
  return {
1023
1204
  observedKey(match) {
1024
1205
  for (const entry of entries.values())
@@ -1071,7 +1252,7 @@ export function createGatewayResourceCache(options) {
1071
1252
  notify(entry);
1072
1253
  });
1073
1254
  }
1074
- receive(entry.state);
1255
+ deliver(entry, receive);
1075
1256
  if (entry.state.status !== 'ready' || entry.dirty)
1076
1257
  schedule(entry);
1077
1258
  return () => {
@@ -1091,13 +1272,21 @@ export function createGatewayResourceCache(options) {
1091
1272
  throw new Error('Resource cache is disposed');
1092
1273
  return entryFor(query).state;
1093
1274
  },
1094
- async retry(query, placement, signal) {
1275
+ async readStored(query, refresh = true) {
1095
1276
  if (disposed)
1096
1277
  throw new Error('Resource cache is disposed');
1278
+ const ownerVersion = hydrationOwnerVersion;
1279
+ await barrier;
1097
1280
  const entry = entryFor(query);
1098
- await fetchEntry(entry, false, placement, signal);
1281
+ await entry.hydration;
1282
+ if (disposed || ownerVersion !== hydrationOwnerVersion)
1283
+ throw new Error('The offline store owner changed');
1284
+ // fetchEntry records network refusals in the query state and notifies its readers.
1285
+ if (refresh && entry.state.data !== undefined)
1286
+ void fetchEntry(entry);
1099
1287
  return entry.state;
1100
1288
  },
1289
+ retry: retryQuery,
1101
1290
  async retryAfterPending(query) {
1102
1291
  if (disposed)
1103
1292
  throw new Error('Resource cache is disposed');
@@ -1112,20 +1301,22 @@ export function createGatewayResourceCache(options) {
1112
1301
  readContent: (key) => readContentBytes((target) => target.getContent(contentKey(key)), false),
1113
1302
  runListProjections: () => runList.projections(),
1114
1303
  openContent: (key) => readContentBytes((target) => target.getContent(contentKey(key)), true),
1304
+ async forgetContent(key) {
1305
+ const target = store;
1306
+ const ownerVersion = hydrationOwnerVersion;
1307
+ await updateStore(target, () => target === store && !target.deleted && ownerVersion === hydrationOwnerVersion, () => target.deleteContent(contentKey(key)));
1308
+ },
1115
1309
  readContentByAlias: (alias) => readContentBytes((target) => target.getContentByAlias(alias), false),
1116
1310
  openContentByAlias: (alias) => readContentBytes((target) => target.getContentByAlias(alias), true),
1117
1311
  storeContent,
1118
1312
  retainContent,
1119
- contentWriteToken: () => ({
1120
- connectionId: connection.id,
1121
- ownerVersion: hydrationOwnerVersion,
1122
- storeVersion,
1123
- }),
1313
+ contentWriteToken,
1124
1314
  async dispose(reason) {
1125
1315
  if (disposed)
1126
1316
  return;
1127
1317
  const withdrawing = collectors.close();
1128
1318
  disposed = true;
1319
+ detailSync.reset();
1129
1320
  version++;
1130
1321
  unsubscribeChanges();
1131
1322
  unsubscribeConnection();