@volter/world-core 2.0.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/LICENSE +202 -0
- package/README.md +29 -0
- package/app-route.cjs +154 -0
- package/app-route.d.cts +7 -0
- package/attach.cjs +80 -0
- package/dist/app-route.cjs +154 -0
- package/dist/app-route.d.cts +7 -0
- package/dist/attach.cjs +80 -0
- package/dist/generated/pack-facts.json +4306 -0
- package/dist/inject.cjs +1097 -0
- package/dist/network-policy.cjs +92 -0
- package/dist/network-policy.d.cts +10 -0
- package/dist/src/actions.d.ts +276 -0
- package/dist/src/actions.js +436 -0
- package/dist/src/ancestry.d.ts +22 -0
- package/dist/src/ancestry.js +238 -0
- package/dist/src/args.d.ts +3 -0
- package/dist/src/args.js +12 -0
- package/dist/src/blob-store.d.ts +55 -0
- package/dist/src/blob-store.js +186 -0
- package/dist/src/brand-tokens.d.ts +2 -0
- package/dist/src/brand-tokens.js +17 -0
- package/dist/src/changeset.d.ts +431 -0
- package/dist/src/changeset.js +0 -0
- package/dist/src/client-bundle.d.ts +1 -0
- package/dist/src/client-bundle.js +28 -0
- package/dist/src/credential.d.ts +38 -0
- package/dist/src/credential.js +114 -0
- package/dist/src/derived-core.d.ts +452 -0
- package/dist/src/derived-core.js +782 -0
- package/dist/src/derived.d.ts +84 -0
- package/dist/src/derived.js +122 -0
- package/dist/src/emit.d.ts +106 -0
- package/dist/src/emit.js +157 -0
- package/dist/src/executor.d.ts +120 -0
- package/dist/src/executor.js +387 -0
- package/dist/src/file-response.d.ts +3 -0
- package/dist/src/file-response.js +22 -0
- package/dist/src/fork.d.ts +26 -0
- package/dist/src/fork.js +68 -0
- package/dist/src/git/history.d.ts +36 -0
- package/dist/src/git/history.js +298 -0
- package/dist/src/git/index.d.ts +6 -0
- package/dist/src/git/index.js +6 -0
- package/dist/src/git/inflate.d.ts +11 -0
- package/dist/src/git/inflate.js +194 -0
- package/dist/src/git/objects.d.ts +64 -0
- package/dist/src/git/objects.js +161 -0
- package/dist/src/git/pack.d.ts +14 -0
- package/dist/src/git/pack.js +199 -0
- package/dist/src/git/refs.d.ts +19 -0
- package/dist/src/git/refs.js +35 -0
- package/dist/src/git/smart-http.d.ts +45 -0
- package/dist/src/git/smart-http.js +223 -0
- package/dist/src/hash.d.ts +38 -0
- package/dist/src/hash.js +48 -0
- package/dist/src/head.d.ts +140 -0
- package/dist/src/head.js +313 -0
- package/dist/src/history.d.ts +76 -0
- package/dist/src/history.js +322 -0
- package/dist/src/index.d.ts +73 -0
- package/dist/src/index.js +98 -0
- package/dist/src/lifecycle.d.ts +1 -0
- package/dist/src/lifecycle.js +8 -0
- package/dist/src/log.d.ts +254 -0
- package/dist/src/log.js +801 -0
- package/dist/src/mirror-shell.d.ts +2 -0
- package/dist/src/mirror-shell.js +13 -0
- package/dist/src/observe.d.ts +49 -0
- package/dist/src/observe.js +148 -0
- package/dist/src/pack-assets.d.ts +30 -0
- package/dist/src/pack-assets.js +88 -0
- package/dist/src/packRegistry.d.ts +374 -0
- package/dist/src/packRegistry.js +142 -0
- package/dist/src/placeholder-remote.d.ts +22 -0
- package/dist/src/placeholder-remote.js +86 -0
- package/dist/src/proxy.d.ts +25 -0
- package/dist/src/proxy.js +155 -0
- package/dist/src/rateBudget.d.ts +367 -0
- package/dist/src/rateBudget.js +925 -0
- package/dist/src/references.d.ts +18 -0
- package/dist/src/references.js +27 -0
- package/dist/src/remote-execute.d.ts +22 -0
- package/dist/src/remote-execute.js +1 -0
- package/dist/src/resource-blob.d.ts +10 -0
- package/dist/src/resource-blob.js +56 -0
- package/dist/src/scenario.d.ts +197 -0
- package/dist/src/scenario.js +425 -0
- package/dist/src/schemas.d.ts +78 -0
- package/dist/src/schemas.js +50 -0
- package/dist/src/serve-http.d.ts +48 -0
- package/dist/src/serve-http.js +340 -0
- package/dist/src/serve.d.ts +147 -0
- package/dist/src/serve.js +507 -0
- package/dist/src/shared-blob-index.d.ts +4 -0
- package/dist/src/shared-blob-index.js +126 -0
- package/dist/src/state-system.d.ts +70 -0
- package/dist/src/state-system.js +90 -0
- package/dist/src/storage.d.ts +101 -0
- package/dist/src/storage.js +337 -0
- package/dist/src/twin-fetch.d.ts +64 -0
- package/dist/src/twin-fetch.js +91 -0
- package/dist/src/types.d.ts +40 -0
- package/dist/src/types.js +1 -0
- package/dist/src/v1-removed.d.ts +159 -0
- package/dist/src/v1-removed.js +124 -0
- package/dist/src/volter-home.d.ts +5 -0
- package/dist/src/volter-home.js +10 -0
- package/dist/src/world-clock.d.ts +4 -0
- package/dist/src/world-clock.js +32 -0
- package/dist/src/world-env.d.ts +3 -0
- package/dist/src/world-env.js +22 -0
- package/dist/src/world-store-sql.d.ts +27 -0
- package/dist/src/world-store-sql.js +86 -0
- package/dist/src/world-store.d.ts +168 -0
- package/dist/src/world-store.js +475 -0
- package/dist/src/worldConfig.d.ts +9 -0
- package/dist/src/worldConfig.js +17 -0
- package/dist/stream-bridge.cjs +80 -0
- package/dist/vendor-hosts.cjs +200 -0
- package/generated/pack-facts.json +4306 -0
- package/inject.cjs +1097 -0
- package/network-policy.cjs +92 -0
- package/network-policy.d.cts +10 -0
- package/package.json +103 -0
- package/src/actions.ts +564 -0
- package/src/ancestry.ts +213 -0
- package/src/args.ts +14 -0
- package/src/blob-store.ts +185 -0
- package/src/brand-tokens.ts +17 -0
- package/src/changeset.ts +1032 -0
- package/src/client-bundle.ts +29 -0
- package/src/credential.ts +140 -0
- package/src/derived-core.ts +1004 -0
- package/src/derived.ts +176 -0
- package/src/emit.ts +242 -0
- package/src/executor.ts +431 -0
- package/src/file-response.ts +22 -0
- package/src/fork.ts +89 -0
- package/src/git/history.ts +177 -0
- package/src/git/index.ts +6 -0
- package/src/git/inflate.ts +125 -0
- package/src/git/objects.ts +110 -0
- package/src/git/pack.ts +105 -0
- package/src/git/refs.ts +25 -0
- package/src/git/smart-http.ts +149 -0
- package/src/hash.ts +66 -0
- package/src/head.ts +318 -0
- package/src/history.ts +246 -0
- package/src/index.ts +323 -0
- package/src/lifecycle.ts +8 -0
- package/src/log.ts +793 -0
- package/src/mirror-shell.ts +15 -0
- package/src/observe.ts +130 -0
- package/src/pack-assets.ts +81 -0
- package/src/packRegistry.ts +408 -0
- package/src/placeholder-remote.ts +81 -0
- package/src/proxy.ts +183 -0
- package/src/rateBudget.ts +1115 -0
- package/src/references.ts +46 -0
- package/src/remote-execute.ts +26 -0
- package/src/resource-blob.ts +57 -0
- package/src/scenario.ts +479 -0
- package/src/schemas.ts +56 -0
- package/src/serve-http.ts +299 -0
- package/src/serve.ts +618 -0
- package/src/shared-blob-index.ts +108 -0
- package/src/state-system.ts +115 -0
- package/src/storage.ts +407 -0
- package/src/twin-fetch.ts +147 -0
- package/src/types.ts +50 -0
- package/src/v1-removed.ts +172 -0
- package/src/volter-home.ts +11 -0
- package/src/world-clock.ts +33 -0
- package/src/world-env.ts +18 -0
- package/src/world-store-sql.ts +118 -0
- package/src/world-store.ts +572 -0
- package/src/worldConfig.ts +27 -0
- package/stream-bridge.cjs +80 -0
- package/vendor-hosts.cjs +200 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
// Twin serve layer (the twins architecture notes, increment 1): answer vendor-shaped
|
|
2
|
+
// READS from the local event-sourced state — the "local twin" read path. An app
|
|
3
|
+
// or agent points at this instead of the real vendor; it responds from local
|
|
4
|
+
// state with the real service unreachable.
|
|
5
|
+
//
|
|
6
|
+
// There is ONE twin, not a set of "modes". Pulling reality, writing locally, and
|
|
7
|
+
// forking are operations on the same substrate (events + local actions + the
|
|
8
|
+
// projection over them), not exclusive modes — see the architecture doc, "a twin
|
|
9
|
+
// is a repo". The only serve-time policy here is `readOnly`: a twin accepts local
|
|
10
|
+
// writes (as actions) unless you start it read-only (a pure mirror of pulled
|
|
11
|
+
// reality). Forking is a separate data operation (fork.ts), never a serve mode.
|
|
12
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
13
|
+
import { serveHttp, setServeDecorator, WORLD_BOOT_PATH } from "./serve-http.js";
|
|
14
|
+
import { performAtHead } from "./head.js";
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
import { hashFieldValue } from "./hash.js";
|
|
18
|
+
import { appendActionIfAbsent, appendActionOccurrence, decideAndAppendAction, projectResources } from "./actions.js";
|
|
19
|
+
import { observeWorldPaths, worldPaths } from "./storage.js";
|
|
20
|
+
import { getActiveWorldStore } from "./world-store.js";
|
|
21
|
+
/** The journal read back: every entry kept for a service, oldest first (an empty list when the journal is off or empty). */
|
|
22
|
+
export function readTwinRequestJournal(service, root) {
|
|
23
|
+
const path = twinRequestJournalPath(service, root);
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const file of [`${path}.1`, path]) {
|
|
26
|
+
const text = getActiveWorldStore().read(file);
|
|
27
|
+
if (!text)
|
|
28
|
+
continue;
|
|
29
|
+
for (const line of text.split('\n')) {
|
|
30
|
+
if (!line)
|
|
31
|
+
continue;
|
|
32
|
+
try {
|
|
33
|
+
out.push(JSON.parse(line));
|
|
34
|
+
}
|
|
35
|
+
catch { /* a torn line at rotation */ }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
/** ~1 MB cap before a one-deep rotation — small enough to never matter on disk, large enough for
|
|
41
|
+
* tens of thousands of entries (a shape line is ~80 bytes). */
|
|
42
|
+
const REQUEST_JOURNAL_MAX_BYTES = 1_000_000;
|
|
43
|
+
/**
|
|
44
|
+
* Does this header/param NAME look like it carries a credential? Deliberately a generic word
|
|
45
|
+
* predicate, never a vendor table: the kernel must not learn that Datadog calls its keys
|
|
46
|
+
* `DD-API-KEY` or that Postmark calls its tokens `X-Postmark-Server-Token`. Both fall out of
|
|
47
|
+
* `key`/`token` as name segments, and so does every vendor that has not been written yet.
|
|
48
|
+
*/
|
|
49
|
+
const CREDENTIAL_NAME = /(?:^|[-_.])(?:auth|authorization|token|jwt|key|apikey|secret|credential|credentials|signature|sig|password|passwd|pwd|session|sessionid|cookie|access[-_]?token|refresh[-_]?token)(?:[-_.]|$)/i;
|
|
50
|
+
/** Auth schemes recognised in a value even when the NAME says nothing (`Bearer …` in `x-custom`). */
|
|
51
|
+
const CREDENTIAL_SCHEME = /^(Bearer|Basic|Digest|Token|Negotiate|NTLM|OAuth|Signature|Hawk|AWS4-HMAC-SHA256|SharedKey|SharedKeyLite|GoogleLogin)\s+(\S[\s\S]*)$/i;
|
|
52
|
+
/** Non-reversible, stable fingerprint. 64 bits of sha256 — enough that two distinct credentials
|
|
53
|
+
* never collide in a journal, far too little to walk back to a real key. */
|
|
54
|
+
export function credentialFingerprint(value) {
|
|
55
|
+
return `sha256:${createHash('sha256').update(value, 'utf8').digest('hex').slice(0, 16)}`;
|
|
56
|
+
}
|
|
57
|
+
function credentialShapeFor(name, value, where) {
|
|
58
|
+
const scheme = CREDENTIAL_SCHEME.exec(value);
|
|
59
|
+
if (!scheme && !CREDENTIAL_NAME.test(name))
|
|
60
|
+
return undefined;
|
|
61
|
+
const lower = name.toLowerCase();
|
|
62
|
+
if (value === '')
|
|
63
|
+
return { name: lower, in: where, empty: true };
|
|
64
|
+
if (scheme)
|
|
65
|
+
return { name: lower, in: where, scheme: scheme[1], fp: credentialFingerprint(scheme[2]) };
|
|
66
|
+
return { name: lower, in: where, fp: credentialFingerprint(value) };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The credential SHAPE of one request: every credential-looking header and query param, named and
|
|
70
|
+
* fingerprinted, sorted for stable diffing. Values never leave this function.
|
|
71
|
+
*/
|
|
72
|
+
export function twinRequestCredentials(headers, url) {
|
|
73
|
+
const shapes = [];
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
const add = (name, value, where) => {
|
|
76
|
+
const shape = credentialShapeFor(name, value, where);
|
|
77
|
+
if (!shape)
|
|
78
|
+
return;
|
|
79
|
+
const key = `${shape.in}:${shape.name}`;
|
|
80
|
+
if (seen.has(key))
|
|
81
|
+
return;
|
|
82
|
+
seen.add(key);
|
|
83
|
+
shapes.push(shape);
|
|
84
|
+
};
|
|
85
|
+
try {
|
|
86
|
+
if (typeof headers.forEach === 'function') {
|
|
87
|
+
headers.forEach((value, name) => add(name, value, 'header'));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
for (const [name, value] of Object.entries(headers))
|
|
91
|
+
add(name, String(value), 'header');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch { /* a malformed header bag must not fail a serve */ }
|
|
95
|
+
try {
|
|
96
|
+
if (url !== undefined) {
|
|
97
|
+
const search = typeof url === 'string' ? new URL(url, 'http://twin.invalid').search : url.search;
|
|
98
|
+
for (const [name, value] of new URLSearchParams(search))
|
|
99
|
+
add(name, value, 'query');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch { /* an unparseable URL must not fail a serve */ }
|
|
103
|
+
return shapes.sort((a, b) => (a.in === b.in ? a.name.localeCompare(b.name) : a.in.localeCompare(b.in)));
|
|
104
|
+
}
|
|
105
|
+
export function twinRequestJournalEnabled(env = process.env) {
|
|
106
|
+
return env.VOLTER_TWIN_REQUEST_JOURNAL === '1';
|
|
107
|
+
}
|
|
108
|
+
/** Where a service's request journal lives: beside its `actions.jsonl`. */
|
|
109
|
+
export function twinRequestJournalPath(service, root) {
|
|
110
|
+
return join(dirname(worldPaths(service, root).events), 'requests.jsonl');
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Append one served request to the journal — a no-op unless VOLTER_TWIN_REQUEST_JOURNAL=1, so
|
|
114
|
+
* the default path does zero I/O. Never throws: an observability append must not fail a serve.
|
|
115
|
+
*/
|
|
116
|
+
export function journalTwinRequest(service, entry, root) {
|
|
117
|
+
if (!twinRequestJournalEnabled())
|
|
118
|
+
return;
|
|
119
|
+
try {
|
|
120
|
+
const store = getActiveWorldStore();
|
|
121
|
+
const path = twinRequestJournalPath(service, root);
|
|
122
|
+
store.mkdir(dirname(path));
|
|
123
|
+
const size = store.stat(path)?.size ?? 0;
|
|
124
|
+
if (size >= REQUEST_JOURNAL_MAX_BYTES) {
|
|
125
|
+
// one-deep rotation: the previous overflow is overwritten, the live file starts fresh.
|
|
126
|
+
store.writeAtomic(`${path}.1`, store.read(path) ?? '');
|
|
127
|
+
store.write(path, '');
|
|
128
|
+
}
|
|
129
|
+
const line = JSON.stringify({
|
|
130
|
+
at: entry.at ?? new Date().toISOString(),
|
|
131
|
+
method: entry.method.toUpperCase(),
|
|
132
|
+
path: entry.path.split('?')[0], // the query STRING never lands; its credential params do, named + fingerprinted
|
|
133
|
+
status: entry.status,
|
|
134
|
+
...(typeof entry.ms === 'number' ? { ms: entry.ms } : {}),
|
|
135
|
+
...(entry.credentials?.length ? { credentials: entry.credentials } : {}),
|
|
136
|
+
});
|
|
137
|
+
store.append(path, `${line}\n`);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// journaling is best-effort observability; the serve path must not care
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ── how a wrapped `Bun.serve` learns which twin it is ──────────────────────────────────────────
|
|
144
|
+
// `Bun.serve({port, fetch})` carries neither the vendor id nor the state root — both live in the
|
|
145
|
+
// factory's closure. They are recoverable without touching a single pack, because handling a
|
|
146
|
+
// request makes the pack call the kernel, and every kernel state path funnels through
|
|
147
|
+
// `worldPaths(service, root)` (storage.ts). Observing the FIRST such call a server makes is the
|
|
148
|
+
// twin naming itself: `datadog` under `/tmp/world/data/datadog`. It is cached per server, so only
|
|
149
|
+
// the first request pays for it. A server whose first request touches no state falls back to the
|
|
150
|
+
// call site (`packages/twin/<vendor>/…` or `@volter/twin-<vendor>`) and the process's `--root`
|
|
151
|
+
// argument, which is how the world runtime launches every twin.
|
|
152
|
+
export const TWIN_JOURNAL_IDENTITY = Symbol.for('volter.twin.requestJournal.identity');
|
|
153
|
+
const JOURNAL_SHIM_INSTALLED = Symbol.for('volter.twin.requestJournal.installed');
|
|
154
|
+
/** One capture slot per in-flight request, so two twins co-located in ONE process (the shared
|
|
155
|
+
* host) can never read each other's identity off a racing sibling's first request.
|
|
156
|
+
*
|
|
157
|
+
* LAZY ON PURPOSE, and this is load-bearing rather than style. Mirror-UI packs import their own
|
|
158
|
+
* server module into the BROWSER bundle and rely on Bun tree-shaking the server-only half away
|
|
159
|
+
* (see any `*-mirror-ui.ts` header). A module-scope `new AsyncLocalStorage()` is a side effect,
|
|
160
|
+
* so it survives tree-shaking and lands in the client — where `node:async_hooks` does not
|
|
161
|
+
* resolve, `new` throws at bundle evaluation, and the React app never mounts. That took out
|
|
162
|
+
* every mirror UI in the estate at once. Nothing in this module may run at module scope. */
|
|
163
|
+
let identityCapture;
|
|
164
|
+
function identitySlot() {
|
|
165
|
+
identityCapture ??= new AsyncLocalStorage();
|
|
166
|
+
return identityCapture;
|
|
167
|
+
}
|
|
168
|
+
function observeIdentityFromWorldPaths() {
|
|
169
|
+
observeWorldPaths((service, root) => {
|
|
170
|
+
const slot = identityCapture?.getStore();
|
|
171
|
+
if (!slot || slot.identity !== undefined)
|
|
172
|
+
return;
|
|
173
|
+
slot.identity = root === undefined ? { service } : { service, root };
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/** The `--root DIR` a twin was launched with (the world runtime's spawn contract), if any. */
|
|
177
|
+
function rootFromArgv(argv = process.argv) {
|
|
178
|
+
const index = argv.indexOf('--root');
|
|
179
|
+
const value = index >= 0 ? argv[index + 1] : undefined;
|
|
180
|
+
return value && !value.startsWith('--') ? value : undefined;
|
|
181
|
+
}
|
|
182
|
+
/** The vendor a `Bun.serve` call site belongs to, from its module path. Infra packages are not
|
|
183
|
+
* vendors, so they are skipped: whoever called THEM is the twin. */
|
|
184
|
+
const INFRA_PACKAGES = new Set(['world-runtime', 'world-tooling', 'world-attach', 'world-browser-assets', 'world-host']);
|
|
185
|
+
export function vendorFromStack(stack) {
|
|
186
|
+
if (!stack)
|
|
187
|
+
return undefined;
|
|
188
|
+
const pattern = /(?:packages[\\/]twin[\\/]|@volter[\\/]twin-)([A-Za-z0-9_-]+)/g;
|
|
189
|
+
for (const match of stack.matchAll(pattern)) {
|
|
190
|
+
const name = match[1];
|
|
191
|
+
if (!INFRA_PACKAGES.has(name))
|
|
192
|
+
return name;
|
|
193
|
+
}
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Wrap `Bun.serve` so EVERY twin's HTTP surface journals, with no per-pack line. Idempotent
|
|
198
|
+
* (a `Symbol.for` marker survives a second copy of this module) and a strict pass-through when
|
|
199
|
+
* VOLTER_TWIN_REQUEST_JOURNAL is not `1` — the default path adds one function call per request
|
|
200
|
+
* and does zero I/O. Called once at module load, below; exported so a test can assert it.
|
|
201
|
+
*
|
|
202
|
+
* A pack that knows its own identity may declare it by putting `{service, root}` on the serve
|
|
203
|
+
* options under `TWIN_JOURNAL_IDENTITY` (a symbol key Bun's option reader ignores). Nothing in
|
|
204
|
+
* the estate needs to: `createTwinServer` is the only caller, because it is the one server whose
|
|
205
|
+
* service is an argument rather than a fact about the pack.
|
|
206
|
+
*/
|
|
207
|
+
export function installTwinRequestJournal() {
|
|
208
|
+
// a browser bundle (a mirror UI carrying the kernel): nothing to wrap, nothing to touch
|
|
209
|
+
if (typeof window !== 'undefined' || typeof document !== 'undefined')
|
|
210
|
+
return false;
|
|
211
|
+
const bun = globalThis.Bun;
|
|
212
|
+
const original = bun && typeof bun.serve === 'function' ? bun.serve : undefined;
|
|
213
|
+
if (original && original[JOURNAL_SHIM_INSTALLED])
|
|
214
|
+
return true;
|
|
215
|
+
// Registered HERE, not at module scope, for the same reason the capture slot is lazy: a
|
|
216
|
+
// module-scope registration is a side effect that survives tree-shaking into the client.
|
|
217
|
+
observeIdentityFromWorldPaths();
|
|
218
|
+
// every server made through the seam (serve-http.ts) journals — on Node this is the only wrap
|
|
219
|
+
setServeDecorator((options) => journalingFetch(options, vendorFromStack(new Error().stack)));
|
|
220
|
+
if (!original)
|
|
221
|
+
return false; // Node: no global to wrap; the seam carries the journal
|
|
222
|
+
const wrapped = function serve(options, ...rest) {
|
|
223
|
+
const config = options;
|
|
224
|
+
if (!config || typeof config.fetch !== 'function')
|
|
225
|
+
return original.apply(this, [options, ...rest]);
|
|
226
|
+
// Captured HERE, at the `Bun.serve` call, because this is the only moment the pack's own
|
|
227
|
+
// module is on the stack — by the time a request is served, Bun is the caller.
|
|
228
|
+
const callSiteVendor = config[TWIN_JOURNAL_IDENTITY] ? undefined : vendorFromStack(new Error().stack);
|
|
229
|
+
return original.apply(this, [{ ...config, fetch: journalingFetch(config, callSiteVendor) }, ...rest]);
|
|
230
|
+
};
|
|
231
|
+
Object.defineProperty(wrapped, JOURNAL_SHIM_INSTALLED, { value: true });
|
|
232
|
+
bun.serve = wrapped;
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
/** The journaling form of a server's fetch handler: identity resolved once per SERVER (declared
|
|
236
|
+
* under TWIN_JOURNAL_IDENTITY, else observed from the first state-touching request, else the
|
|
237
|
+
* call-site vendor + `--root`), every request journaled with its method, path and status. */
|
|
238
|
+
function journalingFetch(config, callSiteVendor) {
|
|
239
|
+
if (config.twinRequestJournal === false)
|
|
240
|
+
return config.fetch;
|
|
241
|
+
{
|
|
242
|
+
const declared = config[TWIN_JOURNAL_IDENTITY];
|
|
243
|
+
// Resolved once per SERVER, not per request: a twin's identity is a constant of the server.
|
|
244
|
+
let identity = declared;
|
|
245
|
+
const inner = config.fetch;
|
|
246
|
+
const journaling = async function (request, server) {
|
|
247
|
+
// The World's own boot probe (serve-http.ts, WORLD_BOOT_PATH) is not the app's traffic: never journaled.
|
|
248
|
+
if (!twinRequestJournalEnabled() || new URL(request.url).pathname === WORLD_BOOT_PATH)
|
|
249
|
+
return inner.call(this, request, server);
|
|
250
|
+
const slot = {};
|
|
251
|
+
let status = 500;
|
|
252
|
+
const startedAt = Date.now();
|
|
253
|
+
try {
|
|
254
|
+
const response = (await identitySlot().run(slot, () => inner.call(this, request, server)));
|
|
255
|
+
if (response instanceof Response)
|
|
256
|
+
status = response.status;
|
|
257
|
+
else if (response === undefined)
|
|
258
|
+
return response; // upgraded (websocket) — nothing served
|
|
259
|
+
return response;
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
// Only an OBSERVED identity is cached: it is the twin's own words. A fallback is
|
|
263
|
+
// per-request, so the first request that touches state upgrades every later one instead
|
|
264
|
+
// of locking a guessed root in for the life of the server.
|
|
265
|
+
identity ??= slot.identity;
|
|
266
|
+
const resolved = identity ?? fallbackIdentity(callSiteVendor);
|
|
267
|
+
if (resolved) {
|
|
268
|
+
const url = new URL(request.url);
|
|
269
|
+
journalTwinRequest(resolved.service, {
|
|
270
|
+
method: request.method,
|
|
271
|
+
path: url.pathname,
|
|
272
|
+
status,
|
|
273
|
+
ms: Date.now() - startedAt,
|
|
274
|
+
credentials: twinRequestCredentials(request.headers, url),
|
|
275
|
+
}, resolved.root);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
return journaling;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/** Identity for a server whose first request touched no state: the vendor that owns the calling
|
|
283
|
+
* module, plus the `--root` the world runtime launched this twin with. */
|
|
284
|
+
function fallbackIdentity(callSiteVendor) {
|
|
285
|
+
const service = process.env.VOLTER_TWIN_JOURNAL_SERVICE || callSiteVendor;
|
|
286
|
+
if (!service || !/^[A-Za-z0-9_-]+$/.test(service))
|
|
287
|
+
return undefined;
|
|
288
|
+
const root = rootFromArgv();
|
|
289
|
+
return root === undefined ? { service } : { service, root };
|
|
290
|
+
}
|
|
291
|
+
// One call, at module load. `@volter/world-core`'s entrypoint re-exports this module, and every pack
|
|
292
|
+
// that serves imports the kernel, so wrapping here is what makes the journal a property of the
|
|
293
|
+
// PRODUCT rather than of the four packs that once remembered to call it.
|
|
294
|
+
//
|
|
295
|
+
// It is a no-op wherever `Bun.serve` is absent, which is what keeps it safe in a browser bundle:
|
|
296
|
+
// off-Bun it returns before touching `node:async_hooks`, `process.argv`, or the storage hook.
|
|
297
|
+
installTwinRequestJournal();
|
|
298
|
+
// The twin's current view = the OBSERVED mirror (events.jsonl) with the local
|
|
299
|
+
// ACTION log (actions.jsonl) projected over it (R18). Observed facts and local
|
|
300
|
+
// actions are kept separate; this is the single projected read model.
|
|
301
|
+
export function twinResources(service, root) {
|
|
302
|
+
return projectResources(service, root);
|
|
303
|
+
}
|
|
304
|
+
function actionForTwinWrite(service, write) {
|
|
305
|
+
const occurredAt = write.occurredAt ?? new Date().toISOString();
|
|
306
|
+
// Idempotency: dedup a RE-ISSUED IDENTICAL write only. The key includes a hash of the write
|
|
307
|
+
// CONTENT, not just the timestamp. `undefined` keys are omitted by canonicalJson, preserving
|
|
308
|
+
// every pre-input/projection action id for existing packs.
|
|
309
|
+
const contentHash = hashFieldValue({ operation: write.operation, subjectId: write.subjectId, fields: write.fields, input: write.input, projection: write.projection });
|
|
310
|
+
// A caller-supplied identity MAY carry an occurrence ordinal, and must: `actionId`'s one
|
|
311
|
+
// legitimate caller is replay, and a replayed changeset that contains a REPEAT carries the
|
|
312
|
+
// source's `<base>#1` verbatim. A guard here that rejected ordinal suffixes — added to stop a
|
|
313
|
+
// pack squatting slot #5 — threw on exactly that replay, and nothing had tested "replay a
|
|
314
|
+
// repeat" (found 2026-09-04 in review). The squat it guarded against is a misuse of a
|
|
315
|
+
// replay-only field, is LOUD on collision, and corrupts nothing; breaking the CI primitive to
|
|
316
|
+
// prevent it was the wrong trade.
|
|
317
|
+
const actionId = write.actionId
|
|
318
|
+
?? (write.idempotencyKey
|
|
319
|
+
? `twin:${service}:${write.operation}:${write.subjectId}:idem:${write.idempotencyKey}`
|
|
320
|
+
: `twin:${service}:${write.operation}:${write.subjectId}:${occurredAt}:${contentHash}${write.uniqueness ? `:${write.uniqueness}` : ''}`);
|
|
321
|
+
return {
|
|
322
|
+
id: actionId,
|
|
323
|
+
service,
|
|
324
|
+
op: 'set',
|
|
325
|
+
operation: write.operation,
|
|
326
|
+
subject: { type: write.subjectType, id: write.subjectId },
|
|
327
|
+
occurredAt,
|
|
328
|
+
...(write.actor ? { actor: write.actor } : {}),
|
|
329
|
+
...(write.preconditions?.length ? { preconditions: write.preconditions } : {}),
|
|
330
|
+
...(write.correlationId ? { correlationId: write.correlationId } : {}),
|
|
331
|
+
...(write.idempotencyKey ? { idempotencyKey: write.idempotencyKey } : {}),
|
|
332
|
+
...(write.input ? { input: write.input } : {}),
|
|
333
|
+
fields: write.fields,
|
|
334
|
+
...(write.projection ? { projection: write.projection } : {}),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Decide a state-dependent local write and append it under the same cross-process action lock.
|
|
339
|
+
* Use this when acceptance or the new fields depend on current projected state; a caller-side
|
|
340
|
+
* read followed by `applyTwinWrite` is not atomic across processes.
|
|
341
|
+
*/
|
|
342
|
+
export async function applyTwinWriteAtomic(service, prepare, root) {
|
|
343
|
+
// Same ruling as applyTwinWrite: a local write is an OCCURRENCE unless the caller supplied
|
|
344
|
+
// identity. The mode rides on the decision because only the callback knows which write it chose.
|
|
345
|
+
const committed = decideAndAppendAction(service, (resources) => {
|
|
346
|
+
const decision = prepare(resources);
|
|
347
|
+
if (decision.kind === 'skip')
|
|
348
|
+
return { kind: 'skip', value: decision.value };
|
|
349
|
+
return {
|
|
350
|
+
kind: 'append',
|
|
351
|
+
value: decision.value,
|
|
352
|
+
action: actionForTwinWrite(service, decision.write),
|
|
353
|
+
identity: decision.write.idempotencyKey || decision.write.actionId ? 'caller' : 'occurrence',
|
|
354
|
+
};
|
|
355
|
+
}, root);
|
|
356
|
+
// as applyTwinWrite: a seed's write is the placeholder's default data, never performed
|
|
357
|
+
const head = committed.action && committed.appended && !committed.placeholder ? await performAtHead(service, committed.action, root) : { performed: false };
|
|
358
|
+
return {
|
|
359
|
+
value: committed.value,
|
|
360
|
+
...(committed.action ? { result: { status: committed.appended ? 'performed' : 'replayed', actionId: committed.action.id, ...(head.externalId ? { externalId: head.externalId } : {}) } } : {}),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
// Resolve a read request against the twin's resources. Returns the matched
|
|
364
|
+
// resource(s) + an HTTP-ish status, deterministically from state.
|
|
365
|
+
// GET / -> { service, mode, resourceTypes, count }
|
|
366
|
+
// GET /<type> -> list of resources of that type
|
|
367
|
+
// GET /<type>/<id> -> one resource (id may be url-encoded)
|
|
368
|
+
export function resolveTwinRead(service, pathname, opts = {}) {
|
|
369
|
+
const resources = twinResources(service, opts.root);
|
|
370
|
+
const parts = pathname.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
|
|
371
|
+
if (parts.length === 0) {
|
|
372
|
+
const types = [...new Set(resources.map((r) => r.type))].sort();
|
|
373
|
+
return { status: 200, body: { service, resourceTypes: types, count: resources.length } };
|
|
374
|
+
}
|
|
375
|
+
const type = parts[0];
|
|
376
|
+
const ofType = resources.filter((r) => r.type === type);
|
|
377
|
+
if (parts.length === 1) {
|
|
378
|
+
return { status: 200, body: { type, count: ofType.length, items: ofType } };
|
|
379
|
+
}
|
|
380
|
+
const id = decodeURIComponent(parts.slice(1).join('/'));
|
|
381
|
+
const found = ofType.find((r) => r.id === id);
|
|
382
|
+
if (!found)
|
|
383
|
+
return { status: 404, body: { error: 'not_found', type, id } };
|
|
384
|
+
return { status: 200, body: found };
|
|
385
|
+
}
|
|
386
|
+
// Simulator/fork-mode write: the twin ACCEPTS a write as a LOCAL ACTION appended
|
|
387
|
+
// to the action log (R18) — NOT an observed event and NOT an egress/real write.
|
|
388
|
+
// State is the mirror with this action projected over it, so a subsequent read
|
|
389
|
+
// returns the change. The observed event log is untouched; the real service is
|
|
390
|
+
// provably untouched (no network I/O, no egress). Pushing the action to the real
|
|
391
|
+
// vendor is a separate, explicit step that records egress + confirms the action.
|
|
392
|
+
export async function applyTwinWrite(service, write, root) {
|
|
393
|
+
const action = actionForTwinWrite(service, write);
|
|
394
|
+
// A LOCAL VENDOR WRITE IS AN OCCURRENCE. Two calls are two actions, even byte-identical in the
|
|
395
|
+
// same instant, and the ordinal in the action id says which occurrence this is. The kernel used
|
|
396
|
+
// to derive identity from (content + occurredAt millisecond) and drop a repeat as `replayed`,
|
|
397
|
+
// which silently lost any write returning a subject to a value it previously held — permanently,
|
|
398
|
+
// under a pinned world clock, where every write in a world shares one instant. That default cost
|
|
399
|
+
// github, slack and jira live defects while the recipe told each pack to defend itself with a
|
|
400
|
+
// per-write ordinal and a fifth of the catalog opted out via `uniqueness`. A default every pack
|
|
401
|
+
// must remember to defend against is not a default. See docs/contributing/adding-a-twin.md
|
|
402
|
+
// #5-build-on-the-shared-kernel--dont-reinvent, "A local write is an occurrence".
|
|
403
|
+
//
|
|
404
|
+
// AT-MOST-ONCE is opt in, by KEY: `idempotencyKey` makes the id a function of the caller's own
|
|
405
|
+
// request identity, so a re-issue collapses onto the first and answers `replayed`. Content can
|
|
406
|
+
// never decide this — it cannot separate the same request delivered twice from the same change
|
|
407
|
+
// made twice — which is why the kernel no longer tries.
|
|
408
|
+
//
|
|
409
|
+
// `uniqueness` is subsumed and still honoured: it was the escape hatch packs reached for when
|
|
410
|
+
// the default was backwards (a billable AI completion that must ledger every call), and with
|
|
411
|
+
// occurrence semantics it is simply redundant rather than wrong.
|
|
412
|
+
//
|
|
413
|
+
// correlationId (D3): a caller-supplied request-scoped id lands on the action row; the appenders
|
|
414
|
+
// generate one when omitted, so it's never missing.
|
|
415
|
+
const committed = write.idempotencyKey || write.actionId
|
|
416
|
+
? appendActionIfAbsent(action, root)
|
|
417
|
+
: appendActionOccurrence(action, root);
|
|
418
|
+
// The COMMITTED action's id, not the one computed before the lock: an occurrence past the first
|
|
419
|
+
// carries an ordinal, and a caller told the base id would be naming an action that is not there.
|
|
420
|
+
const { appended, action: landed } = committed;
|
|
421
|
+
// THE HEAD (head.ts): a twin whose root says `auto` performs the entry now, before the app is
|
|
422
|
+
// answered; the vendor's id, when it minted one, is the id the answer carries.
|
|
423
|
+
// a write landed as the placeholder's default data (a seed) is a pull, never a commit: the head is not asked
|
|
424
|
+
const head = appended && !committed.placeholder ? await performAtHead(service, landed, root) : { performed: false };
|
|
425
|
+
const answerId = head.externalId ?? write.subjectId;
|
|
426
|
+
const result = { status: appended ? 'performed' : 'replayed', actionId: landed.id, ...(head.externalId ? { externalId: head.externalId } : {}), ...(head.data !== undefined ? { vendorData: head.data } : {}) };
|
|
427
|
+
// THE WRITTEN RESOURCE, read when a caller asks for it. Projecting the whole tree after every write to answer it
|
|
428
|
+
// made each write O(tree) and a batch of N writes O(N x tree), whether or not the caller used the answer (a Timestream
|
|
429
|
+
// seed of a member's wearable history stalled its twin for minutes). Resolved by (type, id): an id alone is ambiguous
|
|
430
|
+
// when two resource TYPES share it.
|
|
431
|
+
let resolved;
|
|
432
|
+
return {
|
|
433
|
+
result,
|
|
434
|
+
get resource() {
|
|
435
|
+
return resolved ??= projectResources(service, root).find((r) => r.type === write.subjectType && r.id === answerId)
|
|
436
|
+
?? { id: answerId, type: write.subjectType, updatedAt: landed.occurredAt, ...write.fields };
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
export async function createTwinServer(options) {
|
|
441
|
+
const readOnly = options.readOnly ?? false;
|
|
442
|
+
const serveOptions = {
|
|
443
|
+
port: options.port ?? 0,
|
|
444
|
+
idleTimeout: 60,
|
|
445
|
+
async fetch(request) {
|
|
446
|
+
const url = new URL(request.url);
|
|
447
|
+
const json = (status, body) => new Response(JSON.stringify(body, null, 2), { status, headers: { 'content-type': 'application/json' } });
|
|
448
|
+
if (request.method === 'GET') {
|
|
449
|
+
// Re-read per request so a concurrently-syncing twin serves fresh state.
|
|
450
|
+
const { status, body } = resolveTwinRead(options.service, url.pathname, { root: options.root });
|
|
451
|
+
return json(status, body);
|
|
452
|
+
}
|
|
453
|
+
// Writes are accepted as local actions unless this twin was started read-only.
|
|
454
|
+
if (readOnly)
|
|
455
|
+
return json(405, { error: 'read_only', hint: 'this twin was started read-only; omit readOnly to accept writes' });
|
|
456
|
+
const parts = url.pathname.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
|
|
457
|
+
if (parts.length < 2)
|
|
458
|
+
return json(400, { error: 'write_needs_type_and_id', hint: 'POST /<type>/<id> with a JSON body of fields' });
|
|
459
|
+
let fields;
|
|
460
|
+
try {
|
|
461
|
+
fields = (await request.json());
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return json(400, { error: 'invalid_json_body' });
|
|
465
|
+
}
|
|
466
|
+
// AT-MOST-ONCE OVER HTTP, the way vendors spell it. A local write is an occurrence, so an
|
|
467
|
+
// identical repeat POST is a second action and answers 201 — which is the truth, and a change
|
|
468
|
+
// from the old content-dedupe behaviour. A caller that means "the same request again" sends
|
|
469
|
+
// `Idempotency-Key`, exactly as Stripe and others define it, and gets 200 `replayed`. Without
|
|
470
|
+
// this the `replayed` arm below was unreachable on the kernel's own write endpoint.
|
|
471
|
+
const idempotencyKey = request.headers.get('idempotency-key') ?? undefined;
|
|
472
|
+
let committed;
|
|
473
|
+
try {
|
|
474
|
+
committed = await applyTwinWrite(options.service, {
|
|
475
|
+
operation: `${request.method.toLowerCase()}.${parts[0]}`,
|
|
476
|
+
subjectType: parts[0],
|
|
477
|
+
subjectId: decodeURIComponent(parts.slice(1).join('/')),
|
|
478
|
+
fields,
|
|
479
|
+
actor: { kind: 'agent' },
|
|
480
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
481
|
+
}, options.root);
|
|
482
|
+
}
|
|
483
|
+
catch (e) {
|
|
484
|
+
// A key REUSED WITH A DIFFERENT PAYLOAD is the caller's error, and the vendors that have
|
|
485
|
+
// this header say so with a 400 and an `idempotency_error` code. The kernel raises it as a
|
|
486
|
+
// conflicting-duplicate throw, which this server was letting escape as a 500 HTML page —
|
|
487
|
+
// found 2026-09-04 in review, one day after the header was wired. Anything else is still
|
|
488
|
+
// a genuine failure and still propagates.
|
|
489
|
+
if (idempotencyKey && e instanceof Error && e.message.startsWith('Conflicting duplicate twin action')) {
|
|
490
|
+
return json(400, { error: 'idempotency_error', message: `Keys for idempotent requests can only be used with the same parameters they were first used with. Try using a key other than '${idempotencyKey}' if you meant to execute a different request.` });
|
|
491
|
+
}
|
|
492
|
+
throw e;
|
|
493
|
+
}
|
|
494
|
+
const { result, resource } = committed;
|
|
495
|
+
return json(result.status === 'replayed' ? 200 : 201, { status: result.status, resource });
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
// This generic server's twin is an ARGUMENT, not a fact about a pack, so it NAMES ITSELF for the
|
|
499
|
+
// request journal rather than being recognised from its call site. Every vendor pack is journaled
|
|
500
|
+
// without declaring anything (installTwinRequestJournal, above); this is the lone declaration.
|
|
501
|
+
Object.defineProperty(serveOptions, TWIN_JOURNAL_IDENTITY, {
|
|
502
|
+
value: { service: options.service, ...(options.root !== undefined ? { root: options.root } : {}) },
|
|
503
|
+
enumerable: true,
|
|
504
|
+
});
|
|
505
|
+
const server = await serveHttp(serveOptions);
|
|
506
|
+
return { port: server.port, stop: () => { void server.stop(true); } };
|
|
507
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// A disposable lookup cache, not a byte store. Only owning World files retain payloads.
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { closeSync, constants, fstatSync, ftruncateSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
const MAX_BUCKET_BYTES = 128 * 1024;
|
|
6
|
+
const MAX_ENTRIES = 32;
|
|
7
|
+
const MAX_PATHS = 4;
|
|
8
|
+
function privateFile(stat) {
|
|
9
|
+
return stat.isFile() && stat.nlink === 1 && (!process.getuid || stat.uid === process.getuid()) && (stat.mode & 0o077) === 0;
|
|
10
|
+
}
|
|
11
|
+
function privateDirectory(root) {
|
|
12
|
+
try {
|
|
13
|
+
const stat = lstatSync(root);
|
|
14
|
+
return stat.isDirectory() && (stat.mode & 0o077) === 0 && (!process.getuid || stat.uid === process.getuid());
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function readIndex(file) {
|
|
21
|
+
let fd;
|
|
22
|
+
try {
|
|
23
|
+
fd = openSync(file, constants.O_RDONLY | constants.O_NONBLOCK | (constants.O_NOFOLLOW ?? 0));
|
|
24
|
+
const stat = fstatSync(fd);
|
|
25
|
+
if (!privateFile(stat) || stat.size > MAX_BUCKET_BYTES)
|
|
26
|
+
return {};
|
|
27
|
+
const parsed = JSON.parse(readFileSync(fd, 'utf8'));
|
|
28
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
29
|
+
return {};
|
|
30
|
+
const index = {};
|
|
31
|
+
for (const [key, paths] of Object.entries(parsed).slice(-MAX_ENTRIES)) {
|
|
32
|
+
if (!/^\d+:[a-f0-9]{64}$/.test(key) || !Array.isArray(paths))
|
|
33
|
+
continue;
|
|
34
|
+
index[key] = paths.filter((path) => typeof path === 'string' && path.length <= 4096 && isAbsolute(path)).slice(0, MAX_PATHS);
|
|
35
|
+
}
|
|
36
|
+
return index;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
if (fd !== undefined)
|
|
43
|
+
closeSync(fd);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function digestFile(file, size) {
|
|
47
|
+
const fd = openSync(file, 'r');
|
|
48
|
+
try {
|
|
49
|
+
const hash = createHash('sha256');
|
|
50
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
51
|
+
let remaining = size;
|
|
52
|
+
while (remaining > 0) {
|
|
53
|
+
const length = readSync(fd, buffer, 0, Math.min(buffer.length, remaining), null);
|
|
54
|
+
if (!length)
|
|
55
|
+
return null;
|
|
56
|
+
hash.update(buffer.subarray(0, length));
|
|
57
|
+
remaining -= length;
|
|
58
|
+
}
|
|
59
|
+
if (readSync(fd, buffer, 0, 1, null))
|
|
60
|
+
return null;
|
|
61
|
+
return hash.digest('hex');
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
closeSync(fd);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function sharedBlobIndex(root, destinationDevice, digest) {
|
|
68
|
+
// 256 buckets with bounded entries/bytes keep metadata bounded across roots and devices.
|
|
69
|
+
const file = join(root, `${digest.slice(0, 2)}.json`);
|
|
70
|
+
const entry = `${destinationDevice}:${digest}`;
|
|
71
|
+
return {
|
|
72
|
+
reuse(temporary, size) {
|
|
73
|
+
if (!privateDirectory(root))
|
|
74
|
+
return false;
|
|
75
|
+
for (const candidate of readIndex(file)[entry] ?? []) {
|
|
76
|
+
try {
|
|
77
|
+
const source = lstatSync(candidate);
|
|
78
|
+
if (!source.isFile() || source.dev !== destinationDevice || source.size !== size
|
|
79
|
+
|| (process.getuid && source.uid !== process.getuid()))
|
|
80
|
+
continue;
|
|
81
|
+
linkSync(candidate, temporary);
|
|
82
|
+
// Check the actual linked file, not a source path that could have been replaced.
|
|
83
|
+
const linked = lstatSync(temporary);
|
|
84
|
+
if (linked.isFile() && linked.dev === destinationDevice && linked.size === size
|
|
85
|
+
&& (!process.getuid || linked.uid === process.getuid()) && digestFile(temporary, size) === digest)
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch { /* A cache miss, stale path, unsupported link, or corrupt candidate. */ }
|
|
89
|
+
rmSync(temporary, { force: true });
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
},
|
|
93
|
+
record(key) {
|
|
94
|
+
try {
|
|
95
|
+
const path = resolve(key);
|
|
96
|
+
if (path.length > 4096)
|
|
97
|
+
return;
|
|
98
|
+
const index = readIndex(file);
|
|
99
|
+
const paths = [path, ...(index[entry] ?? []).filter((old) => old !== path)].slice(0, MAX_PATHS);
|
|
100
|
+
delete index[entry];
|
|
101
|
+
index[entry] = paths;
|
|
102
|
+
let text = JSON.stringify(index);
|
|
103
|
+
while (Object.keys(index).length > MAX_ENTRIES || Buffer.byteLength(text) > MAX_BUCKET_BYTES) {
|
|
104
|
+
delete index[Object.keys(index)[0]];
|
|
105
|
+
text = JSON.stringify(index);
|
|
106
|
+
}
|
|
107
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
108
|
+
if (!privateDirectory(root))
|
|
109
|
+
return;
|
|
110
|
+
// A torn/concurrent index update is just a cache miss. Writing the bounded
|
|
111
|
+
// bucket directly avoids retaining metadata temp files after SIGKILL.
|
|
112
|
+
const fd = openSync(file, constants.O_WRONLY | constants.O_CREAT | constants.O_NONBLOCK | (constants.O_NOFOLLOW ?? 0), 0o600);
|
|
113
|
+
try {
|
|
114
|
+
if (!privateFile(fstatSync(fd)))
|
|
115
|
+
return;
|
|
116
|
+
ftruncateSync(fd, 0);
|
|
117
|
+
writeFileSync(fd, text);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
closeSync(fd);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch { /* Index storage is optional: the World already owns its complete bytes. */ }
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|