@farmslot/protocol 0.75.0 → 0.77.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 +32 -0
- package/dist/client-cache/cache.d.ts.map +1 -1
- package/dist/client-cache/cache.js +198 -21
- 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 +3 -0
- package/dist/client-cache/command-method.js.map +1 -1
- package/dist/client-cache/index.d.ts +1 -1
- package/dist/client-cache/index.d.ts.map +1 -1
- package/dist/client-cache/index.js +1 -1
- package/dist/client-cache/index.js.map +1 -1
- package/dist/client-cache/prefetch.d.ts +13 -1
- package/dist/client-cache/prefetch.d.ts.map +1 -1
- package/dist/client-cache/prefetch.js +88 -26
- package/dist/client-cache/prefetch.js.map +1 -1
- package/dist/client-cache/read-stored.d.ts +5 -1
- package/dist/client-cache/read-stored.d.ts.map +1 -1
- package/dist/client-cache/read-stored.js +64 -1
- package/dist/client-cache/read-stored.js.map +1 -1
- package/dist/client-cache/registry.d.ts +29 -1
- package/dist/client-cache/registry.d.ts.map +1 -1
- package/dist/client-cache/registry.js +55 -4
- package/dist/client-cache/registry.js.map +1 -1
- package/dist/client-cache/store.d.ts +4 -0
- package/dist/client-cache/store.d.ts.map +1 -1
- package/dist/client-cache/store.js +9 -1
- package/dist/client-cache/store.js.map +1 -1
- package/dist/client-cache/transport.d.ts +3 -1
- package/dist/client-cache/transport.d.ts.map +1 -1
- package/dist/client-cache/transport.js +2 -1
- package/dist/client-cache/transport.js.map +1 -1
- package/dist/client-cache/types.d.ts +8 -1
- package/dist/client-cache/types.d.ts.map +1 -1
- package/dist/contracts/agents.d.ts +5 -2
- package/dist/contracts/agents.d.ts.map +1 -1
- package/dist/contracts/agents.js.map +1 -1
- package/dist/contracts/config.d.ts +3 -0
- package/dist/contracts/config.d.ts.map +1 -1
- package/dist/contracts/config.js +5 -1
- package/dist/contracts/config.js.map +1 -1
- package/dist/contracts/family.d.ts +55 -3
- package/dist/contracts/family.d.ts.map +1 -1
- package/dist/contracts/family.js.map +1 -1
- package/dist/contracts/review-workspace.d.ts +23 -1
- package/dist/contracts/review-workspace.d.ts.map +1 -1
- package/dist/contracts/review-workspace.js +3 -0
- package/dist/contracts/review-workspace.js.map +1 -1
- package/dist/contracts/reviews.d.ts +1 -0
- package/dist/contracts/reviews.d.ts.map +1 -1
- package/dist/contracts/reviews.js.map +1 -1
- package/dist/contracts/runs.d.ts +47 -5
- package/dist/contracts/runs.d.ts.map +1 -1
- package/dist/contracts/runs.js.map +1 -1
- package/dist/contracts/runtime-capabilities.d.ts +9 -0
- package/dist/contracts/runtime-capabilities.d.ts.map +1 -1
- package/dist/contracts/runtime-capabilities.js.map +1 -1
- package/dist/contracts/slot-recovery.d.ts +2 -2
- package/dist/contracts/slot-recovery.d.ts.map +1 -1
- package/dist/contracts/slot-recovery.js +5 -3
- package/dist/contracts/slot-recovery.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/performance/client.d.ts +18 -0
- package/dist/performance/client.d.ts.map +1 -0
- package/dist/performance/client.js +66 -0
- package/dist/performance/client.js.map +1 -0
- package/dist/performance/histogram.d.ts +5 -0
- package/dist/performance/histogram.d.ts.map +1 -0
- package/dist/performance/histogram.js +38 -0
- package/dist/performance/histogram.js.map +1 -0
- package/dist/performance/index.d.ts +10 -0
- package/dist/performance/index.d.ts.map +1 -0
- package/dist/performance/index.js +10 -0
- package/dist/performance/index.js.map +1 -0
- package/dist/performance/paint.d.ts +3 -0
- package/dist/performance/paint.d.ts.map +1 -0
- package/dist/performance/paint.js +14 -0
- package/dist/performance/paint.js.map +1 -0
- package/dist/performance/prometheus.d.ts +3 -0
- package/dist/performance/prometheus.d.ts.map +1 -0
- package/dist/performance/prometheus.js +53 -0
- package/dist/performance/prometheus.js.map +1 -0
- package/dist/performance/ranking.d.ts +26 -0
- package/dist/performance/ranking.d.ts.map +1 -0
- package/dist/performance/ranking.js +92 -0
- package/dist/performance/ranking.js.map +1 -0
- package/dist/performance/recorder.d.ts +43 -0
- package/dist/performance/recorder.d.ts.map +1 -0
- package/dist/performance/recorder.js +352 -0
- package/dist/performance/recorder.js.map +1 -0
- package/dist/performance/types.d.ts +93 -0
- package/dist/performance/types.d.ts.map +1 -0
- package/dist/performance/types.js +40 -0
- package/dist/performance/types.js.map +1 -0
- package/dist/performance/validation.d.ts +3 -0
- package/dist/performance/validation.d.ts.map +1 -0
- package/dist/performance/validation.js +55 -0
- package/dist/performance/validation.js.map +1 -0
- package/dist/recipes/step-io.d.ts +2 -0
- package/dist/recipes/step-io.d.ts.map +1 -1
- package/dist/recipes/step-io.js.map +1 -1
- package/dist/rpc/auth.d.ts +1 -0
- package/dist/rpc/auth.d.ts.map +1 -1
- package/dist/rpc/auth.js.map +1 -1
- package/dist/rpc/fs.d.ts +2 -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 +18 -0
- package/dist/rpc/native-session.d.ts.map +1 -1
- package/dist/rpc/native-session.js.map +1 -1
- package/dist/rpc/performance.d.ts +6 -0
- package/dist/rpc/performance.d.ts.map +1 -0
- package/dist/rpc/performance.js +2 -0
- package/dist/rpc/performance.js.map +1 -0
- package/dist/rpc/registry.d.ts +3 -0
- package/dist/rpc/registry.d.ts.map +1 -1
- package/dist/rpc/registry.js +3 -0
- package/dist/rpc/registry.js.map +1 -1
- package/dist/rpc/run.d.ts +4 -0
- package/dist/rpc/run.d.ts.map +1 -1
- package/dist/rpc/runtime-capabilities.d.ts +6 -0
- package/dist/rpc/runtime-capabilities.d.ts.map +1 -1
- package/dist/rpc/runtime-capabilities.js.map +1 -1
- package/dist/rpc/slot.d.ts +2 -0
- package/dist/rpc/slot.d.ts.map +1 -1
- package/dist/rpc/tmux.d.ts +9 -1
- package/dist/rpc/tmux.d.ts.map +1 -1
- package/dist/runs/list-trim.d.ts.map +1 -1
- package/dist/runs/list-trim.js +1 -19
- package/dist/runs/list-trim.js.map +1 -1
- package/dist/surfaces/command-center.js +1 -1
- package/dist/surfaces/command-center.js.map +1 -1
- package/dist/surfaces/run-view-state.d.ts +22 -0
- package/dist/surfaces/run-view-state.d.ts.map +1 -0
- package/dist/surfaces/run-view-state.js +26 -0
- package/dist/surfaces/run-view-state.js.map +1 -0
- package/dist/surfaces/view-links.d.ts +2 -1
- package/dist/surfaces/view-links.d.ts.map +1 -1
- package/dist/surfaces/view-links.js +161 -18
- package/dist/surfaces/view-links.js.map +1 -1
- package/dist/surfaces/view-state.d.ts +50 -0
- package/dist/surfaces/view-state.d.ts.map +1 -0
- package/dist/surfaces/view-state.js +181 -0
- package/dist/surfaces/view-state.js.map +1 -0
- package/dist/transport/gateway-recovery.d.ts +39 -0
- package/dist/transport/gateway-recovery.d.ts.map +1 -0
- package/dist/transport/gateway-recovery.js +134 -0
- package/dist/transport/gateway-recovery.js.map +1 -0
- package/dist/utf8-byte-length.d.ts +2 -0
- package/dist/utf8-byte-length.d.ts.map +1 -0
- package/dist/utf8-byte-length.js +22 -0
- package/dist/utf8-byte-length.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,38 @@ All notable changes to `@farmslot/protocol` are tracked here.
|
|
|
6
6
|
|
|
7
7
|
- Active-development baseline; add user-facing changes here before release or package publication.
|
|
8
8
|
|
|
9
|
+
## 0.77.0 - 2026-11-06
|
|
10
|
+
|
|
11
|
+
- Add a family usage projection for shared session receipts, accounting contributions and review-round timing.
|
|
12
|
+
- Measure shared RPC and offline cache performance, with authenticated diagnostics and action budgets.
|
|
13
|
+
- Share context-configured display state across clients and expose bounded resident content previews for cached first paint.
|
|
14
|
+
- Preserve structured missing-file errors through display reads.
|
|
15
|
+
- Restore the Review History choice from shared links and retain it across native tab navigation.
|
|
16
|
+
- Give current run reads a bounded two-minute window for publication-source and retained-evidence verification, so a slow read can refresh a ready gate.
|
|
17
|
+
- Add shared pipeline session usage receipts with runner, model, effort, role, flow, identity, token splits, pricing status and duration, retained across relaunches and replays.
|
|
18
|
+
- `run.extendReviewBudget` takes an optional `slotId`: the slot a failed run whose slot was released re-binds at the approved head.
|
|
19
|
+
- Clarify that farm-wide worker discovery skips disconnected machines and pools with no enabled slots.
|
|
20
|
+
- Retain watched check failure conclusions and CI fix scope refusals for operator decisions.
|
|
21
|
+
- Allow the four scripted recovery behaviours in scenario configuration.
|
|
22
|
+
- Declare bounded shared runtime claims and project recording capability metadata.
|
|
23
|
+
- Record when a pending ticket refresh is superseded so its history survives a fresh admission.
|
|
24
|
+
- Add opt-in ticket scope refresh and its person action history to publication package refresh.
|
|
25
|
+
- Retain slotless review workspaces and exact reviewer conversations for a configurable project window, default seven days, so re-reviews resume the prior session on only the new diff. Expired storage is reclaimed safely and expiry is reported before a cold review.
|
|
26
|
+
- Runs record `requestedSlotId`, the slot a caller or operator explicitly asked for, separately from the slot the engine placed them on.
|
|
27
|
+
- Share bounded reconnect backoff, liveness probes and lifecycle/network recovery across Command Center and Companion.
|
|
28
|
+
- Resume interrupted recovery without an identity from the client. Keep live owners and local work protected, and park unpublished commits retained by a terminal owner.
|
|
29
|
+
- Open held review source without rereading it and batch missing source reads during prefetch.
|
|
30
|
+
|
|
31
|
+
## 0.76.0 - 2026-11-04
|
|
32
|
+
|
|
33
|
+
- Define admin-person standalone session requester attribution, execution-target availability and real home metadata.
|
|
34
|
+
- Start native conversations from a prompt, a filtered workspace or custom directory on a catalogued machine, and shared harness/model/effort controls.
|
|
35
|
+
- Add machine-scoped creation of isolated terminal sessions.
|
|
36
|
+
- Preserve run artifact browser filters and file selection in shared desktop view links.
|
|
37
|
+
- Run records expose decisionHistoryCount for retired decision rows stored separately from routine run reads and events.
|
|
38
|
+
- Preserve expanded review-round selections in shared run links across Command Center and Companion.
|
|
39
|
+
- Serialize cache admission and eviction across browser tabs sharing a durable store so their combined data stays within the configured budgets.
|
|
40
|
+
|
|
9
41
|
## 0.75.0 - 2026-11-03
|
|
10
42
|
|
|
11
43
|
- `SlotReleaseParams.skipArtifacts` is removed: a slot release no longer copies artifacts, so there is nothing to skip (F313).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/client-cache/cache.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/client-cache/cache.ts"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EACV,oBAAoB,EACpB,2BAA2B,EAM3B,kBAAkB,EAInB,MAAM,YAAY,CAAC;AAwDpB;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,kBAAkB,GAAG,SAAS,GAAG,OAAO,CAE1E;AA8CD,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,2BAA2B,GACnC,oBAAoB,CAoiDtB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { afterForegroundPaint } from '../performance/paint.js';
|
|
1
2
|
import { Methods } from '../rpc/registry.js';
|
|
2
3
|
import { runListVersion } from '../runs/list-version.js';
|
|
3
4
|
import { ResourceCollectorAdmission } from './collector-admission.js';
|
|
@@ -64,6 +65,36 @@ function profileId(connection) {
|
|
|
64
65
|
}
|
|
65
66
|
export function createGatewayResourceCache(options) {
|
|
66
67
|
const transport = options.transport;
|
|
68
|
+
const metrics = options.performance ?? transport.performance;
|
|
69
|
+
const readContexts = new WeakMap();
|
|
70
|
+
const collectionIdentities = new Map();
|
|
71
|
+
function request(method, params, signal) {
|
|
72
|
+
const context = readContexts.get(signal);
|
|
73
|
+
let identity = context?.identity ?? collectionIdentities.get(method);
|
|
74
|
+
if (!identity) {
|
|
75
|
+
identity = {};
|
|
76
|
+
collectionIdentities.set(method, identity);
|
|
77
|
+
}
|
|
78
|
+
metrics?.reread(method, identity);
|
|
79
|
+
return transport.request(method, params, signal, context?.blocking ? 'interactive-read' : 'background-refresh');
|
|
80
|
+
}
|
|
81
|
+
function recordRead(entry, source, blocking) {
|
|
82
|
+
if (!metrics)
|
|
83
|
+
return;
|
|
84
|
+
const labels = { method: entry.query.method, cache_source: source };
|
|
85
|
+
metrics.increment('farmslot_client_cache_reads_total', labels);
|
|
86
|
+
if (entry.state.status === 'stale')
|
|
87
|
+
metrics.increment('farmslot_client_stale_reads_total', labels);
|
|
88
|
+
if (blocking)
|
|
89
|
+
metrics.increment('farmslot_client_blocking_reads_total', labels);
|
|
90
|
+
const age = entry.state.observedAt === undefined
|
|
91
|
+
? undefined
|
|
92
|
+
: (Date.now() - entry.state.observedAt) / 1000;
|
|
93
|
+
if (age !== undefined && Number.isFinite(age) && age >= 0)
|
|
94
|
+
metrics.observe('farmslot_client_cache_age_seconds', labels, age);
|
|
95
|
+
else
|
|
96
|
+
metrics.increment('farmslot_client_unknown_age_total', labels);
|
|
97
|
+
}
|
|
67
98
|
let store = options.store ?? createMemoryResourceStore();
|
|
68
99
|
let metadata;
|
|
69
100
|
let startupStore;
|
|
@@ -75,6 +106,35 @@ export function createGatewayResourceCache(options) {
|
|
|
75
106
|
let version = 0;
|
|
76
107
|
let storeVersion = 0;
|
|
77
108
|
let hydrationOwnerVersion = 0;
|
|
109
|
+
// A bounded working set of the store's aliased bytes, for synchronous cached paint.
|
|
110
|
+
// Eviction and authority changes invalidate it with the durable rows.
|
|
111
|
+
const residentContent = new Map();
|
|
112
|
+
const residentLimit = Math.min(options.contentBudgetBytes ?? defaultContentBudgetBytes, 8 * 1024 * 1024);
|
|
113
|
+
let residentVersion = 0;
|
|
114
|
+
function forgetResident(key) {
|
|
115
|
+
residentVersion++;
|
|
116
|
+
for (const [alias, value] of residentContent)
|
|
117
|
+
if (value.key.kind === key.kind && value.key.id === key.id)
|
|
118
|
+
residentContent.delete(alias);
|
|
119
|
+
}
|
|
120
|
+
async function removeStoredContent(target, key) {
|
|
121
|
+
forgetResident(key);
|
|
122
|
+
await target.deleteContent(key);
|
|
123
|
+
forgetResident(key);
|
|
124
|
+
}
|
|
125
|
+
function rememberResident(alias, key, data) {
|
|
126
|
+
residentContent.delete(alias);
|
|
127
|
+
if (data.byteLength > residentLimit)
|
|
128
|
+
return;
|
|
129
|
+
residentContent.set(alias, { key, data: data.slice() });
|
|
130
|
+
let bytes = [...residentContent.values()].reduce((sum, value) => sum + value.data.byteLength, 0);
|
|
131
|
+
for (const [old, value] of residentContent) {
|
|
132
|
+
if (bytes <= residentLimit)
|
|
133
|
+
break;
|
|
134
|
+
residentContent.delete(old);
|
|
135
|
+
bytes -= value.data.byteLength;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
78
138
|
let lastUsed = 0;
|
|
79
139
|
let storageNotice;
|
|
80
140
|
let needsStore = options.store === undefined && options.openStore !== undefined;
|
|
@@ -89,7 +149,7 @@ export function createGatewayResourceCache(options) {
|
|
|
89
149
|
let unsubscribeStore = store.onDeleted(storeDeleted);
|
|
90
150
|
// Version lists: one shared run collection answers every run.list view.
|
|
91
151
|
const runList = new RunListCollection({
|
|
92
|
-
request: (params, signal) =>
|
|
152
|
+
request: (params, signal) => request(Methods.RUN_LIST, params, signal),
|
|
93
153
|
persist: (merge) => {
|
|
94
154
|
const writingStore = store;
|
|
95
155
|
const incarnation = storeVersion;
|
|
@@ -142,6 +202,7 @@ export function createGatewayResourceCache(options) {
|
|
|
142
202
|
return;
|
|
143
203
|
usable = false;
|
|
144
204
|
updateCollectors();
|
|
205
|
+
residentContent.clear();
|
|
145
206
|
hydrationOwnerVersion++;
|
|
146
207
|
version++;
|
|
147
208
|
forgetRunList();
|
|
@@ -151,6 +212,7 @@ export function createGatewayResourceCache(options) {
|
|
|
151
212
|
}
|
|
152
213
|
function replaceStore(next) {
|
|
153
214
|
unsubscribeStore();
|
|
215
|
+
residentContent.clear();
|
|
154
216
|
store = next;
|
|
155
217
|
storeVersion++;
|
|
156
218
|
startupStore = undefined;
|
|
@@ -232,13 +294,24 @@ export function createGatewayResourceCache(options) {
|
|
|
232
294
|
for (const entry of entries.values())
|
|
233
295
|
notify(entry);
|
|
234
296
|
}
|
|
297
|
+
function withStoreWriteLock(target, write) {
|
|
298
|
+
return target.withWriteLock ? target.withWriteLock(write) : write();
|
|
299
|
+
}
|
|
235
300
|
/** Retention, writes and lifecycle changes share one ordered store boundary. */
|
|
236
|
-
function updateStore(target, current, update) {
|
|
301
|
+
function updateStore(target, current, update, lockWrites = true) {
|
|
237
302
|
const updating = barrier.then(async () => {
|
|
238
303
|
if (!current())
|
|
239
304
|
return;
|
|
240
305
|
try {
|
|
241
|
-
|
|
306
|
+
const write = async () => {
|
|
307
|
+
// A queued cross-tab write must recheck the owner after acquiring its lock.
|
|
308
|
+
if (current())
|
|
309
|
+
await update();
|
|
310
|
+
};
|
|
311
|
+
if (lockWrites)
|
|
312
|
+
await withStoreWriteLock(target, write);
|
|
313
|
+
else
|
|
314
|
+
await write();
|
|
242
315
|
}
|
|
243
316
|
catch (error) {
|
|
244
317
|
await recoverStore(error, target);
|
|
@@ -317,6 +390,27 @@ export function createGatewayResourceCache(options) {
|
|
|
317
390
|
if (saved.id !== RUN_LIST_COLLECTION_ID)
|
|
318
391
|
startupQueries.set(saved.id, saved);
|
|
319
392
|
}
|
|
393
|
+
// Restore small aliased report bodies before exposing their cached run. Network
|
|
394
|
+
// prefetch can be queued behind other gates, even when these bytes are already held.
|
|
395
|
+
let contentRemaining = residentLimit;
|
|
396
|
+
const content = (await readingStore.listContent())
|
|
397
|
+
.filter((entry) => entry.kind === 'artifact' && entry.aliases?.length)
|
|
398
|
+
.sort((a, b) => b.accessedAt - a.accessedAt);
|
|
399
|
+
if (!current())
|
|
400
|
+
return;
|
|
401
|
+
for (const info of content) {
|
|
402
|
+
const aliasCount = info.aliases?.length ?? 0;
|
|
403
|
+
if (info.bytes * aliasCount > contentRemaining)
|
|
404
|
+
continue;
|
|
405
|
+
const saved = await readingStore.getContent(info);
|
|
406
|
+
if (!current())
|
|
407
|
+
return;
|
|
408
|
+
if (!saved || saved.data.byteLength * aliasCount > contentRemaining)
|
|
409
|
+
continue;
|
|
410
|
+
contentRemaining -= saved.data.byteLength * aliasCount;
|
|
411
|
+
for (const name of info.aliases ?? [])
|
|
412
|
+
rememberResident(name, saved, saved.data);
|
|
413
|
+
}
|
|
320
414
|
if (rows)
|
|
321
415
|
runList.hydrate(rows);
|
|
322
416
|
startupStore = readingStore;
|
|
@@ -367,6 +461,8 @@ export function createGatewayResourceCache(options) {
|
|
|
367
461
|
// Stored facts belong to an owner, not a socket attempt. Fence a revoked
|
|
368
462
|
// owner's pending I/O synchronously, before queued deletion can run.
|
|
369
463
|
if (revoke || scopeChanged || authorityChanged) {
|
|
464
|
+
metrics?.resetScope();
|
|
465
|
+
residentContent.clear();
|
|
370
466
|
hydrationOwnerVersion++;
|
|
371
467
|
forgetRunList();
|
|
372
468
|
}
|
|
@@ -410,6 +506,7 @@ export function createGatewayResourceCache(options) {
|
|
|
410
506
|
switchingProfile ||
|
|
411
507
|
((next.connected || next.authority !== undefined) &&
|
|
412
508
|
(storedScopeChanged || storedAuthorityChanged))) {
|
|
509
|
+
residentContent.clear();
|
|
413
510
|
storeVersion++;
|
|
414
511
|
forgetRunList();
|
|
415
512
|
for (const entry of entries.values())
|
|
@@ -453,9 +550,11 @@ export function createGatewayResourceCache(options) {
|
|
|
453
550
|
if (store.persistent && retentionSweptStore !== store) {
|
|
454
551
|
const target = store;
|
|
455
552
|
try {
|
|
456
|
-
|
|
457
|
-
await target.
|
|
458
|
-
|
|
553
|
+
await withStoreWriteLock(target, async () => {
|
|
554
|
+
for (const id of upstreamRetentionEvictions(await target.listQueries()))
|
|
555
|
+
await target.deleteQuery(id);
|
|
556
|
+
retentionSweptStore = target;
|
|
557
|
+
});
|
|
459
558
|
}
|
|
460
559
|
catch (error) {
|
|
461
560
|
await recoverStore(error, target);
|
|
@@ -561,7 +660,7 @@ export function createGatewayResourceCache(options) {
|
|
|
561
660
|
};
|
|
562
661
|
entries.set(query.id, entry);
|
|
563
662
|
const saved = isRunListView(query) ? heldRunListView(query) : takeStartupQuery(query);
|
|
564
|
-
if (applyStoredQuery(entry, saved)) {
|
|
663
|
+
if (applyStoredQuery(entry, saved, 'memory')) {
|
|
565
664
|
entry.hydrated = true;
|
|
566
665
|
queueMicrotask(prune);
|
|
567
666
|
}
|
|
@@ -577,7 +676,7 @@ export function createGatewayResourceCache(options) {
|
|
|
577
676
|
startupQueries.delete(id);
|
|
578
677
|
return saved;
|
|
579
678
|
}
|
|
580
|
-
function applyStoredQuery(entry, saved) {
|
|
679
|
+
function applyStoredQuery(entry, saved, origin = 'stored') {
|
|
581
680
|
const query = entry.query;
|
|
582
681
|
if (!saved ||
|
|
583
682
|
!query.persisted ||
|
|
@@ -585,6 +684,7 @@ export function createGatewayResourceCache(options) {
|
|
|
585
684
|
(query.method === Methods.NATIVE_SESSION_READ &&
|
|
586
685
|
!storedNativeHistoryMatches(query.params, saved.data)))
|
|
587
686
|
return false;
|
|
687
|
+
entry.metricSource = origin;
|
|
588
688
|
const sourceObservation = query.sourceObservation?.(saved.data);
|
|
589
689
|
entry.state = {
|
|
590
690
|
status: 'stale',
|
|
@@ -679,7 +779,9 @@ export function createGatewayResourceCache(options) {
|
|
|
679
779
|
if (evictionCodes.has(queryError(error).code)) {
|
|
680
780
|
const evictingStore = store;
|
|
681
781
|
forgetRunList();
|
|
682
|
-
void updateStore(evictingStore, () => evictingStore === store && !evictingStore.deleted, () => evictingStore.deleteQuery(RUN_LIST_COLLECTION_ID)
|
|
782
|
+
void updateStore(evictingStore, () => evictingStore === store && !evictingStore.deleted, () => evictingStore.deleteQuery(RUN_LIST_COLLECTION_ID),
|
|
783
|
+
// Queue one-statement refusal evictions before their callers settle.
|
|
784
|
+
false);
|
|
683
785
|
}
|
|
684
786
|
throw error;
|
|
685
787
|
}
|
|
@@ -693,7 +795,7 @@ export function createGatewayResourceCache(options) {
|
|
|
693
795
|
observedAt: runList.observedAt,
|
|
694
796
|
};
|
|
695
797
|
// A view whose summaries the version reply cannot answer keeps its own full read.
|
|
696
|
-
return
|
|
798
|
+
return request(Methods.RUN_LIST, params, signal);
|
|
697
799
|
}
|
|
698
800
|
function stampCovers(entry, stamp) {
|
|
699
801
|
if (!stamp ||
|
|
@@ -788,6 +890,10 @@ export function createGatewayResourceCache(options) {
|
|
|
788
890
|
prefetch,
|
|
789
891
|
};
|
|
790
892
|
entry.pending = pending;
|
|
893
|
+
const refreshStarted = metrics?.clock();
|
|
894
|
+
const refreshEpoch = metrics?.epoch;
|
|
895
|
+
const blocking = entry.state.data === undefined;
|
|
896
|
+
readContexts.set(pending.controller.signal, { identity: entry, blocking });
|
|
791
897
|
entry.dirty = false;
|
|
792
898
|
entry.rereadFloor = undefined;
|
|
793
899
|
const unknownGeneration = entry.unknownGeneration;
|
|
@@ -814,8 +920,8 @@ export function createGatewayResourceCache(options) {
|
|
|
814
920
|
const reply = fromRunList
|
|
815
921
|
? await readRunListView(entry, pending.controller.signal)
|
|
816
922
|
: entry.query.method === Methods.NATIVE_SESSION_READ
|
|
817
|
-
? await readNativeHistory(entry.query.params, entry.state.data, (params, signal) =>
|
|
818
|
-
: await
|
|
923
|
+
? await readNativeHistory(entry.query.params, entry.state.data, (params, signal) => request(Methods.NATIVE_SESSION_READ, params, signal), pending.controller.signal)
|
|
924
|
+
: await request(entry.query.method, entry.query.params, pending.controller.signal);
|
|
819
925
|
if (disposed ||
|
|
820
926
|
store.deleted ||
|
|
821
927
|
pending.version !== version ||
|
|
@@ -844,6 +950,7 @@ export function createGatewayResourceCache(options) {
|
|
|
844
950
|
throw Object.assign(new Error(sourceObservation.error?.message ?? 'The gateway has not observed an upstream value.'), {
|
|
845
951
|
code: sourceObservation.error?.code ?? 'UPSTREAM_UNOBSERVED',
|
|
846
952
|
});
|
|
953
|
+
entry.metricSource = 'network';
|
|
847
954
|
entry.state = {
|
|
848
955
|
status: covered
|
|
849
956
|
? sourceObservation && sourceObservation.status !== 'ready'
|
|
@@ -939,7 +1046,7 @@ export function createGatewayResourceCache(options) {
|
|
|
939
1046
|
return;
|
|
940
1047
|
}
|
|
941
1048
|
for (const key of victims.content) {
|
|
942
|
-
await writingStore
|
|
1049
|
+
await removeStoredContent(writingStore, key);
|
|
943
1050
|
if (!current())
|
|
944
1051
|
return;
|
|
945
1052
|
}
|
|
@@ -1012,7 +1119,9 @@ export function createGatewayResourceCache(options) {
|
|
|
1012
1119
|
// Commit an accepted refusal before a listener queues its profile's close.
|
|
1013
1120
|
const eviction = updateStore(evictingStore, () => evictingStore === store &&
|
|
1014
1121
|
evictingIncarnation === storeVersion &&
|
|
1015
|
-
!evictingStore.deleted, () => evictingStore.deleteQuery(nativeHistoryStorageId(entry.query))
|
|
1122
|
+
!evictingStore.deleted, () => evictingStore.deleteQuery(nativeHistoryStorageId(entry.query)),
|
|
1123
|
+
// Refusals must not wait for another tab's admission lock before enqueueing deletion.
|
|
1124
|
+
false);
|
|
1016
1125
|
notify(entry);
|
|
1017
1126
|
// An authorization refusal settles its callers in the reply's own task, so a prefetch
|
|
1018
1127
|
// stops before a store callback can release another read. The eviction is queued.
|
|
@@ -1038,6 +1147,13 @@ export function createGatewayResourceCache(options) {
|
|
|
1038
1147
|
notify(entry);
|
|
1039
1148
|
}
|
|
1040
1149
|
finally {
|
|
1150
|
+
if (metrics && refreshStarted !== undefined && metrics.epoch === refreshEpoch)
|
|
1151
|
+
metrics.observe(blocking
|
|
1152
|
+
? 'farmslot_client_blocking_refresh_seconds'
|
|
1153
|
+
: 'farmslot_client_background_refresh_seconds', {
|
|
1154
|
+
method: entry.query.method,
|
|
1155
|
+
action_class: blocking ? 'interactive-read' : 'background-refresh',
|
|
1156
|
+
}, metrics.clock() - refreshStarted);
|
|
1041
1157
|
if (entry.pending === pending) {
|
|
1042
1158
|
entry.pending = undefined;
|
|
1043
1159
|
if (entry.dirty && entry.listeners.size && usable)
|
|
@@ -1086,6 +1202,25 @@ export function createGatewayResourceCache(options) {
|
|
|
1086
1202
|
: undefined);
|
|
1087
1203
|
return saved?.data;
|
|
1088
1204
|
}
|
|
1205
|
+
async function readAliasedContent(alias, open) {
|
|
1206
|
+
const version = residentVersion;
|
|
1207
|
+
const owner = hydrationOwnerVersion;
|
|
1208
|
+
await barrier;
|
|
1209
|
+
if (owner !== hydrationOwnerVersion || disposed)
|
|
1210
|
+
return undefined;
|
|
1211
|
+
const incarnation = storeVersion;
|
|
1212
|
+
const saved = await readStoredContent((target) => target.getContentByAlias(alias), open
|
|
1213
|
+
? (target, { kind, id }) => target.touchContent({ kind, id }, Date.now(), 'opened')
|
|
1214
|
+
: undefined);
|
|
1215
|
+
if (version !== residentVersion ||
|
|
1216
|
+
owner !== hydrationOwnerVersion ||
|
|
1217
|
+
incarnation !== storeVersion ||
|
|
1218
|
+
disposed)
|
|
1219
|
+
return undefined;
|
|
1220
|
+
if (saved)
|
|
1221
|
+
rememberResident(alias, saved, saved.data);
|
|
1222
|
+
return saved?.data;
|
|
1223
|
+
}
|
|
1089
1224
|
/** Content writes accept only the current connection's owner, from the token's fetch onward. */
|
|
1090
1225
|
async function writeContent(token, write) {
|
|
1091
1226
|
if (disposed || !connection.connected)
|
|
@@ -1145,7 +1280,7 @@ export function createGatewayResourceCache(options) {
|
|
|
1145
1280
|
return false;
|
|
1146
1281
|
}
|
|
1147
1282
|
for (const victim of victims.content) {
|
|
1148
|
-
await target
|
|
1283
|
+
await removeStoredContent(target, victim);
|
|
1149
1284
|
if (!current())
|
|
1150
1285
|
return false;
|
|
1151
1286
|
}
|
|
@@ -1158,8 +1293,11 @@ export function createGatewayResourceCache(options) {
|
|
|
1158
1293
|
accessedAt,
|
|
1159
1294
|
});
|
|
1160
1295
|
}
|
|
1161
|
-
if (input.alias !== undefined && current())
|
|
1296
|
+
if (input.alias !== undefined && current()) {
|
|
1162
1297
|
await target.putContentAlias(input.alias, key);
|
|
1298
|
+
if (current())
|
|
1299
|
+
rememberResident(input.alias, key, data);
|
|
1300
|
+
}
|
|
1163
1301
|
return true;
|
|
1164
1302
|
});
|
|
1165
1303
|
}
|
|
@@ -1193,8 +1331,38 @@ export function createGatewayResourceCache(options) {
|
|
|
1193
1331
|
observe(query, listener) {
|
|
1194
1332
|
if (disposed)
|
|
1195
1333
|
throw new Error('Resource cache is disposed');
|
|
1334
|
+
const started = metrics?.clock();
|
|
1335
|
+
const epoch = metrics?.epoch;
|
|
1336
|
+
const alreadyHeld = entries.get(query.id)?.state.data !== undefined;
|
|
1196
1337
|
const entry = entryFor(query);
|
|
1197
|
-
const
|
|
1338
|
+
const blocking = entry.state.data === undefined;
|
|
1339
|
+
let first = true, active = true;
|
|
1340
|
+
let cancelPaint;
|
|
1341
|
+
const receive = (state) => {
|
|
1342
|
+
listener(state);
|
|
1343
|
+
if (state.data === undefined)
|
|
1344
|
+
return;
|
|
1345
|
+
const source = alreadyHeld ? 'memory' : (entry.metricSource ?? 'unknown');
|
|
1346
|
+
recordRead(entry, source, blocking && first);
|
|
1347
|
+
if (!first || !metrics || started === undefined)
|
|
1348
|
+
return;
|
|
1349
|
+
first = false;
|
|
1350
|
+
if (connection.foreground === false)
|
|
1351
|
+
return;
|
|
1352
|
+
cancelPaint = (options.afterPaint ?? afterForegroundPaint)(() => {
|
|
1353
|
+
if (!active || disposed || metrics.epoch !== epoch || connection.foreground === false)
|
|
1354
|
+
return;
|
|
1355
|
+
metrics.observe(source === 'network'
|
|
1356
|
+
? 'farmslot_client_visible_read_seconds'
|
|
1357
|
+
: 'farmslot_client_cached_paint_seconds', {
|
|
1358
|
+
method: query.method,
|
|
1359
|
+
action_class: source === 'network' ? 'interactive-read' : 'cached-paint',
|
|
1360
|
+
cache_source: source,
|
|
1361
|
+
}, metrics.clock() - started);
|
|
1362
|
+
});
|
|
1363
|
+
if (!cancelPaint)
|
|
1364
|
+
metrics.increment('farmslot_client_unmeasured_paints_total', { method: query.method });
|
|
1365
|
+
};
|
|
1198
1366
|
entry.listeners.add(receive);
|
|
1199
1367
|
updateCollectors();
|
|
1200
1368
|
if (query.retention?.cohort === 'workspace') {
|
|
@@ -1239,6 +1407,8 @@ export function createGatewayResourceCache(options) {
|
|
|
1239
1407
|
if (entry.state.status !== 'ready' || entry.dirty)
|
|
1240
1408
|
schedule(entry);
|
|
1241
1409
|
return () => {
|
|
1410
|
+
active = false;
|
|
1411
|
+
cancelPaint?.();
|
|
1242
1412
|
entry.listeners.delete(receive);
|
|
1243
1413
|
updateCollectors();
|
|
1244
1414
|
if (!entry.listeners.size) {
|
|
@@ -1253,7 +1423,9 @@ export function createGatewayResourceCache(options) {
|
|
|
1253
1423
|
read(query) {
|
|
1254
1424
|
if (disposed)
|
|
1255
1425
|
throw new Error('Resource cache is disposed');
|
|
1256
|
-
|
|
1426
|
+
const entry = entryFor(query);
|
|
1427
|
+
recordRead(entry, 'memory', entry.state.data === undefined);
|
|
1428
|
+
return entry.state;
|
|
1257
1429
|
},
|
|
1258
1430
|
async readStored(query, refresh = true) {
|
|
1259
1431
|
if (disposed)
|
|
@@ -1285,20 +1457,25 @@ export function createGatewayResourceCache(options) {
|
|
|
1285
1457
|
runListProjections: () => runList.projections(),
|
|
1286
1458
|
openContent: (key) => readContentBytes((target) => target.getContent(contentKey(key)), true),
|
|
1287
1459
|
async forgetContent(key) {
|
|
1460
|
+
forgetResident(key);
|
|
1288
1461
|
const target = store;
|
|
1289
1462
|
const ownerVersion = hydrationOwnerVersion;
|
|
1290
|
-
await updateStore(target, () => target === store && !target.deleted && ownerVersion === hydrationOwnerVersion, () => target
|
|
1463
|
+
await updateStore(target, () => target === store && !target.deleted && ownerVersion === hydrationOwnerVersion, () => removeStoredContent(target, contentKey(key)));
|
|
1291
1464
|
},
|
|
1292
|
-
|
|
1293
|
-
|
|
1465
|
+
peekContentByAlias: (alias) => residentContent.get(alias)?.data.slice(),
|
|
1466
|
+
readContentByAlias: (alias) => readAliasedContent(alias, false),
|
|
1467
|
+
openContentByAlias: (alias) => readAliasedContent(alias, true),
|
|
1294
1468
|
storeContent,
|
|
1295
1469
|
retainContent,
|
|
1296
1470
|
contentWriteToken,
|
|
1297
1471
|
async dispose(reason) {
|
|
1472
|
+
if (reason && reason !== 'network' && reason !== 'background' && reason !== 'token-refresh')
|
|
1473
|
+
metrics?.resetScope();
|
|
1298
1474
|
if (disposed)
|
|
1299
1475
|
return;
|
|
1300
1476
|
const withdrawing = collectors.close();
|
|
1301
1477
|
disposed = true;
|
|
1478
|
+
residentContent.clear();
|
|
1302
1479
|
detailSync.reset();
|
|
1303
1480
|
version++;
|
|
1304
1481
|
unsubscribeChanges();
|