@farmslot/protocol 0.76.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 +22 -0
- package/dist/client-cache/cache.d.ts.map +1 -1
- package/dist/client-cache/cache.js +174 -14
- 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 +2 -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 +2 -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 +45 -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/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 +2 -0
- package/dist/rpc/registry.d.ts.map +1 -1
- package/dist/rpc/registry.js +2 -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 +1 -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/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 +1 -1
- package/dist/surfaces/view-links.d.ts.map +1 -1
- package/dist/surfaces/view-links.js +144 -16
- 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,28 @@ 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
|
+
|
|
9
31
|
## 0.76.0 - 2026-11-04
|
|
10
32
|
|
|
11
33
|
- Define admin-person standalone session requester attribution, execution-target availability and real home metadata.
|
|
@@ -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;
|
|
@@ -328,6 +390,27 @@ export function createGatewayResourceCache(options) {
|
|
|
328
390
|
if (saved.id !== RUN_LIST_COLLECTION_ID)
|
|
329
391
|
startupQueries.set(saved.id, saved);
|
|
330
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
|
+
}
|
|
331
414
|
if (rows)
|
|
332
415
|
runList.hydrate(rows);
|
|
333
416
|
startupStore = readingStore;
|
|
@@ -378,6 +461,8 @@ export function createGatewayResourceCache(options) {
|
|
|
378
461
|
// Stored facts belong to an owner, not a socket attempt. Fence a revoked
|
|
379
462
|
// owner's pending I/O synchronously, before queued deletion can run.
|
|
380
463
|
if (revoke || scopeChanged || authorityChanged) {
|
|
464
|
+
metrics?.resetScope();
|
|
465
|
+
residentContent.clear();
|
|
381
466
|
hydrationOwnerVersion++;
|
|
382
467
|
forgetRunList();
|
|
383
468
|
}
|
|
@@ -421,6 +506,7 @@ export function createGatewayResourceCache(options) {
|
|
|
421
506
|
switchingProfile ||
|
|
422
507
|
((next.connected || next.authority !== undefined) &&
|
|
423
508
|
(storedScopeChanged || storedAuthorityChanged))) {
|
|
509
|
+
residentContent.clear();
|
|
424
510
|
storeVersion++;
|
|
425
511
|
forgetRunList();
|
|
426
512
|
for (const entry of entries.values())
|
|
@@ -574,7 +660,7 @@ export function createGatewayResourceCache(options) {
|
|
|
574
660
|
};
|
|
575
661
|
entries.set(query.id, entry);
|
|
576
662
|
const saved = isRunListView(query) ? heldRunListView(query) : takeStartupQuery(query);
|
|
577
|
-
if (applyStoredQuery(entry, saved)) {
|
|
663
|
+
if (applyStoredQuery(entry, saved, 'memory')) {
|
|
578
664
|
entry.hydrated = true;
|
|
579
665
|
queueMicrotask(prune);
|
|
580
666
|
}
|
|
@@ -590,7 +676,7 @@ export function createGatewayResourceCache(options) {
|
|
|
590
676
|
startupQueries.delete(id);
|
|
591
677
|
return saved;
|
|
592
678
|
}
|
|
593
|
-
function applyStoredQuery(entry, saved) {
|
|
679
|
+
function applyStoredQuery(entry, saved, origin = 'stored') {
|
|
594
680
|
const query = entry.query;
|
|
595
681
|
if (!saved ||
|
|
596
682
|
!query.persisted ||
|
|
@@ -598,6 +684,7 @@ export function createGatewayResourceCache(options) {
|
|
|
598
684
|
(query.method === Methods.NATIVE_SESSION_READ &&
|
|
599
685
|
!storedNativeHistoryMatches(query.params, saved.data)))
|
|
600
686
|
return false;
|
|
687
|
+
entry.metricSource = origin;
|
|
601
688
|
const sourceObservation = query.sourceObservation?.(saved.data);
|
|
602
689
|
entry.state = {
|
|
603
690
|
status: 'stale',
|
|
@@ -708,7 +795,7 @@ export function createGatewayResourceCache(options) {
|
|
|
708
795
|
observedAt: runList.observedAt,
|
|
709
796
|
};
|
|
710
797
|
// A view whose summaries the version reply cannot answer keeps its own full read.
|
|
711
|
-
return
|
|
798
|
+
return request(Methods.RUN_LIST, params, signal);
|
|
712
799
|
}
|
|
713
800
|
function stampCovers(entry, stamp) {
|
|
714
801
|
if (!stamp ||
|
|
@@ -803,6 +890,10 @@ export function createGatewayResourceCache(options) {
|
|
|
803
890
|
prefetch,
|
|
804
891
|
};
|
|
805
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 });
|
|
806
897
|
entry.dirty = false;
|
|
807
898
|
entry.rereadFloor = undefined;
|
|
808
899
|
const unknownGeneration = entry.unknownGeneration;
|
|
@@ -829,8 +920,8 @@ export function createGatewayResourceCache(options) {
|
|
|
829
920
|
const reply = fromRunList
|
|
830
921
|
? await readRunListView(entry, pending.controller.signal)
|
|
831
922
|
: entry.query.method === Methods.NATIVE_SESSION_READ
|
|
832
|
-
? await readNativeHistory(entry.query.params, entry.state.data, (params, signal) =>
|
|
833
|
-
: 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);
|
|
834
925
|
if (disposed ||
|
|
835
926
|
store.deleted ||
|
|
836
927
|
pending.version !== version ||
|
|
@@ -859,6 +950,7 @@ export function createGatewayResourceCache(options) {
|
|
|
859
950
|
throw Object.assign(new Error(sourceObservation.error?.message ?? 'The gateway has not observed an upstream value.'), {
|
|
860
951
|
code: sourceObservation.error?.code ?? 'UPSTREAM_UNOBSERVED',
|
|
861
952
|
});
|
|
953
|
+
entry.metricSource = 'network';
|
|
862
954
|
entry.state = {
|
|
863
955
|
status: covered
|
|
864
956
|
? sourceObservation && sourceObservation.status !== 'ready'
|
|
@@ -954,7 +1046,7 @@ export function createGatewayResourceCache(options) {
|
|
|
954
1046
|
return;
|
|
955
1047
|
}
|
|
956
1048
|
for (const key of victims.content) {
|
|
957
|
-
await writingStore
|
|
1049
|
+
await removeStoredContent(writingStore, key);
|
|
958
1050
|
if (!current())
|
|
959
1051
|
return;
|
|
960
1052
|
}
|
|
@@ -1055,6 +1147,13 @@ export function createGatewayResourceCache(options) {
|
|
|
1055
1147
|
notify(entry);
|
|
1056
1148
|
}
|
|
1057
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);
|
|
1058
1157
|
if (entry.pending === pending) {
|
|
1059
1158
|
entry.pending = undefined;
|
|
1060
1159
|
if (entry.dirty && entry.listeners.size && usable)
|
|
@@ -1103,6 +1202,25 @@ export function createGatewayResourceCache(options) {
|
|
|
1103
1202
|
: undefined);
|
|
1104
1203
|
return saved?.data;
|
|
1105
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
|
+
}
|
|
1106
1224
|
/** Content writes accept only the current connection's owner, from the token's fetch onward. */
|
|
1107
1225
|
async function writeContent(token, write) {
|
|
1108
1226
|
if (disposed || !connection.connected)
|
|
@@ -1162,7 +1280,7 @@ export function createGatewayResourceCache(options) {
|
|
|
1162
1280
|
return false;
|
|
1163
1281
|
}
|
|
1164
1282
|
for (const victim of victims.content) {
|
|
1165
|
-
await target
|
|
1283
|
+
await removeStoredContent(target, victim);
|
|
1166
1284
|
if (!current())
|
|
1167
1285
|
return false;
|
|
1168
1286
|
}
|
|
@@ -1175,8 +1293,11 @@ export function createGatewayResourceCache(options) {
|
|
|
1175
1293
|
accessedAt,
|
|
1176
1294
|
});
|
|
1177
1295
|
}
|
|
1178
|
-
if (input.alias !== undefined && current())
|
|
1296
|
+
if (input.alias !== undefined && current()) {
|
|
1179
1297
|
await target.putContentAlias(input.alias, key);
|
|
1298
|
+
if (current())
|
|
1299
|
+
rememberResident(input.alias, key, data);
|
|
1300
|
+
}
|
|
1180
1301
|
return true;
|
|
1181
1302
|
});
|
|
1182
1303
|
}
|
|
@@ -1210,8 +1331,38 @@ export function createGatewayResourceCache(options) {
|
|
|
1210
1331
|
observe(query, listener) {
|
|
1211
1332
|
if (disposed)
|
|
1212
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;
|
|
1213
1337
|
const entry = entryFor(query);
|
|
1214
|
-
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
|
+
};
|
|
1215
1366
|
entry.listeners.add(receive);
|
|
1216
1367
|
updateCollectors();
|
|
1217
1368
|
if (query.retention?.cohort === 'workspace') {
|
|
@@ -1256,6 +1407,8 @@ export function createGatewayResourceCache(options) {
|
|
|
1256
1407
|
if (entry.state.status !== 'ready' || entry.dirty)
|
|
1257
1408
|
schedule(entry);
|
|
1258
1409
|
return () => {
|
|
1410
|
+
active = false;
|
|
1411
|
+
cancelPaint?.();
|
|
1259
1412
|
entry.listeners.delete(receive);
|
|
1260
1413
|
updateCollectors();
|
|
1261
1414
|
if (!entry.listeners.size) {
|
|
@@ -1270,7 +1423,9 @@ export function createGatewayResourceCache(options) {
|
|
|
1270
1423
|
read(query) {
|
|
1271
1424
|
if (disposed)
|
|
1272
1425
|
throw new Error('Resource cache is disposed');
|
|
1273
|
-
|
|
1426
|
+
const entry = entryFor(query);
|
|
1427
|
+
recordRead(entry, 'memory', entry.state.data === undefined);
|
|
1428
|
+
return entry.state;
|
|
1274
1429
|
},
|
|
1275
1430
|
async readStored(query, refresh = true) {
|
|
1276
1431
|
if (disposed)
|
|
@@ -1302,20 +1457,25 @@ export function createGatewayResourceCache(options) {
|
|
|
1302
1457
|
runListProjections: () => runList.projections(),
|
|
1303
1458
|
openContent: (key) => readContentBytes((target) => target.getContent(contentKey(key)), true),
|
|
1304
1459
|
async forgetContent(key) {
|
|
1460
|
+
forgetResident(key);
|
|
1305
1461
|
const target = store;
|
|
1306
1462
|
const ownerVersion = hydrationOwnerVersion;
|
|
1307
|
-
await updateStore(target, () => target === store && !target.deleted && ownerVersion === hydrationOwnerVersion, () => target
|
|
1463
|
+
await updateStore(target, () => target === store && !target.deleted && ownerVersion === hydrationOwnerVersion, () => removeStoredContent(target, contentKey(key)));
|
|
1308
1464
|
},
|
|
1309
|
-
|
|
1310
|
-
|
|
1465
|
+
peekContentByAlias: (alias) => residentContent.get(alias)?.data.slice(),
|
|
1466
|
+
readContentByAlias: (alias) => readAliasedContent(alias, false),
|
|
1467
|
+
openContentByAlias: (alias) => readAliasedContent(alias, true),
|
|
1311
1468
|
storeContent,
|
|
1312
1469
|
retainContent,
|
|
1313
1470
|
contentWriteToken,
|
|
1314
1471
|
async dispose(reason) {
|
|
1472
|
+
if (reason && reason !== 'network' && reason !== 'background' && reason !== 'token-refresh')
|
|
1473
|
+
metrics?.resetScope();
|
|
1315
1474
|
if (disposed)
|
|
1316
1475
|
return;
|
|
1317
1476
|
const withdrawing = collectors.close();
|
|
1318
1477
|
disposed = true;
|
|
1478
|
+
residentContent.clear();
|
|
1319
1479
|
detailSync.reset();
|
|
1320
1480
|
version++;
|
|
1321
1481
|
unsubscribeChanges();
|