@farmslot/protocol 0.83.1 → 0.84.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.
- package/CHANGELOG.md +52 -0
- package/dist/checklist-target.d.ts +5 -1
- package/dist/checklist-target.d.ts.map +1 -1
- package/dist/checklist-target.js +41 -0
- package/dist/checklist-target.js.map +1 -1
- package/dist/client-cache/cache.d.ts.map +1 -1
- package/dist/client-cache/cache.js +370 -26
- package/dist/client-cache/cache.js.map +1 -1
- package/dist/client-cache/command-method.d.ts +1 -1
- package/dist/client-cache/command-method.d.ts.map +1 -1
- package/dist/client-cache/command-method.js +1 -0
- package/dist/client-cache/command-method.js.map +1 -1
- package/dist/client-cache/entity-collection.d.ts +115 -0
- package/dist/client-cache/entity-collection.d.ts.map +1 -0
- package/dist/client-cache/entity-collection.js +323 -0
- package/dist/client-cache/entity-collection.js.map +1 -0
- package/dist/client-cache/entity-guards.d.ts +47 -0
- package/dist/client-cache/entity-guards.d.ts.map +1 -0
- package/dist/client-cache/entity-guards.js +193 -0
- package/dist/client-cache/entity-guards.js.map +1 -0
- package/dist/client-cache/entity-views.d.ts +62 -0
- package/dist/client-cache/entity-views.d.ts.map +1 -0
- package/dist/client-cache/entity-views.js +386 -0
- package/dist/client-cache/entity-views.js.map +1 -0
- package/dist/client-cache/index.d.ts +4 -0
- package/dist/client-cache/index.d.ts.map +1 -1
- package/dist/client-cache/index.js +4 -0
- package/dist/client-cache/index.js.map +1 -1
- package/dist/client-cache/keys.d.ts +13 -0
- package/dist/client-cache/keys.d.ts.map +1 -1
- package/dist/client-cache/keys.js +1 -0
- package/dist/client-cache/keys.js.map +1 -1
- package/dist/client-cache/native-history.d.ts +9 -2
- package/dist/client-cache/native-history.d.ts.map +1 -1
- package/dist/client-cache/native-history.js +66 -18
- package/dist/client-cache/native-history.js.map +1 -1
- package/dist/client-cache/registry.d.ts +15 -0
- package/dist/client-cache/registry.d.ts.map +1 -1
- package/dist/client-cache/registry.js +10 -0
- package/dist/client-cache/registry.js.map +1 -1
- package/dist/client-cache/run-details.d.ts +2 -1
- package/dist/client-cache/run-details.d.ts.map +1 -1
- package/dist/client-cache/run-details.js.map +1 -1
- package/dist/client-cache/run-list-versions.d.ts.map +1 -1
- package/dist/client-cache/run-list-versions.js.map +1 -1
- package/dist/client-cache/sync-state.d.ts +23 -0
- package/dist/client-cache/sync-state.d.ts.map +1 -0
- package/dist/client-cache/sync-state.js +39 -0
- package/dist/client-cache/sync-state.js.map +1 -0
- package/dist/client-cache/text-links.d.ts +11 -0
- package/dist/client-cache/text-links.d.ts.map +1 -0
- package/dist/client-cache/text-links.js +61 -0
- package/dist/client-cache/text-links.js.map +1 -0
- package/dist/client-cache/types.d.ts +11 -0
- package/dist/client-cache/types.d.ts.map +1 -1
- package/dist/contracts/agents.d.ts +6 -0
- package/dist/contracts/agents.d.ts.map +1 -1
- package/dist/contracts/agents.js.map +1 -1
- package/dist/contracts/analytics.d.ts +2 -2
- package/dist/contracts/analytics.d.ts.map +1 -1
- package/dist/contracts/evals.d.ts +9 -0
- package/dist/contracts/evals.d.ts.map +1 -1
- package/dist/contracts/evals.js +5 -0
- package/dist/contracts/evals.js.map +1 -1
- package/dist/contracts/family.d.ts +1 -1
- package/dist/contracts/family.d.ts.map +1 -1
- package/dist/contracts/pr-rules.d.ts +1 -1
- package/dist/contracts/pr-rules.d.ts.map +1 -1
- package/dist/contracts/pr-rules.js +3 -1
- package/dist/contracts/pr-rules.js.map +1 -1
- package/dist/contracts/review-workspace.d.ts +12 -0
- package/dist/contracts/review-workspace.d.ts.map +1 -1
- package/dist/contracts/review-workspace.js +9 -2
- package/dist/contracts/review-workspace.js.map +1 -1
- package/dist/contracts/reviews.d.ts +1 -1
- package/dist/contracts/reviews.d.ts.map +1 -1
- package/dist/contracts/run-lifecycle-view.d.ts.map +1 -1
- package/dist/contracts/run-lifecycle-view.js +9 -7
- package/dist/contracts/run-lifecycle-view.js.map +1 -1
- package/dist/contracts/runs.d.ts +77 -23
- package/dist/contracts/runs.d.ts.map +1 -1
- package/dist/contracts/runs.js +2 -2
- package/dist/contracts/runs.js.map +1 -1
- package/dist/contracts/slot-recovery.d.ts +10 -1
- package/dist/contracts/slot-recovery.d.ts.map +1 -1
- package/dist/contracts/slot-recovery.js +18 -10
- package/dist/contracts/slot-recovery.js.map +1 -1
- package/dist/contracts/slots.d.ts +31 -1
- package/dist/contracts/slots.d.ts.map +1 -1
- package/dist/contracts/task.d.ts +2 -0
- package/dist/contracts/task.d.ts.map +1 -1
- package/dist/contracts/task.js.map +1 -1
- package/dist/contracts/template-placeholders.d.ts.map +1 -1
- package/dist/contracts/template-placeholders.js +3 -2
- package/dist/contracts/template-placeholders.js.map +1 -1
- package/dist/deep-freeze.d.ts +6 -0
- package/dist/deep-freeze.d.ts.map +1 -0
- package/dist/deep-freeze.js +13 -0
- package/dist/deep-freeze.js.map +1 -0
- package/dist/file-stamp.d.ts +10 -0
- package/dist/file-stamp.d.ts.map +1 -0
- package/dist/file-stamp.js +12 -0
- package/dist/file-stamp.js.map +1 -0
- package/dist/fleet/host-load.d.ts +18 -0
- package/dist/fleet/host-load.d.ts.map +1 -0
- package/dist/fleet/host-load.js +30 -0
- package/dist/fleet/host-load.js.map +1 -0
- package/dist/fleet/stale.d.ts +13 -0
- package/dist/fleet/stale.d.ts.map +1 -0
- package/dist/fleet/stale.js +18 -0
- package/dist/fleet/stale.js.map +1 -0
- package/dist/fleet/summary.d.ts +8 -0
- package/dist/fleet/summary.d.ts.map +1 -0
- package/dist/fleet/summary.js +45 -0
- package/dist/fleet/summary.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/integrations/pr-rule-config.d.ts +2 -0
- package/dist/integrations/pr-rule-config.d.ts.map +1 -1
- package/dist/integrations/pr-rule-config.js +17 -4
- package/dist/integrations/pr-rule-config.js.map +1 -1
- package/dist/integrations/pr-workflow-defaults.d.ts.map +1 -1
- package/dist/integrations/pr-workflow-defaults.js +6 -3
- package/dist/integrations/pr-workflow-defaults.js.map +1 -1
- package/dist/node/farm-paths.d.ts +5 -0
- package/dist/node/farm-paths.d.ts.map +1 -1
- package/dist/node/farm-paths.js +64 -27
- package/dist/node/farm-paths.js.map +1 -1
- package/dist/node/pack-portability.d.ts +16 -0
- package/dist/node/pack-portability.d.ts.map +1 -1
- package/dist/node/pack-portability.js +169 -32
- package/dist/node/pack-portability.js.map +1 -1
- package/dist/node/slot-by-repo.d.ts +15 -1
- package/dist/node/slot-by-repo.d.ts.map +1 -1
- package/dist/node/slot-by-repo.js +115 -31
- package/dist/node/slot-by-repo.js.map +1 -1
- package/dist/node/tcp-metrics.d.ts +7 -0
- package/dist/node/tcp-metrics.d.ts.map +1 -0
- package/dist/node/tcp-metrics.js +131 -0
- package/dist/node/tcp-metrics.js.map +1 -0
- package/dist/performance/ranking.d.ts.map +1 -1
- package/dist/performance/ranking.js +1 -0
- package/dist/performance/ranking.js.map +1 -1
- package/dist/performance/recorder.d.ts.map +1 -1
- package/dist/performance/recorder.js +1 -1
- package/dist/performance/recorder.js.map +1 -1
- package/dist/performance/types.d.ts +5 -1
- package/dist/performance/types.d.ts.map +1 -1
- package/dist/performance/types.js +3 -0
- package/dist/performance/types.js.map +1 -1
- package/dist/performance/validation.d.ts.map +1 -1
- package/dist/performance/validation.js +19 -9
- package/dist/performance/validation.js.map +1 -1
- package/dist/recipe/common.d.ts.map +1 -1
- package/dist/recipe/common.js +2 -1
- package/dist/recipe/common.js.map +1 -1
- package/dist/recipe/manifest.js +1 -1
- package/dist/recipe/manifest.js.map +1 -1
- package/dist/rpc/auth.d.ts +6 -0
- package/dist/rpc/auth.d.ts.map +1 -1
- package/dist/rpc/auth.js.map +1 -1
- package/dist/rpc/clip.d.ts +38 -0
- package/dist/rpc/clip.d.ts.map +1 -0
- package/dist/rpc/clip.js +26 -0
- package/dist/rpc/clip.js.map +1 -0
- package/dist/rpc/eval.d.ts +5 -0
- package/dist/rpc/eval.d.ts.map +1 -1
- package/dist/rpc/fleet.d.ts +28 -1
- package/dist/rpc/fleet.d.ts.map +1 -1
- package/dist/rpc/fleet.js.map +1 -1
- package/dist/rpc/fs.d.ts +18 -0
- package/dist/rpc/fs.d.ts.map +1 -1
- package/dist/rpc/fs.js +2 -0
- package/dist/rpc/fs.js.map +1 -1
- package/dist/rpc/index.d.ts +1 -0
- package/dist/rpc/index.d.ts.map +1 -1
- package/dist/rpc/index.js +1 -0
- package/dist/rpc/index.js.map +1 -1
- package/dist/rpc/native-session.d.ts +31 -0
- package/dist/rpc/native-session.d.ts.map +1 -1
- package/dist/rpc/native-session.js.map +1 -1
- package/dist/rpc/pr.d.ts +24 -0
- package/dist/rpc/pr.d.ts.map +1 -1
- package/dist/rpc/registry.d.ts +3 -0
- package/dist/rpc/registry.d.ts.map +1 -1
- package/dist/rpc/registry.js +4 -0
- package/dist/rpc/registry.js.map +1 -1
- package/dist/rpc/run.d.ts +2 -1
- package/dist/rpc/run.d.ts.map +1 -1
- package/dist/rpc/run.js.map +1 -1
- package/dist/rpc/runner-models.d.ts.map +1 -1
- package/dist/rpc/runner-models.js +1 -0
- package/dist/rpc/runner-models.js.map +1 -1
- package/dist/rpc/slot.d.ts +26 -1
- package/dist/rpc/slot.d.ts.map +1 -1
- package/dist/rpc/slot.js +1 -0
- package/dist/rpc/slot.js.map +1 -1
- package/dist/runs/evidence-summary.d.ts +60 -4
- package/dist/runs/evidence-summary.d.ts.map +1 -1
- package/dist/runs/evidence-summary.js +148 -12
- package/dist/runs/evidence-summary.js.map +1 -1
- package/dist/runs/family-readiness.d.ts +4 -3
- package/dist/runs/family-readiness.d.ts.map +1 -1
- package/dist/runs/family-readiness.js.map +1 -1
- package/dist/runs/linked-tickets.d.ts +1 -1
- package/dist/runs/linked-tickets.d.ts.map +1 -1
- package/dist/runs/list-row.d.ts +57 -0
- package/dist/runs/list-row.d.ts.map +1 -0
- package/dist/runs/list-row.js +2 -0
- package/dist/runs/list-row.js.map +1 -0
- package/dist/runs/list-trim.d.ts +36 -4
- package/dist/runs/list-trim.d.ts.map +1 -1
- package/dist/runs/list-trim.js +327 -9
- package/dist/runs/list-trim.js.map +1 -1
- package/dist/runs/list-version.d.ts +2 -1
- package/dist/runs/list-version.d.ts.map +1 -1
- package/dist/runs/list-version.js.map +1 -1
- package/dist/runs/review-chain.d.ts +34 -2
- package/dist/runs/review-chain.d.ts.map +1 -1
- package/dist/runs/review-chain.js +31 -4
- package/dist/runs/review-chain.js.map +1 -1
- package/dist/runs/review-retries.d.ts +13 -3
- package/dist/runs/review-retries.d.ts.map +1 -1
- package/dist/runs/review-retries.js.map +1 -1
- package/dist/runs/run-family-groups.d.ts +7 -7
- package/dist/runs/run-family-groups.d.ts.map +1 -1
- package/dist/runs/run-family-groups.js.map +1 -1
- package/dist/runs/run-list-filter.d.ts +2 -1
- package/dist/runs/run-list-filter.d.ts.map +1 -1
- package/dist/runs/run-list-filter.js.map +1 -1
- package/dist/runs/run-list-observation.d.ts.map +1 -1
- package/dist/runs/run-list-observation.js.map +1 -1
- package/dist/runs/run-list-view.d.ts +3 -2
- package/dist/runs/run-list-view.d.ts.map +1 -1
- package/dist/runs/run-list-view.js.map +1 -1
- package/dist/slots/tracking-branch.d.ts.map +1 -1
- package/dist/slots/tracking-branch.js +4 -3
- package/dist/slots/tracking-branch.js.map +1 -1
- package/dist/surfaces/view-state.js +1 -1
- package/dist/surfaces/view-state.js.map +1 -1
- package/dist/transport/events.d.ts +42 -12
- package/dist/transport/events.d.ts.map +1 -1
- package/dist/transport/events.js +0 -1
- package/dist/transport/events.js.map +1 -1
- package/dist/transport/observations.d.ts +2 -0
- package/dist/transport/observations.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +12 -1
- package/scripts/literal-replacement.d.mts +2 -0
- package/scripts/literal-replacement.mjs +4 -0
|
@@ -3,6 +3,7 @@ import { Methods } from '../rpc/registry.js';
|
|
|
3
3
|
import { runListVersion } from '../runs/list-version.js';
|
|
4
4
|
import { ResourceCollectorAdmission } from './collector-admission.js';
|
|
5
5
|
import { resourceBudgetEvictions, resourceContentWithinLimits, verifyResourceContent, verifyResourceContentInSlices, } from './content.js';
|
|
6
|
+
import { createFleetEntityView, createPREntityView, isEntityStoreId, prEntityStoreId, prViewProject, } from './entity-views.js';
|
|
6
7
|
import { canonicalResourceJson, isKnownResourceFamily, RESOURCE_PUBLISH_INTERVAL_MS, RESOURCE_REREAD_INTERVAL_MS, RESOURCE_RETRY_MAX_WAIT_MS, resourceKeyId, } from './keys.js';
|
|
7
8
|
import { createNativeConversationCache, NATIVE_CONVERSATION_STORAGE_PREFIX, nativeConversationStorageId, } from './native-conversation.js';
|
|
8
9
|
import { nativeHistoryParams, nativeHistoryQuery, nativeHistoryStorageId, readNativeHistory, storedNativeHistoryMatches, } from './native-history.js';
|
|
@@ -10,6 +11,7 @@ import { resourceQuery } from './registry.js';
|
|
|
10
11
|
import { createRunDetailSync, runDetailTarget } from './run-details.js';
|
|
11
12
|
import { isRunListCollectionRead, RUN_LIST_COLLECTION_ID, RunListCollection, runListSummaryFlags, } from './run-list-versions.js';
|
|
12
13
|
import { createMemoryResourceStore, normalizeResourceStoreScope, readStoredQueryBatch, resourceStoreScopeId, sameResourceAuthority, } from './store.js';
|
|
14
|
+
import { resourceSyncState } from './sync-state.js';
|
|
13
15
|
import { isUpstreamStoredQuery, upstreamRetentionEvictions } from './upstream-retention.js';
|
|
14
16
|
import { workspaceRetentionEvictions, workspaceRetentionMembership, } from './workspace-retention.js';
|
|
15
17
|
const deletionReasons = new Set([
|
|
@@ -64,6 +66,11 @@ function profileId(connection) {
|
|
|
64
66
|
profileId: connection.scope.profileId,
|
|
65
67
|
});
|
|
66
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* A view assembled from pushed entities stores its whole snapshot at most this often; each
|
|
71
|
+
* pushed row is stored at once. It is the floor a full read of the fleet rereads at.
|
|
72
|
+
*/
|
|
73
|
+
const ENTITY_SNAPSHOT_INTERVAL_MS = RESOURCE_REREAD_INTERVAL_MS.slots ?? 10_000;
|
|
67
74
|
export function createGatewayResourceCache(options) {
|
|
68
75
|
const transport = options.transport;
|
|
69
76
|
const metrics = options.performance ?? transport.performance;
|
|
@@ -142,9 +149,13 @@ export function createGatewayResourceCache(options) {
|
|
|
142
149
|
let storageNotice;
|
|
143
150
|
let needsStore = options.store === undefined && options.openStore !== undefined;
|
|
144
151
|
let deletionRequired = false;
|
|
152
|
+
let recoveryAttempted = false;
|
|
153
|
+
let failedWriteKey;
|
|
145
154
|
let barrier = Promise.resolve();
|
|
155
|
+
let storeOpening = Promise.resolve();
|
|
146
156
|
const entries = new Map();
|
|
147
157
|
const revisions = new Map();
|
|
158
|
+
const syncListeners = new Map();
|
|
148
159
|
const encoder = new TextEncoder();
|
|
149
160
|
const collectors = new ResourceCollectorAdmission(transport);
|
|
150
161
|
let retiringStore = false;
|
|
@@ -162,11 +173,44 @@ export function createGatewayResourceCache(options) {
|
|
|
162
173
|
writingStore === store &&
|
|
163
174
|
incarnation === storeVersion &&
|
|
164
175
|
!writingStore.deleted;
|
|
165
|
-
return updateStore(writingStore, current, () => writingStore.mergeRows(RUN_LIST_COLLECTION_ID, merge));
|
|
176
|
+
return updateStore(writingStore, current, () => writingStore.mergeRows(RUN_LIST_COLLECTION_ID, merge), true, RUN_LIST_COLLECTION_ID);
|
|
166
177
|
},
|
|
167
178
|
maxBodyBytes: resourceQuery(Methods.RUN_LIST).maxBytes,
|
|
168
179
|
});
|
|
169
180
|
let runListLoad;
|
|
181
|
+
// Entity sync (ADR-071 Amendment 4): fleet.status and each pr.list scope are answered from
|
|
182
|
+
// entity collections when the gateway advertises entitySync. Rows outlive a connection; their
|
|
183
|
+
// listing is verified by a versions read on every connection before the view is current.
|
|
184
|
+
const fleetView = createFleetEntityView();
|
|
185
|
+
const prViews = new Map();
|
|
186
|
+
const entitySyncOn = () => connection.capabilities?.resourceChanges?.entitySync === true;
|
|
187
|
+
const entityViews = () => [fleetView, ...prViews.values()];
|
|
188
|
+
/** The view a query reads through, created on first use; undefined without entity sync. */
|
|
189
|
+
function entityViewFor(query) {
|
|
190
|
+
if (!entitySyncOn())
|
|
191
|
+
return undefined;
|
|
192
|
+
if (fleetView.serves(query))
|
|
193
|
+
return fleetView;
|
|
194
|
+
const scope = prViewProject(query);
|
|
195
|
+
const collectionKey = query.dependencies.find((key) => key.family === 'upstream');
|
|
196
|
+
if (!scope || !collectionKey)
|
|
197
|
+
return undefined;
|
|
198
|
+
const storeId = prEntityStoreId(scope.project);
|
|
199
|
+
let view = prViews.get(storeId);
|
|
200
|
+
if (!view) {
|
|
201
|
+
view = createPREntityView(scope.project, collectionKey);
|
|
202
|
+
prViews.set(storeId, view);
|
|
203
|
+
}
|
|
204
|
+
return view;
|
|
205
|
+
}
|
|
206
|
+
/** Advances when the views are discarded: stored rows read for an earlier one are dropped. */
|
|
207
|
+
let entityViewGeneration = 0;
|
|
208
|
+
function forgetEntityViews() {
|
|
209
|
+
entityViewGeneration++;
|
|
210
|
+
for (const view of entityViews())
|
|
211
|
+
view.clear();
|
|
212
|
+
prViews.clear();
|
|
213
|
+
}
|
|
170
214
|
const fallbackDetailTargets = new Map();
|
|
171
215
|
const detailSync = createRunDetailSync({
|
|
172
216
|
connection: () => connection,
|
|
@@ -210,6 +254,7 @@ export function createGatewayResourceCache(options) {
|
|
|
210
254
|
hydrationOwnerVersion++;
|
|
211
255
|
version++;
|
|
212
256
|
forgetRunList();
|
|
257
|
+
forgetEntityViews();
|
|
213
258
|
for (const entry of entries.values())
|
|
214
259
|
invalidate(entry, true);
|
|
215
260
|
transport.resynchronize();
|
|
@@ -238,10 +283,29 @@ export function createGatewayResourceCache(options) {
|
|
|
238
283
|
console.error(`Resource cache observer of ${entry.query.id} failed:`, error);
|
|
239
284
|
}
|
|
240
285
|
}
|
|
286
|
+
const failedSyncObservers = new WeakSet();
|
|
287
|
+
function notifySync() {
|
|
288
|
+
if (!syncListeners.size)
|
|
289
|
+
return;
|
|
290
|
+
const observed = [...entries.values()].filter((entry) => entry.listeners.size > 0);
|
|
291
|
+
for (const [listener, match] of syncListeners) {
|
|
292
|
+
const state = resourceSyncState(observed.filter((entry) => !match || match(entry.query)).map((entry) => entry.state), usable);
|
|
293
|
+
try {
|
|
294
|
+
listener(state);
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
if (failedSyncObservers.has(listener))
|
|
298
|
+
continue;
|
|
299
|
+
failedSyncObservers.add(listener);
|
|
300
|
+
console.error('Resource cache sync observer failed:', error);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
241
304
|
function notify(entry) {
|
|
242
305
|
entry.state = { ...entry.state, storageNotice };
|
|
243
306
|
for (const listener of entry.listeners)
|
|
244
307
|
deliver(entry, listener);
|
|
308
|
+
notifySync();
|
|
245
309
|
}
|
|
246
310
|
function cancelScheduledRead(entry) {
|
|
247
311
|
if (entry.readTimer !== undefined)
|
|
@@ -279,12 +343,20 @@ export function createGatewayResourceCache(options) {
|
|
|
279
343
|
}
|
|
280
344
|
}
|
|
281
345
|
}
|
|
282
|
-
|
|
346
|
+
// Healthy small queries and admission no-ops cannot confirm that the failed
|
|
347
|
+
// collection/row can be persisted. Only that same completed write re-arms it.
|
|
348
|
+
function confirmPersistentWrite(target, key) {
|
|
349
|
+
if (target === store && target.persistent && key === failedWriteKey) {
|
|
350
|
+
recoveryAttempted = false;
|
|
351
|
+
failedWriteKey = undefined;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async function recoverStore(error, failed = store, writeKey) {
|
|
283
355
|
// A retired store's late I/O failure cannot replace the new principal's store.
|
|
284
356
|
if (failed !== store || disposed)
|
|
285
357
|
return;
|
|
358
|
+
failedWriteKey = writeKey;
|
|
286
359
|
replaceStore(createMemoryResourceStore());
|
|
287
|
-
// Retry on the next connection lifecycle, never in an I/O failure loop.
|
|
288
360
|
needsStore = options.openStore !== undefined && canPersist(connection);
|
|
289
361
|
storageNotice = `Cached data will not survive a restart: ${queryError(error).message}`;
|
|
290
362
|
if (metadata)
|
|
@@ -297,14 +369,48 @@ export function createGatewayResourceCache(options) {
|
|
|
297
369
|
storageNotice += `; ${queryError(closeError).message}`;
|
|
298
370
|
}
|
|
299
371
|
options.onStorageNotice?.(storageNotice);
|
|
372
|
+
// An I/O failure may have lost a collection merge. Keep visible values, but
|
|
373
|
+
// require a full gateway versions read and never hydrate its old disk rows.
|
|
374
|
+
forgetRunList();
|
|
375
|
+
// Entity rows are admitted only at a version the gateway lists, so a lost merge costs reads.
|
|
376
|
+
forgetEntityViews();
|
|
300
377
|
for (const entry of entries.values())
|
|
378
|
+
invalidate(entry);
|
|
379
|
+
if (needsStore &&
|
|
380
|
+
metadata &&
|
|
381
|
+
connection.connected &&
|
|
382
|
+
connection.foreground !== false &&
|
|
383
|
+
!recoveryAttempted) {
|
|
384
|
+
recoveryAttempted = true;
|
|
385
|
+
const ticket = version;
|
|
386
|
+
const nextMetadata = metadata;
|
|
387
|
+
await openFreshStore(nextMetadata, connection, false);
|
|
388
|
+
if (disposed || ticket !== version)
|
|
389
|
+
return;
|
|
390
|
+
try {
|
|
391
|
+
const recoveredStore = store;
|
|
392
|
+
await withStoreWriteLock(recoveredStore, async () => {
|
|
393
|
+
if (disposed || ticket !== version || recoveredStore !== store)
|
|
394
|
+
return;
|
|
395
|
+
await recoveredStore.setMetadata(nextMetadata);
|
|
396
|
+
await recoveredStore.deleteQuery(RUN_LIST_COLLECTION_ID);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
catch (retryError) {
|
|
400
|
+
await recoverStore(retryError);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
for (const entry of entries.values()) {
|
|
301
404
|
notify(entry);
|
|
405
|
+
if (usable)
|
|
406
|
+
schedule(entry);
|
|
407
|
+
}
|
|
302
408
|
}
|
|
303
409
|
function withStoreWriteLock(target, write) {
|
|
304
410
|
return target.withWriteLock ? target.withWriteLock(write) : write();
|
|
305
411
|
}
|
|
306
412
|
/** Retention, writes and lifecycle changes share one ordered store boundary. */
|
|
307
|
-
function updateStore(target, current, update, lockWrites = true) {
|
|
413
|
+
function updateStore(target, current, update, lockWrites = true, writeKey) {
|
|
308
414
|
const updating = barrier.then(async () => {
|
|
309
415
|
if (!current())
|
|
310
416
|
return;
|
|
@@ -320,20 +426,28 @@ export function createGatewayResourceCache(options) {
|
|
|
320
426
|
await write();
|
|
321
427
|
}
|
|
322
428
|
catch (error) {
|
|
323
|
-
await recoverStore(error, target);
|
|
429
|
+
await recoverStore(error, target, writeKey);
|
|
324
430
|
}
|
|
325
431
|
});
|
|
326
432
|
barrier = updating;
|
|
327
433
|
return updating;
|
|
328
434
|
}
|
|
329
|
-
async function openFreshStore(next, nextConnection) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
435
|
+
async function openFreshStore(next, nextConnection, hydrate = true) {
|
|
436
|
+
// Hydration/content failures can recover outside the write barrier. Serialize
|
|
437
|
+
// every opener with connection-lifecycle opens, without recursively holding it on failure.
|
|
438
|
+
const opening = storeOpening.then(async () => {
|
|
439
|
+
if (disposed)
|
|
440
|
+
return;
|
|
441
|
+
const previous = store;
|
|
442
|
+
const recoveringMemory = metadata !== undefined && !previous.persistent;
|
|
443
|
+
metadata = next;
|
|
334
444
|
const opened = options.openStore && canPersist(nextConnection)
|
|
335
445
|
? await options.openStore(next)
|
|
336
446
|
: createMemoryResourceStore();
|
|
447
|
+
if (disposed) {
|
|
448
|
+
await opened.close();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
337
451
|
if (opened !== previous) {
|
|
338
452
|
if (recoveringMemory && !opened.persistent) {
|
|
339
453
|
// A failed retry must not discard the active fallback's cached bodies.
|
|
@@ -368,15 +482,22 @@ export function createGatewayResourceCache(options) {
|
|
|
368
482
|
storageNotice = 'Offline data will not survive a restart with this gateway version.';
|
|
369
483
|
options.onStorageNotice?.(storageNotice);
|
|
370
484
|
}
|
|
485
|
+
needsStore =
|
|
486
|
+
options.openStore !== undefined && !store.persistent && canPersist(nextConnection);
|
|
487
|
+
// Same-owner recovery must not replace a newer visible reply with older disk data.
|
|
488
|
+
if (hydrate)
|
|
489
|
+
for (const entry of entries.values())
|
|
490
|
+
if (entry.state.data === undefined && entry.state.status !== 'ready')
|
|
491
|
+
hydrateEntry(entry);
|
|
492
|
+
});
|
|
493
|
+
// The caller handles failures; a later opener still gets a usable queue.
|
|
494
|
+
storeOpening = opening.then(() => undefined, () => undefined);
|
|
495
|
+
try {
|
|
496
|
+
await opening;
|
|
371
497
|
}
|
|
372
498
|
catch (error) {
|
|
373
499
|
await recoverStore(error);
|
|
374
500
|
}
|
|
375
|
-
needsStore = options.openStore !== undefined && !store.persistent && canPersist(nextConnection);
|
|
376
|
-
// Same-owner recovery must not replace a newer visible reply with older disk data.
|
|
377
|
-
for (const entry of entries.values())
|
|
378
|
-
if (entry.state.data === undefined && entry.state.status !== 'ready')
|
|
379
|
-
hydrateEntry(entry);
|
|
380
501
|
}
|
|
381
502
|
async function warmStoredQueries() {
|
|
382
503
|
if (!store.persistent ||
|
|
@@ -406,7 +527,7 @@ export function createGatewayResourceCache(options) {
|
|
|
406
527
|
if (saved.bytes > remaining)
|
|
407
528
|
continue;
|
|
408
529
|
remaining -= saved.bytes;
|
|
409
|
-
if (saved.id !== RUN_LIST_COLLECTION_ID)
|
|
530
|
+
if (saved.id !== RUN_LIST_COLLECTION_ID && !isEntityStoreId(saved.id))
|
|
410
531
|
startupQueries.set(saved.id, saved);
|
|
411
532
|
}
|
|
412
533
|
// Restore small aliased report bodies before exposing their cached run. Network
|
|
@@ -476,7 +597,17 @@ export function createGatewayResourceCache(options) {
|
|
|
476
597
|
return;
|
|
477
598
|
}
|
|
478
599
|
const ticket = ++version;
|
|
600
|
+
recoveryAttempted = false;
|
|
601
|
+
failedWriteKey = undefined;
|
|
479
602
|
runList.abort();
|
|
603
|
+
// A new connection or epoch: rows stay displayed, unverified until a versions read. A gateway
|
|
604
|
+
// without entity sync replaces the screen with full reads, which a view held from before
|
|
605
|
+
// would be older than: the next entity sync starts from what is displayed then.
|
|
606
|
+
if (next.connected && next.capabilities?.resourceChanges?.entitySync !== true)
|
|
607
|
+
forgetEntityViews();
|
|
608
|
+
else
|
|
609
|
+
for (const view of entityViews())
|
|
610
|
+
view.unverify();
|
|
480
611
|
// Stored facts belong to an owner, not a socket attempt. Fence a revoked
|
|
481
612
|
// owner's pending I/O synchronously, before queued deletion can run.
|
|
482
613
|
if (revoke || scopeChanged || authorityChanged) {
|
|
@@ -484,6 +615,7 @@ export function createGatewayResourceCache(options) {
|
|
|
484
615
|
residentContent.clear();
|
|
485
616
|
hydrationOwnerVersion++;
|
|
486
617
|
forgetRunList();
|
|
618
|
+
forgetEntityViews();
|
|
487
619
|
}
|
|
488
620
|
connection = next;
|
|
489
621
|
conversations?.connectionChanged(revoke || scopeChanged || authorityChanged);
|
|
@@ -529,6 +661,7 @@ export function createGatewayResourceCache(options) {
|
|
|
529
661
|
residentContent.clear();
|
|
530
662
|
storeVersion++;
|
|
531
663
|
forgetRunList();
|
|
664
|
+
forgetEntityViews();
|
|
532
665
|
for (const entry of entries.values())
|
|
533
666
|
invalidate(entry, true);
|
|
534
667
|
// Switching profiles retains the old profile's store; revoked authority deletes it.
|
|
@@ -587,6 +720,7 @@ export function createGatewayResourceCache(options) {
|
|
|
587
720
|
if (disposed || ticket !== version)
|
|
588
721
|
return;
|
|
589
722
|
usable = next.connected && next.foreground !== false && !revoke;
|
|
723
|
+
notifySync();
|
|
590
724
|
refreshDetailTargets();
|
|
591
725
|
updateCollectors();
|
|
592
726
|
if (usable)
|
|
@@ -615,12 +749,16 @@ export function createGatewayResourceCache(options) {
|
|
|
615
749
|
updateCollectors();
|
|
616
750
|
version++;
|
|
617
751
|
runList.abort();
|
|
752
|
+
for (const view of entityViews())
|
|
753
|
+
view.unverify();
|
|
618
754
|
for (const entry of entries.values())
|
|
619
755
|
invalidate(entry);
|
|
620
756
|
transport.resynchronize();
|
|
621
757
|
return;
|
|
622
758
|
}
|
|
623
759
|
conversations?.resourcesChanged(payload);
|
|
760
|
+
const entitySync = entitySyncOn();
|
|
761
|
+
const changedViews = new Set();
|
|
624
762
|
for (const change of payload.changes) {
|
|
625
763
|
if (!Number.isSafeInteger(change.revision) || change.revision < 0) {
|
|
626
764
|
usable = false;
|
|
@@ -634,13 +772,22 @@ export function createGatewayResourceCache(options) {
|
|
|
634
772
|
if (change.revision <= (revisions.get(id) ?? -1))
|
|
635
773
|
continue;
|
|
636
774
|
revisions.set(id, change.revision);
|
|
637
|
-
|
|
775
|
+
if (entitySync)
|
|
776
|
+
for (const view of entityViews())
|
|
777
|
+
if (view.applyChange(change))
|
|
778
|
+
changedViews.add(view);
|
|
779
|
+
// An entity view applied its own collection's change above. Any other dependency changes
|
|
780
|
+
// what the gateway would serve without a push: the view is stale until it reads again.
|
|
781
|
+
const affected = [...entries.values()].filter((entry) => entry.dependencyIds.has(id) &&
|
|
782
|
+
!(entitySync && entityViewFor(entry.query)?.collectionKeyId === id));
|
|
638
783
|
// Coverage counts declared dependencies only, so a known key that no entry declares (another
|
|
639
784
|
// slot, an unopened workspace) changes nothing held. Only a family this client cannot name might.
|
|
640
785
|
if (!affected.length && isKnownResourceFamily(change.key?.family))
|
|
641
786
|
continue;
|
|
642
787
|
const targets = affected.length ? affected : [...entries.values()];
|
|
643
788
|
for (const entry of targets) {
|
|
789
|
+
if (entitySync)
|
|
790
|
+
entityViewFor(entry.query)?.requireVersions();
|
|
644
791
|
if (!affected.length)
|
|
645
792
|
entry.unknownGeneration++;
|
|
646
793
|
const floor = entry.query.rereadIntervalMs?.[change.key.family] ??
|
|
@@ -653,6 +800,31 @@ export function createGatewayResourceCache(options) {
|
|
|
653
800
|
schedule(entry);
|
|
654
801
|
}
|
|
655
802
|
}
|
|
803
|
+
for (const view of changedViews)
|
|
804
|
+
refreshEntityView(view);
|
|
805
|
+
}
|
|
806
|
+
/** A notification changed an entity view: reassemble it now; a read follows only if needed. */
|
|
807
|
+
function refreshEntityView(view) {
|
|
808
|
+
for (const entry of entries.values()) {
|
|
809
|
+
if (!view.serves(entry.query))
|
|
810
|
+
continue;
|
|
811
|
+
entry.dirty = true;
|
|
812
|
+
if (!entry.listeners.size) {
|
|
813
|
+
// Nobody observes it now: it is stale until someone does, and reassembled then.
|
|
814
|
+
if (entry.state.status === 'ready')
|
|
815
|
+
entry.state = { ...entry.state, status: 'stale' };
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
if (entry.queued)
|
|
819
|
+
continue;
|
|
820
|
+
entry.queued = true;
|
|
821
|
+
queueMicrotask(() => {
|
|
822
|
+
entry.queued = false;
|
|
823
|
+
// Pushed changes are not paced: assembling them costs no read.
|
|
824
|
+
if (entry.listeners.size && usable && !disposed)
|
|
825
|
+
void fetchEntry(entry, false);
|
|
826
|
+
});
|
|
827
|
+
}
|
|
656
828
|
}
|
|
657
829
|
const conversationDenials = new Map();
|
|
658
830
|
conversations = createNativeConversationCache(transport.conversations, {
|
|
@@ -849,6 +1021,132 @@ export function createGatewayResourceCache(options) {
|
|
|
849
1021
|
}
|
|
850
1022
|
})();
|
|
851
1023
|
}
|
|
1024
|
+
/** The stamp of an entity view: the revisions this client has applied on this connection. */
|
|
1025
|
+
function appliedRevisionStamp(entry) {
|
|
1026
|
+
const epoch = connection.capabilities?.resourceChanges?.epoch;
|
|
1027
|
+
// Sound only for a verified collection: its versions reply came after every notification
|
|
1028
|
+
// published before it, and every notification since was applied to the collection.
|
|
1029
|
+
return epoch
|
|
1030
|
+
? {
|
|
1031
|
+
epoch,
|
|
1032
|
+
dependencies: entry.query.dependencies.map((key) => ({
|
|
1033
|
+
key,
|
|
1034
|
+
revision: revisions.get(resourceKeyId(key)) ?? 0,
|
|
1035
|
+
})),
|
|
1036
|
+
}
|
|
1037
|
+
: undefined;
|
|
1038
|
+
}
|
|
1039
|
+
/** Rows a previous session stored, loaded once per store before the view's first versions read. */
|
|
1040
|
+
async function hydrateEntityView(view) {
|
|
1041
|
+
const readingStore = store;
|
|
1042
|
+
if (view.hydratedFrom === readingStore || !readingStore.getRows)
|
|
1043
|
+
return;
|
|
1044
|
+
view.hydratedFrom = readingStore;
|
|
1045
|
+
const incarnation = storeVersion;
|
|
1046
|
+
const owner = hydrationOwnerVersion;
|
|
1047
|
+
const generation = entityViewGeneration;
|
|
1048
|
+
let stored;
|
|
1049
|
+
try {
|
|
1050
|
+
stored = await readingStore.getRows(view.storeId);
|
|
1051
|
+
}
|
|
1052
|
+
catch (error) {
|
|
1053
|
+
// A failed store read falls back to memory, as every other store read does.
|
|
1054
|
+
await recoverStore(error, readingStore);
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
if (disposed ||
|
|
1058
|
+
store !== readingStore ||
|
|
1059
|
+
readingStore.deleted ||
|
|
1060
|
+
incarnation !== storeVersion ||
|
|
1061
|
+
owner !== hydrationOwnerVersion ||
|
|
1062
|
+
// The view was discarded meanwhile (a gateway without entity sync): these rows are older
|
|
1063
|
+
// than the full read that replaced the screen.
|
|
1064
|
+
generation !== entityViewGeneration)
|
|
1065
|
+
return;
|
|
1066
|
+
view.hydrate(stored);
|
|
1067
|
+
}
|
|
1068
|
+
/** Writes the rows a view changed since its last write, fenced to the current store. */
|
|
1069
|
+
function persistEntityView(view) {
|
|
1070
|
+
const writingStore = store;
|
|
1071
|
+
const incarnation = storeVersion;
|
|
1072
|
+
const changes = view.takeUnstored();
|
|
1073
|
+
const mergeRows = writingStore.mergeRows?.bind(writingStore);
|
|
1074
|
+
if (!mergeRows || !canPersist(connection) || changes.data === undefined)
|
|
1075
|
+
return;
|
|
1076
|
+
if (!changes.metaChanged && !changes.upserts.length && !changes.deletes.length)
|
|
1077
|
+
return;
|
|
1078
|
+
const current = () => !disposed && writingStore === store && incarnation === storeVersion && !writingStore.deleted;
|
|
1079
|
+
const now = Date.now();
|
|
1080
|
+
void updateStore(writingStore, current, () => mergeRows(view.storeId, {
|
|
1081
|
+
entry: {
|
|
1082
|
+
id: view.storeId,
|
|
1083
|
+
data: changes.data,
|
|
1084
|
+
observedAt: now,
|
|
1085
|
+
accessedAt: now,
|
|
1086
|
+
bytes: changes.bytes,
|
|
1087
|
+
},
|
|
1088
|
+
upserts: changes.upserts.map(({ id, version, body, bytes }) => ({
|
|
1089
|
+
id,
|
|
1090
|
+
version,
|
|
1091
|
+
projection: {},
|
|
1092
|
+
body,
|
|
1093
|
+
bytes,
|
|
1094
|
+
})),
|
|
1095
|
+
deletes: changes.deletes,
|
|
1096
|
+
}));
|
|
1097
|
+
}
|
|
1098
|
+
/** An entity view after one sync: pages publish their rows as they land. */
|
|
1099
|
+
async function readEntityView(entry, view, signal) {
|
|
1100
|
+
// What a full read put on screen is the newest this client holds: it seeds an empty view,
|
|
1101
|
+
// and the listing alone says which rows have gone. Stored rows fill a view nothing seeded.
|
|
1102
|
+
view.seed(entry.state.data);
|
|
1103
|
+
if (!view.held)
|
|
1104
|
+
await hydrateEntityView(view);
|
|
1105
|
+
// The read was cancelled while the store answered (a new connection): it sends nothing.
|
|
1106
|
+
if (signal.aborted)
|
|
1107
|
+
throw Object.assign(new Error('Entity view read superseded'), {
|
|
1108
|
+
code: 'RESOURCE_READ_SUPERSEDED',
|
|
1109
|
+
});
|
|
1110
|
+
let requested = false;
|
|
1111
|
+
const publishPage = (done, total) => {
|
|
1112
|
+
const partial = view.answer();
|
|
1113
|
+
entry.state = {
|
|
1114
|
+
...entry.state,
|
|
1115
|
+
...(partial ? { data: partial.data } : {}),
|
|
1116
|
+
catchUpProgress: { done, total },
|
|
1117
|
+
};
|
|
1118
|
+
notify(entry);
|
|
1119
|
+
};
|
|
1120
|
+
try {
|
|
1121
|
+
await view.sync(async (params, readSignal) => {
|
|
1122
|
+
requested = true;
|
|
1123
|
+
return (await request(entry.query.method, params, readSignal)).data;
|
|
1124
|
+
}, publishPage);
|
|
1125
|
+
}
|
|
1126
|
+
catch (error) {
|
|
1127
|
+
if (evictionCodes.has(queryError(error).code)) {
|
|
1128
|
+
const evictingStore = store;
|
|
1129
|
+
view.clear();
|
|
1130
|
+
void updateStore(evictingStore, () => evictingStore === store && !evictingStore.deleted, () => evictingStore.deleteQuery(view.storeId), false);
|
|
1131
|
+
}
|
|
1132
|
+
throw error;
|
|
1133
|
+
}
|
|
1134
|
+
finally {
|
|
1135
|
+
if (entry.state.catchUpProgress !== undefined)
|
|
1136
|
+
entry.state = { ...entry.state, catchUpProgress: undefined };
|
|
1137
|
+
}
|
|
1138
|
+
persistEntityView(view);
|
|
1139
|
+
const answer = view.answer();
|
|
1140
|
+
if (!answer)
|
|
1141
|
+
return request(entry.query.method, entry.query.params, signal);
|
|
1142
|
+
return {
|
|
1143
|
+
data: answer.data,
|
|
1144
|
+
resourceState: appliedRevisionStamp(entry),
|
|
1145
|
+
...(answer.current ? {} : { incomplete: true }),
|
|
1146
|
+
...(requested ? {} : { assembled: true }),
|
|
1147
|
+
observedAt: Date.now(),
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
852
1150
|
function isRunListView(query) {
|
|
853
1151
|
return query.method === Methods.RUN_LIST && !isRunListCollectionRead(query.params);
|
|
854
1152
|
}
|
|
@@ -887,8 +1185,17 @@ export function createGatewayResourceCache(options) {
|
|
|
887
1185
|
const flags = runListFlags();
|
|
888
1186
|
try {
|
|
889
1187
|
// A new view of a current collection needs no read: no runs change since its version reply.
|
|
890
|
-
if (!runList.complete ||
|
|
1188
|
+
if (!runList.complete ||
|
|
1189
|
+
!runList.serves(flags) ||
|
|
1190
|
+
!stampCovers(entry, runList.resourceState)) {
|
|
1191
|
+
const refreshingStore = store;
|
|
1192
|
+
const incarnation = storeVersion;
|
|
891
1193
|
await runList.refresh(flags);
|
|
1194
|
+
// Earlier pages cannot prove that the failed later page can be stored.
|
|
1195
|
+
// Confirm only a complete refresh that never switched its store.
|
|
1196
|
+
if (store === refreshingStore && storeVersion === incarnation && runList.complete)
|
|
1197
|
+
confirmPersistentWrite(refreshingStore, RUN_LIST_COLLECTION_ID);
|
|
1198
|
+
}
|
|
892
1199
|
}
|
|
893
1200
|
catch (error) {
|
|
894
1201
|
// A refused collection read evicts the stored collection, like every refused read.
|
|
@@ -1033,11 +1340,14 @@ export function createGatewayResourceCache(options) {
|
|
|
1033
1340
|
entry.lastRequestedAt = performance.now();
|
|
1034
1341
|
const fromRunList = isRunListView(entry.query) &&
|
|
1035
1342
|
connection.capabilities?.resourceChanges?.versionLists === true;
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1343
|
+
const entityView = entityViewFor(entry.query);
|
|
1344
|
+
const reply = entityView
|
|
1345
|
+
? await readEntityView(entry, entityView, pending.controller.signal)
|
|
1346
|
+
: fromRunList
|
|
1347
|
+
? await readRunListView(entry, pending.controller.signal)
|
|
1348
|
+
: entry.query.method === Methods.NATIVE_SESSION_READ
|
|
1349
|
+
? await readNativeHistory(entry.query.params, entry.state.data, (params, signal) => request(Methods.NATIVE_SESSION_READ, params, signal), pending.controller.signal)
|
|
1350
|
+
: await request(entry.query.method, entry.query.params, pending.controller.signal);
|
|
1041
1351
|
if (disposed ||
|
|
1042
1352
|
store.deleted ||
|
|
1043
1353
|
pending.version !== version ||
|
|
@@ -1094,7 +1404,16 @@ export function createGatewayResourceCache(options) {
|
|
|
1094
1404
|
entry.retryAt = undefined;
|
|
1095
1405
|
entry.lastUsed = ++lastUsed;
|
|
1096
1406
|
// Version-list views are derived from the run collection, which is stored on its own.
|
|
1097
|
-
|
|
1407
|
+
// A view assembled from pushed entities already stored the rows that changed; its whole
|
|
1408
|
+
// snapshot follows no more often than a full read would have been reread.
|
|
1409
|
+
const paced = reply.assembled === true &&
|
|
1410
|
+
Date.now() - (entry.snapshotStoredAt ?? 0) < ENTITY_SNAPSHOT_INTERVAL_MS;
|
|
1411
|
+
const persistResult = entry.query.persisted &&
|
|
1412
|
+
canPersist(connection) &&
|
|
1413
|
+
reply.incomplete === undefined &&
|
|
1414
|
+
!paced;
|
|
1415
|
+
if (persistResult)
|
|
1416
|
+
entry.snapshotStoredAt = Date.now();
|
|
1098
1417
|
const serialized = persistResult
|
|
1099
1418
|
? JSON.stringify(entry.query.persistedData ? entry.query.persistedData(reply.data) : reply.data)
|
|
1100
1419
|
: undefined;
|
|
@@ -1172,6 +1491,8 @@ export function createGatewayResourceCache(options) {
|
|
|
1172
1491
|
.filter((value) => value.id !== saved.id &&
|
|
1173
1492
|
// The run collection is bounded by its own body budget, never by its views.
|
|
1174
1493
|
value.id !== RUN_LIST_COLLECTION_ID &&
|
|
1494
|
+
// Entity rows are bounded by their collection, never by its views.
|
|
1495
|
+
!isEntityStoreId(value.id) &&
|
|
1175
1496
|
(retention.cohort === 'upstream'
|
|
1176
1497
|
? isUpstreamStoredQuery(value.id)
|
|
1177
1498
|
: value.id.startsWith(`${entry.query.method}:`)))
|
|
@@ -1195,16 +1516,28 @@ export function createGatewayResourceCache(options) {
|
|
|
1195
1516
|
}
|
|
1196
1517
|
if (current()) {
|
|
1197
1518
|
await writingStore.putQuery(row);
|
|
1519
|
+
if (current())
|
|
1520
|
+
confirmPersistentWrite(writingStore, row.id);
|
|
1198
1521
|
if (entry.query.method === Methods.CHAT_ARCHIVE && current())
|
|
1199
1522
|
await writingStore.deleteQuery(`chat.history:${canonicalResourceJson(entry.query.params)}`);
|
|
1200
1523
|
}
|
|
1201
|
-
});
|
|
1524
|
+
}, true, nativeHistoryStorageId(entry.query));
|
|
1202
1525
|
}
|
|
1203
1526
|
if (store.persistent && entry.query.method === Methods.RUN_LIST) {
|
|
1204
1527
|
const listed = reply.data;
|
|
1205
1528
|
if (Array.isArray(listed.runs)) {
|
|
1206
|
-
for (const run of listed.runs)
|
|
1529
|
+
for (const run of listed.runs) {
|
|
1530
|
+
// A slim row (it carries the gateway's evidenceSummary) hashes to no version the
|
|
1531
|
+
// gateway serves: its version comes only from the version list, and until that is
|
|
1532
|
+
// held its detail is not treated as changed. An older gateway's trimmed full row is
|
|
1533
|
+
// the version basis itself, so it keeps the derived version. Trim markers cannot
|
|
1534
|
+
// tell the two apart: trimRunForList rows carry payloadTrimmed too.
|
|
1535
|
+
if (run.evidenceSummary !== undefined) {
|
|
1536
|
+
fallbackDetailTargets.delete(run.id);
|
|
1537
|
+
continue;
|
|
1538
|
+
}
|
|
1207
1539
|
fallbackDetailTargets.set(run.id, runDetailTarget(run, runListVersion(run)));
|
|
1540
|
+
}
|
|
1208
1541
|
}
|
|
1209
1542
|
refreshDetailTargets();
|
|
1210
1543
|
}
|
|
@@ -1528,12 +1861,14 @@ export function createGatewayResourceCache(options) {
|
|
|
1528
1861
|
});
|
|
1529
1862
|
}
|
|
1530
1863
|
deliver(entry, receive);
|
|
1864
|
+
notifySync();
|
|
1531
1865
|
if (entry.state.status !== 'ready' || entry.dirty)
|
|
1532
1866
|
schedule(entry);
|
|
1533
1867
|
return () => {
|
|
1534
1868
|
active = false;
|
|
1535
1869
|
cancelPaint?.();
|
|
1536
1870
|
entry.listeners.delete(receive);
|
|
1871
|
+
notifySync();
|
|
1537
1872
|
updateCollectors();
|
|
1538
1873
|
if (!entry.listeners.size) {
|
|
1539
1874
|
cancelScheduledRead(entry);
|
|
@@ -1544,6 +1879,13 @@ export function createGatewayResourceCache(options) {
|
|
|
1544
1879
|
prune();
|
|
1545
1880
|
};
|
|
1546
1881
|
},
|
|
1882
|
+
observeSync(listener, match) {
|
|
1883
|
+
syncListeners.set(listener, match);
|
|
1884
|
+
notifySync();
|
|
1885
|
+
return () => {
|
|
1886
|
+
syncListeners.delete(listener);
|
|
1887
|
+
};
|
|
1888
|
+
},
|
|
1547
1889
|
read(query) {
|
|
1548
1890
|
if (disposed)
|
|
1549
1891
|
throw new Error('Resource cache is disposed');
|
|
@@ -1614,9 +1956,11 @@ export function createGatewayResourceCache(options) {
|
|
|
1614
1956
|
entry.listeners.clear();
|
|
1615
1957
|
}
|
|
1616
1958
|
entries.clear();
|
|
1959
|
+
syncListeners.clear();
|
|
1617
1960
|
await flushingConversations;
|
|
1618
1961
|
conversationFlush = false;
|
|
1619
1962
|
await barrier;
|
|
1963
|
+
await storeOpening;
|
|
1620
1964
|
try {
|
|
1621
1965
|
if (reason && deletionReasons.has(reason))
|
|
1622
1966
|
await store.deleteAll();
|