@volter/world-core 3.0.154 → 3.0.156
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/dist/generated/pack-facts.json +140 -0
- package/dist/inject.cjs +150 -86
- package/dist/src/actions.d.ts +15 -0
- package/dist/src/actions.js +82 -21
- package/dist/src/ancestry.d.ts +5 -4
- package/dist/src/ancestry.js +53 -28
- package/dist/src/changeset.js +1 -1
- package/dist/src/clickhouse/compression.d.ts +28 -0
- package/dist/src/clickhouse/compression.js +281 -0
- package/dist/src/clickhouse/index.d.ts +1 -0
- package/dist/src/clickhouse/index.js +1 -0
- package/dist/src/client-bundle.js +15 -15
- package/dist/src/derived-core.d.ts +12 -1
- package/dist/src/derived-core.js +145 -11
- package/dist/src/derived.js +3 -0
- package/dist/src/fork.js +1 -1
- package/dist/src/graphql-wire.d.ts +5 -0
- package/dist/src/graphql-wire.js +6 -6
- package/dist/src/grpc-wire.js +2 -1
- package/dist/src/h2.js +63 -10
- package/dist/src/head.d.ts +3 -0
- package/dist/src/head.js +9 -0
- package/dist/src/history.d.ts +1 -1
- package/dist/src/history.js +7 -7
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.js +5 -1
- package/dist/src/log.d.ts +23 -3
- package/dist/src/log.js +160 -37
- package/dist/src/managed-original-inputs.d.ts +101 -0
- package/dist/src/managed-original-inputs.js +125 -0
- package/dist/src/mongodb/client.d.ts +8 -0
- package/dist/src/mongodb/client.js +27 -0
- package/dist/src/mongodb/index.d.ts +2 -0
- package/dist/src/mongodb/index.js +5 -0
- package/dist/src/mongodb/stream.d.ts +74 -0
- package/dist/src/mongodb/stream.js +206 -0
- package/dist/src/observe.js +1 -1
- package/dist/src/pack-fetch.d.ts +2 -1
- package/dist/src/pack-fetch.js +21 -9
- package/dist/src/packRegistry.d.ts +10 -14
- package/dist/src/packRegistry.js +2 -13
- package/dist/src/protocol.d.ts +13 -0
- package/dist/src/protocol.js +13 -0
- package/dist/src/redis/engine.d.ts +10 -4
- package/dist/src/redis/engine.js +103 -32
- package/dist/src/redis/member-storage.js +20 -8
- package/dist/src/redis/service-times.d.ts +25 -0
- package/dist/src/redis/service-times.js +87 -0
- package/dist/src/redis/stream.d.ts +2 -1
- package/dist/src/redis/stream.js +109 -20
- package/dist/src/runtime-state.d.ts +11 -0
- package/dist/src/runtime-state.js +13 -0
- package/dist/src/runtime.d.ts +4 -2
- package/dist/src/runtime.js +5 -2
- package/dist/src/serve-http.d.ts +2 -0
- package/dist/src/serve-http.js +21 -4
- package/dist/src/serve.d.ts +10 -0
- package/dist/src/serve.js +43 -3
- package/dist/src/storage.d.ts +2 -1
- package/dist/src/storage.js +6 -3
- package/dist/src/world-boot.d.ts +1 -0
- package/dist/src/world-boot.js +6 -0
- package/dist/src/world-clock.js +6 -2
- package/dist/src/world-store.d.ts +108 -7
- package/dist/src/world-store.js +883 -103
- package/dist/src/xml.d.ts +30 -0
- package/dist/src/xml.js +74 -0
- package/generated/pack-facts.json +140 -0
- package/inject.cjs +150 -86
- package/package.json +20 -1
- package/src/actions.ts +78 -21
- package/src/ancestry.ts +46 -24
- package/src/changeset.ts +1 -1
- package/src/clickhouse/compression.ts +172 -0
- package/src/clickhouse/index.ts +2 -0
- package/src/client-bundle.ts +13 -14
- package/src/derived-core.ts +142 -11
- package/src/derived.ts +3 -0
- package/src/fork.ts +1 -1
- package/src/graphql-wire.ts +9 -6
- package/src/grpc-wire.ts +2 -1
- package/src/h2.ts +46 -5
- package/src/head.ts +9 -0
- package/src/history.ts +7 -7
- package/src/index.ts +6 -1
- package/src/log.ts +147 -32
- package/src/managed-original-inputs.ts +163 -0
- package/src/mongodb/client.ts +27 -0
- package/src/mongodb/index.ts +5 -0
- package/src/mongodb/stream.ts +213 -0
- package/src/observe.ts +1 -1
- package/src/pack-fetch.ts +23 -11
- package/src/packRegistry.ts +3 -15
- package/src/protocol.ts +13 -0
- package/src/redis/engine.ts +100 -57
- package/src/redis/member-storage.ts +17 -9
- package/src/redis/service-times.ts +81 -0
- package/src/redis/stream.ts +66 -14
- package/src/runtime-state.ts +14 -0
- package/src/runtime.ts +6 -1
- package/src/serve-http.ts +23 -4
- package/src/serve.ts +45 -3
- package/src/storage.ts +5 -3
- package/src/world-boot.ts +6 -0
- package/src/world-clock.ts +6 -2
- package/src/world-store.ts +704 -91
- package/src/xml.ts +71 -0
package/dist/src/actions.js
CHANGED
|
@@ -12,16 +12,16 @@ import { parseExactJson } from "./exact-json.js";
|
|
|
12
12
|
//
|
|
13
13
|
// Projection = observed mirror, then apply each `set` transaction in order,
|
|
14
14
|
// skipping any transaction that was reverted or confirmed.
|
|
15
|
-
import { withHistoryLock } from "./ancestry.js";
|
|
15
|
+
import { canonicalStatePath, withHistoryLock } from "./ancestry.js";
|
|
16
16
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
17
17
|
import { randomUUID } from 'node:crypto';
|
|
18
18
|
import { dirname, join } from 'node:path';
|
|
19
19
|
import { canonicalJson, subjectKey } from "./hash.js";
|
|
20
20
|
import { appendDurable, eventsLockPath, ownerStoreRoots, projectionLockPath, twinLog, withFileLock, withoutIdentityClaim, worldPaths } from "./storage.js";
|
|
21
21
|
import { landAsPlaceholder, placeholderPullActive } from "./placeholder-remote.js";
|
|
22
|
-
import { aliasesFrom, dropCheckpoint, landedCopy, landedIds, parentEntries, readTree, treeStamp } from "./log.js";
|
|
22
|
+
import { aliasesFrom, appendToBranchLog, dropCheckpoint, landedCopy, landedIds, parentEntries, readTree, treeStamp } from "./log.js";
|
|
23
23
|
import { inVendorMove, refuseReadOnlyWrite } from "./request-scope.js";
|
|
24
|
-
import { getActiveWorldStore } from "./world-store.js";
|
|
24
|
+
import { getActiveWorldStore, storageLockScope } from "./world-store.js";
|
|
25
25
|
import { currentTraceparent, traceparentForDelivery } from "./trace-context.js";
|
|
26
26
|
export class TwinActionPreconditionError extends Error {
|
|
27
27
|
actionId;
|
|
@@ -36,8 +36,11 @@ export class TwinActionPreconditionError extends Error {
|
|
|
36
36
|
function actionsPath(service, root) {
|
|
37
37
|
return join(dirname(worldPaths(service, root).events), 'actions.jsonl');
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// Canonical, as the history lock's path is (ancestry.ts withHistoryLock), so a take inside it is seen as nested in the same
|
|
40
|
+
// directory whatever the path it was asked by (/var or /private/var). The file is the same one, so a take by another
|
|
41
|
+
// process through the other spelling still excludes this one.
|
|
42
|
+
const actionsLock = (service, root) => join(canonicalStatePath(worldPaths(service, root).dir), 'actions.jsonl.lock');
|
|
43
|
+
const projectionLock = (service, root) => { const paths = worldPaths(service, root); return projectionLockPath({ ...paths, dir: canonicalStatePath(paths.dir) }); };
|
|
41
44
|
/** Every appended action carries a correlationId — generate one when the caller
|
|
42
45
|
* hasn't supplied it, so downstream joins (push ledger, logs) always have an id
|
|
43
46
|
* to key on (D3). Preserves a caller-supplied id (e.g. propagated from an HTTP
|
|
@@ -94,24 +97,38 @@ function actionIds(service, root) {
|
|
|
94
97
|
}
|
|
95
98
|
/** A service's projection lock, under its history lock: one directory's locks are taken history, projection,
|
|
96
99
|
* actions, events, in that order (architecture, the history lock). */
|
|
97
|
-
function withProjection(service, root, fn) {
|
|
98
|
-
|
|
100
|
+
function withProjection(service, root, fn, options = {}) {
|
|
101
|
+
const scope = storageLockScope(options);
|
|
102
|
+
return withHistoryLock(worldPaths(service, root).dir, () => withFileLock(projectionLock(service, root), fn, scope), scope);
|
|
99
103
|
}
|
|
100
104
|
function appendActionRaw(action, root) {
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
appendActionsRaw([action], root);
|
|
106
|
+
}
|
|
107
|
+
/** One service's actions appended as ONE store append, in order: the same lines `appendActionRaw` writes one at a time. */
|
|
108
|
+
function appendActionsRaw(actions, root) {
|
|
109
|
+
if (!actions.length)
|
|
110
|
+
return;
|
|
111
|
+
const service = actions[0].service;
|
|
112
|
+
const path = actionsPath(service, root);
|
|
113
|
+
// No mkdir: every append is made holding the projection lock (withProjection), whose file is in this same directory
|
|
114
|
+
// and was made there by the take (S2 R17 review).
|
|
103
115
|
const memos = idMemoSlot(path);
|
|
104
116
|
const held = memos.get(path);
|
|
105
117
|
const current = held !== undefined && held.key === logKey(path);
|
|
106
|
-
|
|
118
|
+
const data = actions.map((action) => `${JSON.stringify(action)}\n`).join('');
|
|
119
|
+
// the log is the branch log a read's tree is folded from: the append extends that memo too (log.ts appendToBranchLog)
|
|
120
|
+
const after = withHistoryLock(worldPaths(service, root).dir, () => appendToBranchLog(service, root, data, () => appendDurable(path, data)), { operation: 'action append' });
|
|
107
121
|
if (current) {
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
for (const action of actions)
|
|
123
|
+
held.ids.add(action.id);
|
|
124
|
+
held.key = after ?? logKey(path);
|
|
110
125
|
}
|
|
111
126
|
else
|
|
112
127
|
memos.delete(path);
|
|
113
|
-
|
|
114
|
-
|
|
128
|
+
for (const action of actions) {
|
|
129
|
+
twinLog('action.append', { service: action.service, id: action.id, op: action.op, correlationId: action.correlationId });
|
|
130
|
+
appendObserver?.(action, root);
|
|
131
|
+
}
|
|
115
132
|
}
|
|
116
133
|
/** ONE observer of appended entries (state-system.ts: the runtime's `deploy: auto` hook). Called
|
|
117
134
|
* under the append locks, so it must only schedule work, never do it. */
|
|
@@ -173,7 +190,7 @@ export function appendAction(action, root) {
|
|
|
173
190
|
return placeholder;
|
|
174
191
|
appendActionRaw(stamped, root);
|
|
175
192
|
return stamped;
|
|
176
|
-
}));
|
|
193
|
+
}, { operation: 'action append' }), { operation: 'action append' });
|
|
177
194
|
}
|
|
178
195
|
/** Append `action` only if no exact action with the same id already exists — the whole
|
|
179
196
|
* replay/precondition/append decision runs under the service projection + actions locks, so it's
|
|
@@ -196,7 +213,7 @@ export function appendActionIfAbsent(action, root) {
|
|
|
196
213
|
return { action: placeholder, appended: true, placeholder: true };
|
|
197
214
|
appendActionRaw(stamped, root);
|
|
198
215
|
return { action: stamped, appended: true };
|
|
199
|
-
}));
|
|
216
|
+
}, { operation: 'action append' }), { operation: 'action append' });
|
|
200
217
|
}
|
|
201
218
|
/**
|
|
202
219
|
* Append `build(n)` as an OCCURRENCE: the nth time this exact write has been made.
|
|
@@ -224,7 +241,51 @@ export function appendActionOccurrence(base, root) {
|
|
|
224
241
|
return { action: placeholder, appended: true, placeholder: true };
|
|
225
242
|
appendActionRaw(stamped, root);
|
|
226
243
|
return { action: stamped, appended: true };
|
|
227
|
-
}));
|
|
244
|
+
}, { operation: 'action append' }), { operation: 'action append' });
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Append several writes of ONE service as occurrences, in order, under one projection + actions lock and as one store
|
|
248
|
+
* append: each lands with the id, ordinal and correlation `appendActionOccurrence` would give it called once per write
|
|
249
|
+
* in the same order (a write's ordinal counts the batch's earlier writes as taken). A Redis run's flush made one
|
|
250
|
+
* append per changed member and key, each taking the history, projection and actions locks and appending alone, one
|
|
251
|
+
* after another under the twin's single command queue (S2 R19: Twenty's queue scripts spent up to 2.1 s of a 2 s
|
|
252
|
+
* window in flush while 38 commands waited). A write carrying preconditions is not taken here: each is checked against
|
|
253
|
+
* the state the earlier ones made, which only the one-at-a-time path sees. While a placeholder pull is open a write
|
|
254
|
+
* lands as observed events, as `appendActionOccurrence`'s does.
|
|
255
|
+
*/
|
|
256
|
+
export function appendActionOccurrences(bases, root) {
|
|
257
|
+
if (!bases.length)
|
|
258
|
+
return [];
|
|
259
|
+
const service = bases[0].service;
|
|
260
|
+
if (bases.some((base) => base.service !== service))
|
|
261
|
+
throw new Error(`appendActionOccurrences: one service per batch, got ${[...new Set(bases.map((base) => base.service))].join(', ')}`);
|
|
262
|
+
if (bases.some((base) => base.preconditions?.length))
|
|
263
|
+
throw new Error('appendActionOccurrences: a write with preconditions is appended alone');
|
|
264
|
+
refuseReadOnlyWrite(service);
|
|
265
|
+
return withProjection(service, root, () => withFileLock(actionsLock(service, root), () => {
|
|
266
|
+
const logged = actionIds(service, root);
|
|
267
|
+
const batch = new Set();
|
|
268
|
+
const taken = { has: (id) => batch.has(id) || logged.has(id) };
|
|
269
|
+
const landed = [];
|
|
270
|
+
let unwritten = [];
|
|
271
|
+
for (const base of bases) {
|
|
272
|
+
const stamped = occurrenceOf(base, taken, root);
|
|
273
|
+
const placeholder = landIfPlaceholderPull(stamped, root);
|
|
274
|
+
if (placeholder) {
|
|
275
|
+
// no action row: its id is not taken, as it is not in the log the one-at-a-time path reads
|
|
276
|
+
// the lines before it are written first, so the log and the pull keep the writes' order
|
|
277
|
+
appendActionsRaw(unwritten, root);
|
|
278
|
+
unwritten = [];
|
|
279
|
+
landed.push({ action: placeholder, appended: true, placeholder: true });
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
batch.add(stamped.id);
|
|
283
|
+
unwritten.push(stamped);
|
|
284
|
+
landed.push({ action: stamped, appended: true });
|
|
285
|
+
}
|
|
286
|
+
appendActionsRaw(unwritten, root);
|
|
287
|
+
return landed;
|
|
288
|
+
}, { operation: 'action append' }), { operation: 'action append' });
|
|
228
289
|
}
|
|
229
290
|
/** The occurrence rule, shared by both appenders that use it — the ordinal AND the merge base.
|
|
230
291
|
* Pure over (base, the log as it stands): the caller holds the lock. */
|
|
@@ -288,7 +349,7 @@ read = () => projectResources(service, root)) {
|
|
|
288
349
|
return { value: decision.value, action: placeholder, appended: true, placeholder: true };
|
|
289
350
|
appendActionRaw(stamped, root);
|
|
290
351
|
return { value: decision.value, action: stamped, appended: true };
|
|
291
|
-
}));
|
|
352
|
+
}, { operation: 'action decision' }), { operation: 'action decision' });
|
|
292
353
|
}
|
|
293
354
|
export function appendTransactionCommit(commit, root) {
|
|
294
355
|
return appendAction(commit, root);
|
|
@@ -439,9 +500,9 @@ export function confirmAction(opts) {
|
|
|
439
500
|
for (const copy of copies)
|
|
440
501
|
if (!held.has(copy.id))
|
|
441
502
|
appendDurable(paths.events, `${JSON.stringify(copy)}\n`);
|
|
442
|
-
});
|
|
503
|
+
}, { operation: 'action confirmation' });
|
|
443
504
|
dropCheckpoint(opts.service, opts.root);
|
|
444
|
-
});
|
|
505
|
+
}, { operation: 'action confirmation' });
|
|
445
506
|
const observedEventIds = copies.map((c) => c.id);
|
|
446
507
|
return { observedEventId: observedEventIds.at(-1), observedEventIds };
|
|
447
508
|
}
|
|
@@ -477,7 +538,7 @@ export function revertAction(opts) {
|
|
|
477
538
|
});
|
|
478
539
|
appendActionRaw(revert, opts.root);
|
|
479
540
|
return { status: 'reverted', revertId: revert.id };
|
|
480
|
-
}));
|
|
541
|
+
}, { operation: 'action revert' }), { operation: 'action revert' });
|
|
481
542
|
}
|
|
482
543
|
/** The LOCAL OVERLAY: pending actions (set, not reverted, not yet confirmed) — the divergence
|
|
483
544
|
* from the mirror, what a twin's projection folds over the observed state. A QUARANTINED row
|
package/dist/src/ancestry.d.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
+
import { type WorldLockOptions } from './world-store.js';
|
|
1
2
|
export declare const canonicalStatePath: (path: string) => string;
|
|
2
3
|
/** Synchronous only; use exclusive acquisition without age-stealing a live owner.
|
|
3
4
|
* The coordinator lives outside service, instance and hosted World deletion trees. It is held only for ancestry
|
|
4
5
|
* bookkeeping (generations, pins, branch pointers, removal receipts) and the not-being-removed check: every World
|
|
5
6
|
* sharing the VOLTER_HOME waits on it, so no append, fold or caller's critical section runs inside it, and no state lock
|
|
6
7
|
* is taken inside it (`withStateLock` takes its lock first). */
|
|
7
|
-
export declare function withAncestryLock<T>(fn: () => T): T;
|
|
8
|
+
export declare function withAncestryLock<T>(fn: () => T, options?: WorldLockOptions): T;
|
|
8
9
|
/** Run `fn` holding the cross-process lock at `lockPath`, a lock of the directory it sits in: taken first, then the
|
|
9
10
|
* coordinator only to check that the directory is not being removed. Re-entrant within a process. */
|
|
10
|
-
export declare function withStateLock<T>(lockPath: string, fn: () => T): T;
|
|
11
|
+
export declare function withStateLock<T>(lockPath: string, fn: () => T, options?: WorldLockOptions): T;
|
|
11
12
|
/** A state directory's history lock: its appends, captures, observation batches and folds. A branch's is taken before
|
|
12
13
|
* its ancestors'. */
|
|
13
|
-
export declare function withHistoryLock<T>(directory: string, fn: () => T): T;
|
|
14
|
+
export declare function withHistoryLock<T>(directory: string, fn: () => T, options?: WorldLockOptions): T;
|
|
14
15
|
/** Several sibling directories' history locks at once (a World's services, a changeset's targets), in one order. */
|
|
15
|
-
export declare function withHistoryLocks<T>(directories: string[], fn: () => T): T;
|
|
16
|
+
export declare function withHistoryLocks<T>(directories: string[], fn: () => T, options?: WorldLockOptions): T;
|
|
16
17
|
/** Create an identity only for an existing parent. A recreated path gets a new identity. */
|
|
17
18
|
export declare function stateGeneration(dir: string): string;
|
|
18
19
|
/** Must be called in the same critical section as child pointer publication. A crash before
|
package/dist/src/ancestry.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { volterHome } from "./volter-home.js";
|
|
5
5
|
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { stateDirName } from "./storage.js";
|
|
7
|
-
import { getActiveWorldStore } from "./world-store.js";
|
|
7
|
+
import { getActiveWorldStore, storageLockScope, withStorageLockOperation } from "./world-store.js";
|
|
8
8
|
let entered;
|
|
9
9
|
export const canonicalStatePath = (path) => getActiveWorldStore().canonicalPath?.(path) ?? resolve(path);
|
|
10
10
|
const metaPath = (dir) => join(dir, 'ancestry.json');
|
|
@@ -14,7 +14,8 @@ const inside = (base, path) => { const rel = relative(canonicalStatePath(base),
|
|
|
14
14
|
* bookkeeping (generations, pins, branch pointers, removal receipts) and the not-being-removed check: every World
|
|
15
15
|
* sharing the VOLTER_HOME waits on it, so no append, fold or caller's critical section runs inside it, and no state lock
|
|
16
16
|
* is taken inside it (`withStateLock` takes its lock first). */
|
|
17
|
-
export function withAncestryLock(fn) {
|
|
17
|
+
export function withAncestryLock(fn, options = {}) {
|
|
18
|
+
const scope = storageLockScope(options);
|
|
18
19
|
const store = getActiveWorldStore();
|
|
19
20
|
const active = entered ??= new WeakSet();
|
|
20
21
|
if (active.has(store))
|
|
@@ -23,13 +24,13 @@ export function withAncestryLock(fn) {
|
|
|
23
24
|
return store.withLock(path, () => {
|
|
24
25
|
active.add(store);
|
|
25
26
|
try {
|
|
26
|
-
recoverRemovals();
|
|
27
|
+
recoverRemovals(path);
|
|
27
28
|
return fn();
|
|
28
29
|
}
|
|
29
30
|
finally {
|
|
30
31
|
active.delete(store);
|
|
31
32
|
}
|
|
32
|
-
}, { retainLiveOwner: true });
|
|
33
|
+
}, { ...scope, retainLiveOwner: true });
|
|
33
34
|
}
|
|
34
35
|
/** The state locks this process holds, per store: taken again inside one, a lock is already ours. */
|
|
35
36
|
let heldLocks;
|
|
@@ -39,15 +40,27 @@ const heldBy = (store) => { const map = heldLocks ??= new WeakMap(); let set = m
|
|
|
39
40
|
} return set; };
|
|
40
41
|
/** Run `fn` holding the cross-process lock at `lockPath`, a lock of the directory it sits in: taken first, then the
|
|
41
42
|
* coordinator only to check that the directory is not being removed. Re-entrant within a process. */
|
|
42
|
-
export function withStateLock(lockPath, fn) {
|
|
43
|
+
export function withStateLock(lockPath, fn, options = {}) {
|
|
44
|
+
const scope = storageLockScope(options);
|
|
43
45
|
const store = getActiveWorldStore();
|
|
44
46
|
const held = heldBy(store);
|
|
45
47
|
if (held.has(lockPath))
|
|
46
48
|
return fn();
|
|
47
49
|
if (entered?.has(store))
|
|
48
50
|
throw new Error(`State lock ${lockPath} taken inside the ancestry coordinator: a state lock is taken before it, never inside it`);
|
|
51
|
+
// A lock taken inside a held lock of the SAME directory asks the coordinator nothing: the outer take checked that
|
|
52
|
+
// the directory is not being removed, and a removal recorded since waits on that outer lock before it deletes
|
|
53
|
+
// anything, so this operation finishes first, as a holder that checked before the receipt does. Asked again, each
|
|
54
|
+
// write took the machine-wide coordinator three times for one directory.
|
|
55
|
+
const directory = dirname(lockPath);
|
|
56
|
+
const nested = [...held].some((path) => dirname(path) === directory);
|
|
49
57
|
return store.withLock(lockPath, () => {
|
|
50
|
-
|
|
58
|
+
// The removal check reads the receipts without the coordinator, and takes it only where one is there (S2 R17;
|
|
59
|
+
// architecture, the history lock): this holder already has the directory's lock, and a removal writes its receipt
|
|
60
|
+
// under the coordinator before it waits on every lock inside the tree, so a receipt not yet visible here belongs to
|
|
61
|
+
// a removal that waits for this holder, which finishes first. A visible receipt is checked, recovered, as before.
|
|
62
|
+
if (!nested && removalRecorded())
|
|
63
|
+
withAncestryLock(() => assertNotBeingRemoved(directory), { operation: 'removal preflight' });
|
|
51
64
|
held.add(lockPath);
|
|
52
65
|
try {
|
|
53
66
|
return fn();
|
|
@@ -55,12 +68,12 @@ export function withStateLock(lockPath, fn) {
|
|
|
55
68
|
finally {
|
|
56
69
|
held.delete(lockPath);
|
|
57
70
|
}
|
|
58
|
-
});
|
|
71
|
+
}, scope);
|
|
59
72
|
}
|
|
60
73
|
/** A state directory's history lock: its appends, captures, observation batches and folds. A branch's is taken before
|
|
61
74
|
* its ancestors'. */
|
|
62
|
-
export function withHistoryLock(directory, fn) {
|
|
63
|
-
return withStateLock(join(canonicalStatePath(directory), 'history.lock'), fn);
|
|
75
|
+
export function withHistoryLock(directory, fn, options = {}) {
|
|
76
|
+
return withStateLock(join(canonicalStatePath(directory), 'history.lock'), fn, storageLockScope(options));
|
|
64
77
|
}
|
|
65
78
|
/** A lock file a holder has recorded itself in (`{pid, hostname}`, written before the holder runs anything): a file
|
|
66
79
|
* that only ends in `.lock` (a yarn.lock) is not one. */
|
|
@@ -74,9 +87,10 @@ function isHeldLock(text) {
|
|
|
74
87
|
}
|
|
75
88
|
}
|
|
76
89
|
/** Several sibling directories' history locks at once (a World's services, a changeset's targets), in one order. */
|
|
77
|
-
export function withHistoryLocks(directories, fn) {
|
|
90
|
+
export function withHistoryLocks(directories, fn, options = {}) {
|
|
91
|
+
const scope = storageLockScope(options);
|
|
78
92
|
const sorted = [...new Set(directories.map((d) => canonicalStatePath(d)))].sort();
|
|
79
|
-
const take = (i) => (i === sorted.length ? fn() : withHistoryLock(sorted[i], () => take(i + 1)));
|
|
93
|
+
const take = (i) => (i === sorted.length ? fn() : withHistoryLock(sorted[i], () => take(i + 1), scope));
|
|
80
94
|
return take(0);
|
|
81
95
|
}
|
|
82
96
|
/** The locks inside a tree a removal waits on before deleting it. */
|
|
@@ -125,7 +139,7 @@ export function stateGeneration(dir) {
|
|
|
125
139
|
store.writeAtomic(metaPath(dir), JSON.stringify(meta));
|
|
126
140
|
}
|
|
127
141
|
return meta.generation;
|
|
128
|
-
});
|
|
142
|
+
}, { operation: 'state generation' });
|
|
129
143
|
}
|
|
130
144
|
/** Must be called in the same critical section as child pointer publication. A crash before
|
|
131
145
|
* publication leaves a pending pin; it can be ignored only once its writer is provably dead. */
|
|
@@ -254,23 +268,34 @@ function releaseOutgoing(removal) {
|
|
|
254
268
|
}
|
|
255
269
|
/** A crash after deleting files cannot strand committed child pins. Receipts are replayed
|
|
256
270
|
* before any subsequent ancestry publication, so path reuse cannot hide completed removal. */
|
|
257
|
-
function recoverRemovals() {
|
|
271
|
+
function recoverRemovals(lockPath) {
|
|
258
272
|
const store = getActiveWorldStore();
|
|
273
|
+
const receipts = store.list(removalDir()).filter(n => n.endsWith('.json'));
|
|
274
|
+
if (!receipts.length) {
|
|
275
|
+
beingRemoved = [];
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
259
278
|
const pending = [];
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
279
|
+
withStorageLockOperation(lockPath, 'ancestry removal recovery', () => {
|
|
280
|
+
for (const name of receipts) {
|
|
281
|
+
const file = join(removalDir(), name);
|
|
282
|
+
const removal = JSON.parse(store.read(file));
|
|
283
|
+
if (typeof removal.path !== 'string' || !Array.isArray(removal.outgoing))
|
|
284
|
+
throw new Error('Invalid branch removal receipt; retained');
|
|
285
|
+
if (!store.exists(removal.path)) {
|
|
286
|
+
releaseOutgoing(removal);
|
|
287
|
+
store.remove(file);
|
|
288
|
+
}
|
|
289
|
+
else
|
|
290
|
+
pending.push(removal.path);
|
|
268
291
|
}
|
|
269
|
-
|
|
270
|
-
pending.push(removal.path);
|
|
271
|
-
}
|
|
292
|
+
});
|
|
272
293
|
beingRemoved = pending;
|
|
273
294
|
}
|
|
295
|
+
/** Whether any removal receipt is recorded now: read without the coordinator (withStateLock). */
|
|
296
|
+
function removalRecorded() {
|
|
297
|
+
return getActiveWorldStore().list(removalDir()).some((name) => name.endsWith('.json'));
|
|
298
|
+
}
|
|
274
299
|
/** State whose removal is recorded and not yet complete: its deletion runs outside the coordinator, so nothing may pin
|
|
275
300
|
* it as a parent or write into it meanwhile. Read under the coordinator, where recoverRemovals has just refreshed it. */
|
|
276
301
|
let beingRemoved = [];
|
|
@@ -298,21 +323,21 @@ export function withStateRemoval(path, remove, check) {
|
|
|
298
323
|
store.writeAtomic(receipt, JSON.stringify(removal));
|
|
299
324
|
beingRemoved = [...beingRemoved, removal.path];
|
|
300
325
|
return { removal, receipt };
|
|
301
|
-
});
|
|
326
|
+
}, { operation: 'removal preflight' });
|
|
302
327
|
// Each lock is waited on alone, taken and let go: a holder that checked before the receipt finishes, and a taker
|
|
303
328
|
// after it refuses once it holds the lock (withStateLock), so the removal never holds one lock while waiting on
|
|
304
329
|
// another and no lock order can deadlock it. A wait that fails leaves nothing deleted: the receipt is released.
|
|
305
330
|
const held = heldBy(store);
|
|
306
331
|
try {
|
|
307
332
|
for (const lock of locksUnder(path).filter((l) => !held.has(l)))
|
|
308
|
-
store.withLock(lock, () => undefined);
|
|
333
|
+
store.withLock(lock, () => undefined, { operation: 'removal preflight' });
|
|
309
334
|
}
|
|
310
335
|
catch (error) {
|
|
311
336
|
withAncestryLock(() => {
|
|
312
337
|
if (store.exists(receipt))
|
|
313
338
|
store.remove(receipt);
|
|
314
339
|
beingRemoved = beingRemoved.filter((p) => p !== removal.path);
|
|
315
|
-
});
|
|
340
|
+
}, { operation: 'ancestry removal recovery' });
|
|
316
341
|
throw error;
|
|
317
342
|
}
|
|
318
343
|
const result = remove();
|
|
@@ -323,6 +348,6 @@ export function withStateRemoval(path, remove, check) {
|
|
|
323
348
|
releaseOutgoing(removal);
|
|
324
349
|
if (store.exists(receipt))
|
|
325
350
|
store.remove(receipt);
|
|
326
|
-
});
|
|
351
|
+
}, { operation: 'ancestry removal recovery' });
|
|
327
352
|
return result;
|
|
328
353
|
}
|
package/dist/src/changeset.js
CHANGED
|
@@ -727,7 +727,7 @@ export function narrationDrift(changeset) {
|
|
|
727
727
|
return expected === changeset.narration ? null : { recorded: changeset.narration, expected };
|
|
728
728
|
}
|
|
729
729
|
export function rebaseChangeset(changeset, opts) {
|
|
730
|
-
return withHistoryLocks(opts.targets.map((t) => worldPaths(t.stateService, t.controlRoot).dir), () => rebaseChangesetLocked(changeset, opts));
|
|
730
|
+
return withHistoryLocks(opts.targets.map((t) => worldPaths(t.stateService, t.controlRoot).dir), () => rebaseChangesetLocked(changeset, opts), { operation: 'branch-pointer adjustment' });
|
|
731
731
|
}
|
|
732
732
|
function rebaseChangesetLocked(changeset, opts) {
|
|
733
733
|
const at = opts.at ?? new Date().toISOString();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
type Pair = [bigint, bigint];
|
|
2
|
+
/** The CityHash 1.0.2 checksum used by ClickHouse native compression frames. */
|
|
3
|
+
export declare function cityHash128(bytes: Uint8Array): Pair;
|
|
4
|
+
export declare const MAX_COMPRESSED_BLOCK_SIZE = 1073741824;
|
|
5
|
+
export declare const MAX_DECOMPRESSED_BLOCK_SIZE = 1073741824;
|
|
6
|
+
/** LZ4 block sequences. Validate all lengths before allocating the output buffer. */
|
|
7
|
+
export declare function decodeLz4Block(input: Uint8Array, size: number): Uint8Array;
|
|
8
|
+
/** A valid literal-only LZ4 block. */
|
|
9
|
+
export declare function encodeLz4Literals(input: Uint8Array): Uint8Array;
|
|
10
|
+
export declare class ClickHouseCompressionError extends Error {
|
|
11
|
+
readonly code: number;
|
|
12
|
+
constructor(code: number, message: string);
|
|
13
|
+
}
|
|
14
|
+
/** ClickHouse compressed frame, including its sixteen-byte checksum.
|
|
15
|
+
* ICompressionCodec.cpp:62–69,102–116 (SHA2562cfbdbd7…) writes the9-byte header.
|
|
16
|
+
* CompressedReadBufferBase.cpp:193–194,255–256 (SHA2566f5af3a8…) reads low/high
|
|
17
|
+
* little-endian checksum words and verifies the complete header+payload before decompressing.
|
|
18
|
+
*/
|
|
19
|
+
export declare function encodeFrame(input: Uint8Array, method?: 0x82 | 0x02): Uint8Array;
|
|
20
|
+
/** Decode one frame; undefined means the input has not yet supplied the whole frame.
|
|
21
|
+
* Sizes are validated from the header before waiting for its body, and the checksum
|
|
22
|
+
* precedes decompression. bytesRead allows the caller to retain subsequent frames.
|
|
23
|
+
*/
|
|
24
|
+
export declare function decodeFrame(input: Uint8Array): {
|
|
25
|
+
data: Uint8Array;
|
|
26
|
+
bytesRead: number;
|
|
27
|
+
} | undefined;
|
|
28
|
+
export {};
|