@volter/world-core 3.0.155 → 3.0.157
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/attach.cjs +2 -0
- package/capability-decision.cjs +6 -4
- package/dist/attach.cjs +2 -0
- package/dist/capability-decision.cjs +6 -4
- package/dist/generated/pack-facts.json +204 -4
- package/dist/inject.cjs +150 -86
- package/dist/network-policy.cjs +11 -1
- package/dist/network-policy.d.cts +1 -0
- package/dist/pack-facts.d.cts +3 -0
- package/dist/required-credential-transport.cjs +29 -0
- package/dist/required-credential-transport.d.cts +3 -0
- 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/blob-store.d.ts +16 -0
- package/dist/src/blob-store.js +76 -1
- 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/derived-core.d.ts +84 -7
- package/dist/src/derived-core.js +336 -61
- package/dist/src/derived-real.js +97 -15
- package/dist/src/derived.d.ts +5 -0
- package/dist/src/derived.js +3 -0
- package/dist/src/executor.d.ts +7 -0
- package/dist/src/executor.js +15 -1
- package/dist/src/fork.js +1 -1
- package/dist/src/git/lfs.d.ts +13 -3
- package/dist/src/git/lfs.js +41 -9
- package/dist/src/grpc-wire.js +2 -1
- package/dist/src/h2.js +63 -10
- package/dist/src/head.d.ts +5 -0
- package/dist/src/head.js +15 -0
- package/dist/src/history.d.ts +1 -1
- package/dist/src/history.js +7 -7
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.js +6 -1
- package/dist/src/live/ingest.js +9 -13
- package/dist/src/live/rtmp.js +7 -5
- package/dist/src/live/segmenter.js +7 -1
- package/dist/src/log.d.ts +23 -3
- package/dist/src/log.js +160 -37
- package/dist/src/machines.js +5 -1
- package/dist/src/managed-go-programs.d.ts +55 -0
- package/dist/src/managed-go-programs.js +133 -0
- 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 +97 -0
- package/dist/src/mongodb/stream.js +206 -0
- package/dist/src/observe.js +1 -1
- package/dist/src/pack-fetch.d.ts +6 -1
- package/dist/src/pack-fetch.js +69 -14
- package/dist/src/pack-paths.d.ts +2 -1
- package/dist/src/pack-paths.js +6 -1
- 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/resource-blob.d.ts +4 -0
- package/dist/src/resource-blob.js +8 -1
- package/dist/src/runtime-state.d.ts +11 -0
- package/dist/src/runtime-state.js +13 -0
- package/dist/src/runtime.d.ts +7 -5
- package/dist/src/runtime.js +7 -4
- package/dist/src/serve-http.d.ts +2 -0
- package/dist/src/serve-http.js +73 -6
- package/dist/src/serve.d.ts +10 -0
- package/dist/src/serve.js +43 -3
- package/dist/src/sockets.d.ts +4 -1
- package/dist/src/sockets.js +59 -6
- package/dist/src/storage.d.ts +2 -1
- package/dist/src/storage.js +6 -3
- package/dist/src/vendor-call.js +58 -0
- 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 +122 -7
- package/dist/src/world-store.js +959 -103
- package/dist/src/xml.d.ts +30 -0
- package/dist/src/xml.js +74 -0
- package/generated/pack-facts.json +204 -4
- package/inject.cjs +150 -86
- package/network-policy.cjs +11 -1
- package/network-policy.d.cts +1 -0
- package/pack-facts.d.cts +3 -0
- package/package.json +27 -1
- package/required-credential-transport.cjs +29 -0
- package/required-credential-transport.d.cts +3 -0
- package/src/actions.ts +78 -21
- package/src/ancestry.ts +46 -24
- package/src/blob-store.ts +59 -1
- package/src/changeset.ts +1 -1
- package/src/clickhouse/compression.ts +172 -0
- package/src/clickhouse/index.ts +2 -0
- package/src/derived-core.ts +356 -61
- package/src/derived-real.ts +80 -14
- package/src/derived.ts +9 -1
- package/src/executor.ts +17 -3
- package/src/fork.ts +1 -1
- package/src/git/lfs.ts +40 -9
- package/src/grpc-wire.ts +2 -1
- package/src/h2.ts +46 -5
- package/src/head.ts +15 -0
- package/src/history.ts +7 -7
- package/src/index.ts +7 -1
- package/src/live/ingest.ts +7 -3
- package/src/live/rtmp.ts +7 -5
- package/src/live/segmenter.ts +7 -1
- package/src/log.ts +147 -32
- package/src/machines.ts +5 -1
- package/src/managed-go-programs.ts +146 -0
- 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 +223 -0
- package/src/observe.ts +1 -1
- package/src/pack-fetch.ts +66 -16
- package/src/pack-paths.ts +5 -1
- 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/resource-blob.ts +9 -1
- package/src/runtime-state.ts +14 -0
- package/src/runtime.ts +10 -3
- package/src/serve-http.ts +65 -6
- package/src/serve.ts +45 -3
- package/src/sockets.ts +39 -8
- package/src/storage.ts +5 -3
- package/src/vendor-call.ts +54 -0
- package/src/world-boot.ts +6 -0
- package/src/world-clock.ts +6 -2
- package/src/world-store.ts +768 -91
- package/src/xml.ts +71 -0
package/src/ancestry.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { volterHome } from './volter-home.ts';
|
|
5
5
|
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { stateDirName } from './storage.ts';
|
|
7
|
-
import { getActiveWorldStore, type WorldStore } from './world-store.ts';
|
|
7
|
+
import { getActiveWorldStore, storageLockScope, withStorageLockOperation, type WorldLockOptions, type WorldStore } from './world-store.ts';
|
|
8
8
|
|
|
9
9
|
type Pin = { child: string; ownerPid: number; committed?: boolean };
|
|
10
10
|
type Ancestry = { generation: string; children: Pin[] };
|
|
@@ -18,16 +18,17 @@ const inside = (base: string, path: string) => { const rel = relative(canonicalS
|
|
|
18
18
|
* bookkeeping (generations, pins, branch pointers, removal receipts) and the not-being-removed check: every World
|
|
19
19
|
* sharing the VOLTER_HOME waits on it, so no append, fold or caller's critical section runs inside it, and no state lock
|
|
20
20
|
* is taken inside it (`withStateLock` takes its lock first). */
|
|
21
|
-
export function withAncestryLock<T>(fn: () => T): T {
|
|
21
|
+
export function withAncestryLock<T>(fn: () => T, options: WorldLockOptions = {}): T {
|
|
22
|
+
const scope = storageLockScope(options);
|
|
22
23
|
const store = getActiveWorldStore();
|
|
23
24
|
const active = entered ??= new WeakSet<WorldStore>();
|
|
24
25
|
if (active.has(store)) return fn();
|
|
25
26
|
const path = join(volterHome(), 'branch-ancestry.lock');
|
|
26
27
|
return store.withLock(path, () => {
|
|
27
28
|
active.add(store);
|
|
28
|
-
try { recoverRemovals(); return fn(); }
|
|
29
|
+
try { recoverRemovals(path); return fn(); }
|
|
29
30
|
finally { active.delete(store); }
|
|
30
|
-
}, { retainLiveOwner: true });
|
|
31
|
+
}, { ...scope, retainLiveOwner: true });
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/** The state locks this process holds, per store: taken again inside one, a lock is already ours. */
|
|
@@ -36,22 +37,33 @@ const heldBy = (store: WorldStore): Set<string> => { const map = heldLocks ??= n
|
|
|
36
37
|
|
|
37
38
|
/** Run `fn` holding the cross-process lock at `lockPath`, a lock of the directory it sits in: taken first, then the
|
|
38
39
|
* coordinator only to check that the directory is not being removed. Re-entrant within a process. */
|
|
39
|
-
export function withStateLock<T>(lockPath: string, fn: () => T): T {
|
|
40
|
+
export function withStateLock<T>(lockPath: string, fn: () => T, options: WorldLockOptions = {}): T {
|
|
41
|
+
const scope = storageLockScope(options);
|
|
40
42
|
const store = getActiveWorldStore();
|
|
41
43
|
const held = heldBy(store);
|
|
42
44
|
if (held.has(lockPath)) return fn();
|
|
43
45
|
if (entered?.has(store)) throw new Error(`State lock ${lockPath} taken inside the ancestry coordinator: a state lock is taken before it, never inside it`);
|
|
46
|
+
// A lock taken inside a held lock of the SAME directory asks the coordinator nothing: the outer take checked that
|
|
47
|
+
// the directory is not being removed, and a removal recorded since waits on that outer lock before it deletes
|
|
48
|
+
// anything, so this operation finishes first, as a holder that checked before the receipt does. Asked again, each
|
|
49
|
+
// write took the machine-wide coordinator three times for one directory.
|
|
50
|
+
const directory = dirname(lockPath);
|
|
51
|
+
const nested = [...held].some((path) => dirname(path) === directory);
|
|
44
52
|
return store.withLock(lockPath, () => {
|
|
45
|
-
|
|
53
|
+
// The removal check reads the receipts without the coordinator, and takes it only where one is there (S2 R17;
|
|
54
|
+
// architecture, the history lock): this holder already has the directory's lock, and a removal writes its receipt
|
|
55
|
+
// under the coordinator before it waits on every lock inside the tree, so a receipt not yet visible here belongs to
|
|
56
|
+
// a removal that waits for this holder, which finishes first. A visible receipt is checked, recovered, as before.
|
|
57
|
+
if (!nested && removalRecorded()) withAncestryLock(() => assertNotBeingRemoved(directory), { operation: 'removal preflight' });
|
|
46
58
|
held.add(lockPath);
|
|
47
59
|
try { return fn(); } finally { held.delete(lockPath); }
|
|
48
|
-
});
|
|
60
|
+
}, scope);
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
/** A state directory's history lock: its appends, captures, observation batches and folds. A branch's is taken before
|
|
52
64
|
* its ancestors'. */
|
|
53
|
-
export function withHistoryLock<T>(directory: string, fn: () => T): T {
|
|
54
|
-
return withStateLock(join(canonicalStatePath(directory), 'history.lock'), fn);
|
|
65
|
+
export function withHistoryLock<T>(directory: string, fn: () => T, options: WorldLockOptions = {}): T {
|
|
66
|
+
return withStateLock(join(canonicalStatePath(directory), 'history.lock'), fn, storageLockScope(options));
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
/** A lock file a holder has recorded itself in (`{pid, hostname}`, written before the holder runs anything): a file
|
|
@@ -61,9 +73,10 @@ function isHeldLock(text: string | null): boolean {
|
|
|
61
73
|
}
|
|
62
74
|
|
|
63
75
|
/** Several sibling directories' history locks at once (a World's services, a changeset's targets), in one order. */
|
|
64
|
-
export function withHistoryLocks<T>(directories: string[], fn: () => T): T {
|
|
76
|
+
export function withHistoryLocks<T>(directories: string[], fn: () => T, options: WorldLockOptions = {}): T {
|
|
77
|
+
const scope = storageLockScope(options);
|
|
65
78
|
const sorted = [...new Set(directories.map((d) => canonicalStatePath(d)))].sort();
|
|
66
|
-
const take = (i: number): T => (i === sorted.length ? fn() : withHistoryLock(sorted[i]!, () => take(i + 1)));
|
|
79
|
+
const take = (i: number): T => (i === sorted.length ? fn() : withHistoryLock(sorted[i]!, () => take(i + 1), scope));
|
|
67
80
|
return take(0);
|
|
68
81
|
}
|
|
69
82
|
|
|
@@ -105,7 +118,7 @@ export function stateGeneration(dir: string): string {
|
|
|
105
118
|
let meta = metadata(dir);
|
|
106
119
|
if (!meta) { meta = { generation: randomUUID(), children: [] }; store.writeAtomic(metaPath(dir), JSON.stringify(meta)); }
|
|
107
120
|
return meta.generation;
|
|
108
|
-
});
|
|
121
|
+
}, { operation: 'state generation' });
|
|
109
122
|
}
|
|
110
123
|
|
|
111
124
|
/** Must be called in the same critical section as child pointer publication. A crash before
|
|
@@ -220,19 +233,28 @@ function releaseOutgoing(removal: Removal): void {
|
|
|
220
233
|
|
|
221
234
|
/** A crash after deleting files cannot strand committed child pins. Receipts are replayed
|
|
222
235
|
* before any subsequent ancestry publication, so path reuse cannot hide completed removal. */
|
|
223
|
-
function recoverRemovals(): void {
|
|
236
|
+
function recoverRemovals(lockPath: string): void {
|
|
224
237
|
const store = getActiveWorldStore();
|
|
238
|
+
const receipts = store.list(removalDir()).filter(n => n.endsWith('.json'));
|
|
239
|
+
if (!receipts.length) { beingRemoved = []; return; }
|
|
225
240
|
const pending: string[] = [];
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
241
|
+
withStorageLockOperation(lockPath, 'ancestry removal recovery', () => {
|
|
242
|
+
for (const name of receipts) {
|
|
243
|
+
const file = join(removalDir(), name);
|
|
244
|
+
const removal = JSON.parse(store.read(file)!) as Removal;
|
|
245
|
+
if (typeof removal.path !== 'string' || !Array.isArray(removal.outgoing)) throw new Error('Invalid branch removal receipt; retained');
|
|
246
|
+
if (!store.exists(removal.path)) { releaseOutgoing(removal); store.remove(file); }
|
|
247
|
+
else pending.push(removal.path);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
233
250
|
beingRemoved = pending;
|
|
234
251
|
}
|
|
235
252
|
|
|
253
|
+
/** Whether any removal receipt is recorded now: read without the coordinator (withStateLock). */
|
|
254
|
+
function removalRecorded(): boolean {
|
|
255
|
+
return getActiveWorldStore().list(removalDir()).some((name) => name.endsWith('.json'));
|
|
256
|
+
}
|
|
257
|
+
|
|
236
258
|
/** State whose removal is recorded and not yet complete: its deletion runs outside the coordinator, so nothing may pin
|
|
237
259
|
* it as a parent or write into it meanwhile. Read under the coordinator, where recoverRemovals has just refreshed it. */
|
|
238
260
|
let beingRemoved: string[] = [];
|
|
@@ -260,18 +282,18 @@ export function withStateRemoval<T>(path: string, remove: () => T, check?: () =>
|
|
|
260
282
|
store.writeAtomic(receipt, JSON.stringify(removal));
|
|
261
283
|
beingRemoved = [...beingRemoved, removal.path];
|
|
262
284
|
return { removal, receipt };
|
|
263
|
-
});
|
|
285
|
+
}, { operation: 'removal preflight' });
|
|
264
286
|
// Each lock is waited on alone, taken and let go: a holder that checked before the receipt finishes, and a taker
|
|
265
287
|
// after it refuses once it holds the lock (withStateLock), so the removal never holds one lock while waiting on
|
|
266
288
|
// another and no lock order can deadlock it. A wait that fails leaves nothing deleted: the receipt is released.
|
|
267
289
|
const held = heldBy(store);
|
|
268
290
|
try {
|
|
269
|
-
for (const lock of locksUnder(path).filter((l) => !held.has(l))) store.withLock(lock, () => undefined);
|
|
291
|
+
for (const lock of locksUnder(path).filter((l) => !held.has(l))) store.withLock(lock, () => undefined, { operation: 'removal preflight' });
|
|
270
292
|
} catch (error) {
|
|
271
293
|
withAncestryLock(() => {
|
|
272
294
|
if (store.exists(receipt)) store.remove(receipt);
|
|
273
295
|
beingRemoved = beingRemoved.filter((p) => p !== removal.path);
|
|
274
|
-
});
|
|
296
|
+
}, { operation: 'ancestry removal recovery' });
|
|
275
297
|
throw error;
|
|
276
298
|
}
|
|
277
299
|
const result = remove();
|
|
@@ -280,6 +302,6 @@ export function withStateRemoval<T>(path: string, remove: () => T, check?: () =>
|
|
|
280
302
|
// Another process's recovery may have released it already; both steps are idempotent.
|
|
281
303
|
releaseOutgoing(removal);
|
|
282
304
|
if (store.exists(receipt)) store.remove(receipt);
|
|
283
|
-
});
|
|
305
|
+
}, { operation: 'ancestry removal recovery' });
|
|
284
306
|
return result;
|
|
285
307
|
}
|
package/src/blob-store.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// local worlds change NOTHING — scrub, backup, and inspection all still see the same tree.
|
|
20
20
|
// A remote adapter maps the path to an object key (namespace prefix + path).
|
|
21
21
|
import { createHash, randomUUID } from 'node:crypto';
|
|
22
|
-
import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from 'node:fs';
|
|
23
23
|
import { dirname, join } from 'node:path';
|
|
24
24
|
import { volterHome } from './volter-home.ts';
|
|
25
25
|
import { sharedBlobIndex } from './shared-blob-index.ts';
|
|
@@ -46,6 +46,38 @@ export interface BlobStore {
|
|
|
46
46
|
* whole. Optional: `readBlobRange` falls back to `get` (a whole read) for a backend without it —
|
|
47
47
|
* the hosted World's R2 store today. */
|
|
48
48
|
getRange?(key: string, start: number, endInclusive: number): Promise<Uint8Array | null>;
|
|
49
|
+
/** A body's chunks written to `key` as they arrive and hashed as they are written, kept (atomically, as `put`)
|
|
50
|
+
* only when `accept` holds for their size and SHA-256; otherwise nothing is kept. True when kept. So an upload
|
|
51
|
+
* of a large object (a model's weights) never holds more than a chunk. Optional: `writeBlobStream` collects
|
|
52
|
+
* the chunks and `put`s them for a backend without it. */
|
|
53
|
+
putStream?(key: string, chunks: AsyncIterable<Uint8Array>, accept: (written: BlobWritten) => boolean): Promise<boolean>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** What a streamed write wrote: its byte length and its SHA-256 (hex). */
|
|
57
|
+
export type BlobWritten = { size: number; sha256: string };
|
|
58
|
+
|
|
59
|
+
/** A body's chunks to `key` through the active store, kept when `accept` holds for what was written (the store's
|
|
60
|
+
* own streamed write where it has one, else the chunks collected and `put`). True when kept. */
|
|
61
|
+
export async function writeBlobStream(key: string, chunks: AsyncIterable<Uint8Array>, accept: (written: BlobWritten) => boolean): Promise<boolean> {
|
|
62
|
+
const store = getActiveBlobStore();
|
|
63
|
+
if (store.putStream) return store.putStream(key, chunks, accept);
|
|
64
|
+
const parts: Uint8Array[] = [];
|
|
65
|
+
let size = 0;
|
|
66
|
+
for await (const chunk of chunks) { parts.push(chunk); size += chunk.byteLength; }
|
|
67
|
+
const bytes = new Uint8Array(size);
|
|
68
|
+
let at = 0;
|
|
69
|
+
for (const part of parts) { bytes.set(part, at); at += part.byteLength; }
|
|
70
|
+
if (!accept({ size, sha256: blobDigest(bytes) })) return false;
|
|
71
|
+
await store.put(key, bytes);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A web stream's chunks, read by its reader (a browser's ReadableStream need not be async-iterable). */
|
|
76
|
+
export async function* streamChunks(body: ReadableStream<Uint8Array>): AsyncGenerator<Uint8Array> {
|
|
77
|
+
const reader = body.getReader();
|
|
78
|
+
try {
|
|
79
|
+
for (;;) { const { done, value } = await reader.read(); if (done) return; if (value) yield value; }
|
|
80
|
+
} finally { reader.releaseLock(); }
|
|
49
81
|
}
|
|
50
82
|
|
|
51
83
|
/** A range of the blob at `key` through the active store: native when the store has it, else sliced. */
|
|
@@ -86,6 +118,30 @@ export class FsBlobStore implements BlobStore {
|
|
|
86
118
|
index?.record(key);
|
|
87
119
|
} finally { rmSync(tmp, { force: true }); }
|
|
88
120
|
}
|
|
121
|
+
async putStream(key: string, chunks: AsyncIterable<Uint8Array>, accept: (written: BlobWritten) => boolean): Promise<boolean> {
|
|
122
|
+
mkdirSync(dirname(key), { recursive: true });
|
|
123
|
+
const tmp = `${key}.tmp.${randomUUID()}`;
|
|
124
|
+
const hash = createHash('sha256');
|
|
125
|
+
let size = 0;
|
|
126
|
+
try {
|
|
127
|
+
const fd = openSync(tmp, 'wx');
|
|
128
|
+
try {
|
|
129
|
+
for await (const chunk of chunks) {
|
|
130
|
+
hash.update(chunk);
|
|
131
|
+
size += chunk.byteLength;
|
|
132
|
+
for (let at = 0; at < chunk.byteLength;) at += writeSync(fd, chunk, at, chunk.byteLength - at);
|
|
133
|
+
}
|
|
134
|
+
} finally { closeSync(fd); }
|
|
135
|
+
const sha256 = hash.digest('hex');
|
|
136
|
+
if (!accept({ size, sha256 })) return false;
|
|
137
|
+
renameSync(tmp, key);
|
|
138
|
+
if (this.options.sharedIndexRoot !== false) {
|
|
139
|
+
try { sharedBlobIndex(this.options.sharedIndexRoot ?? join(volterHome(), 'blob-index-v1'), statSync(dirname(key)).dev, sha256).record(key); }
|
|
140
|
+
catch { /* The index is an optimization, never a prerequisite for a blob write. */ }
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
} finally { rmSync(tmp, { force: true }); }
|
|
144
|
+
}
|
|
89
145
|
async exists(key: string): Promise<boolean> {
|
|
90
146
|
return existsSync(key);
|
|
91
147
|
}
|
|
@@ -178,7 +234,9 @@ function readOnlyBlobs(store: BlobStore): BlobStore {
|
|
|
178
234
|
exists: (key) => store.exists(key),
|
|
179
235
|
list: (prefix) => store.list(prefix),
|
|
180
236
|
size: (key) => store.size(key),
|
|
237
|
+
...(store.getRange ? { getRange: (key: string, start: number, endInclusive: number) => store.getRange!(key, start, endInclusive) } : {}),
|
|
181
238
|
put: async () => { refuseReadOnlyWrite('blobs'); },
|
|
239
|
+
putStream: async () => { refuseReadOnlyWrite('blobs'); return false; },
|
|
182
240
|
remove: async () => { refuseReadOnlyWrite('blobs'); },
|
|
183
241
|
};
|
|
184
242
|
}
|
package/src/changeset.ts
CHANGED
|
@@ -1042,7 +1042,7 @@ export type RebaseActionResult = {
|
|
|
1042
1042
|
export type RebaseReport = { changeset: string; from: string; to: string | null; at: string; outcome: 'unchanged' | 'rebased' | 'conflict'; actions: RebaseActionResult[] };
|
|
1043
1043
|
|
|
1044
1044
|
export function rebaseChangeset(changeset: Changeset, opts: { targets: RebaseTarget[]; at?: string }): { changeset: Changeset; report: RebaseReport } {
|
|
1045
|
-
return withHistoryLocks(opts.targets.map((t) => worldPaths(t.stateService, t.controlRoot).dir), () => rebaseChangesetLocked(changeset, opts));
|
|
1045
|
+
return withHistoryLocks(opts.targets.map((t) => worldPaths(t.stateService, t.controlRoot).dir), () => rebaseChangesetLocked(changeset, opts), { operation: 'branch-pointer adjustment' });
|
|
1046
1046
|
}
|
|
1047
1047
|
function rebaseChangesetLocked(changeset: Changeset, opts: { targets: RebaseTarget[]; at?: string }): { changeset: Changeset; report: RebaseReport } {
|
|
1048
1048
|
const at = opts.at ?? new Date().toISOString();
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// CityHashv1.0.2 arithmetic, ported from the vendor-carried Google source.
|
|
2
|
+
// Source: ClickHouse/ClickHouse contrib/cityhash102/src/city.cc, SHA256
|
|
3
|
+
// 4bcc8f0c09e3ef38210a0c568efc0550e4cb426719d8ad74a466f152296d0df7.
|
|
4
|
+
// Copyright (c) 2011 Google, Inc.
|
|
5
|
+
//
|
|
6
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
// in the Software without restriction, including without limitation the rights
|
|
9
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
// furnished to do so, subject to the following conditions:
|
|
12
|
+
//
|
|
13
|
+
// The above copyright notice and this permission notice shall be included in
|
|
14
|
+
// all copies or substantial portions of the Software.
|
|
15
|
+
//
|
|
16
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
22
|
+
// THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
const k0 = 0xc3a5c85c97cb3127n, k1 = 0xb492b66fbe98f273n, k2 = 0x9ae16a3b2f90404fn, k3 = 0xc949d7c7509e6557n;
|
|
25
|
+
type Pair = [bigint, bigint];
|
|
26
|
+
const u = (value: bigint): bigint => BigInt.asUintN(64, value);
|
|
27
|
+
const rotate = (value: bigint, shift: number): bigint => shift === 0 ? u(value) : u((u(value) >> BigInt(shift)) | (u(value) << BigInt(64 - shift)));
|
|
28
|
+
const mix = (value: bigint): bigint => u(value) ^ (u(value) >> 47n);
|
|
29
|
+
function hash16(first: bigint, second: bigint): bigint {
|
|
30
|
+
const mul = 0x9ddfea08eb382d69n;
|
|
31
|
+
let a = u((u(first) ^ u(second)) * mul); a ^= a >> 47n;
|
|
32
|
+
let b = u((u(second) ^ a) * mul); b ^= b >> 47n;
|
|
33
|
+
return u(b * mul);
|
|
34
|
+
}
|
|
35
|
+
/** The CityHash 1.0.2 checksum used by ClickHouse native compression frames. */
|
|
36
|
+
export function cityHash128(bytes: Uint8Array): Pair {
|
|
37
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
38
|
+
const fetch = (at: number): bigint => view.getBigUint64(at, true);
|
|
39
|
+
const short = (at: number, length: number): bigint => {
|
|
40
|
+
if (length > 8) { const a = fetch(at), b = fetch(at + length - 8); return hash16(a, rotate(u(b + BigInt(length)), length)) ^ b; }
|
|
41
|
+
if (length >= 4) return hash16(BigInt(length) + (BigInt(view.getUint32(at, true)) << 3n), BigInt(view.getUint32(at + length - 4, true)));
|
|
42
|
+
if (length) { const a = bytes[at]!, b = bytes[at + (length >> 1)]!, c = bytes[at + length - 1]!; return u(mix(u(BigInt(a + (b << 8)) * k2) ^ u(BigInt(length + (c << 2)) * k3)) * k2); }
|
|
43
|
+
return k2;
|
|
44
|
+
};
|
|
45
|
+
const weak = (at: number, first: bigint, second: bigint): Pair => {
|
|
46
|
+
const w = fetch(at), x = fetch(at + 8), y = fetch(at + 16), z = fetch(at + 24);
|
|
47
|
+
let a = u(first + w), b = rotate(u(second + a + z), 21); const c = a;
|
|
48
|
+
a = u(a + x + y); b = u(b + rotate(a, 44)); return [u(a + z), u(b + c)];
|
|
49
|
+
};
|
|
50
|
+
const murmur = (at: number, length: number, seed: Pair): Pair => {
|
|
51
|
+
let [a, b] = seed, c = 0n, d = 0n, left = length - 16;
|
|
52
|
+
if (left <= 0) { a = u(mix(u(a * k1)) * k1); c = u(b * k1 + short(at, length)); d = mix(u(a + (length >= 8 ? fetch(at) : c))); }
|
|
53
|
+
else {
|
|
54
|
+
c = hash16(u(fetch(at + length - 8) + k1), a); d = hash16(u(b + BigInt(length)), u(c + fetch(at + length - 16))); a = u(a + d);
|
|
55
|
+
do { a ^= u(mix(u(fetch(at) * k1)) * k1); a = u(a * k1); b ^= a; c ^= u(mix(u(fetch(at + 8) * k1)) * k1); c = u(c * k1); d ^= c; at += 16; left -= 16; } while (left > 0);
|
|
56
|
+
}
|
|
57
|
+
a = hash16(a, c); b = hash16(d, b); return [a ^ b, hash16(b, a)];
|
|
58
|
+
};
|
|
59
|
+
const seeded = (at: number, length: number, seed: Pair): Pair => {
|
|
60
|
+
if (length < 128) return murmur(at, length, seed);
|
|
61
|
+
let [x, y] = seed, z = u(BigInt(length) * k1);
|
|
62
|
+
let v: Pair = [u(rotate(y ^ k1, 49) * k1 + fetch(at)), 0n]; v[1] = u(rotate(v[0], 42) * k1 + fetch(at + 8));
|
|
63
|
+
let w: Pair = [u(rotate(u(y + z), 35) * k1 + x), u(rotate(u(x + fetch(at + 88)), 53) * k1)];
|
|
64
|
+
const round = (): void => {
|
|
65
|
+
x = u(rotate(u(x + y + v[0] + fetch(at + 16)), 37) * k1); y = u(rotate(u(y + v[1] + fetch(at + 48)), 42) * k1);
|
|
66
|
+
x ^= w[1]; y ^= v[0]; z = rotate(z ^ w[0], 33);
|
|
67
|
+
v = weak(at, u(v[1] * k1), u(x + w[0])); w = weak(at + 32, u(z + w[1]), y);
|
|
68
|
+
[z, x] = [x, z]; at += 64;
|
|
69
|
+
};
|
|
70
|
+
do { round(); round(); length -= 128; } while (length >= 128);
|
|
71
|
+
y = u(y + rotate(w[0], 37) * k0 + z); x = u(x + rotate(u(v[0] + z), 49) * k0);
|
|
72
|
+
for (let done = 0; done < length;) { done += 32; y = u(rotate(u(y - x), 42) * k0 + v[1]); w[0] = u(w[0] + fetch(at + length - done + 16)); x = u(rotate(x, 49) * k0 + w[0]); w[0] = u(w[0] + v[0]); v = weak(at + length - done, v[0], v[1]); }
|
|
73
|
+
x = hash16(x, v[0]); y = hash16(y, w[0]); return [u(hash16(u(x + v[1]), w[1]) + y), hash16(u(x + w[1]), u(y + v[1]))];
|
|
74
|
+
};
|
|
75
|
+
const length = bytes.length;
|
|
76
|
+
if (length >= 16) return seeded(16, length - 16, [fetch(0) ^ k3, fetch(8)]);
|
|
77
|
+
if (length >= 8) return seeded(0, 0, [fetch(0) ^ u(BigInt(length) * k0), fetch(length - 8) ^ k1]);
|
|
78
|
+
return seeded(0, length, [k0, k1]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ClickHouse src/Compression/CompressionInfo.h:7,14 (SHA25614b39ae2…): both limits are1GiB.
|
|
82
|
+
export const MAX_COMPRESSED_BLOCK_SIZE = 0x40000000;
|
|
83
|
+
export const MAX_DECOMPRESSED_BLOCK_SIZE = 0x40000000;
|
|
84
|
+
|
|
85
|
+
/** LZ4 block sequences. Validate all lengths before allocating the output buffer. */
|
|
86
|
+
export function decodeLz4Block(input: Uint8Array, size: number): Uint8Array {
|
|
87
|
+
if (!Number.isSafeInteger(size) || size < 0 || size > MAX_DECOMPRESSED_BLOCK_SIZE || input.length > MAX_COMPRESSED_BLOCK_SIZE)
|
|
88
|
+
throw new RangeError('Invalid LZ4 block size');
|
|
89
|
+
// lz4_Block_format.md:114–126 requires a final literal sequence, five final literals
|
|
90
|
+
// (except a short all-literal block), and the last match at least12 bytes before its end.
|
|
91
|
+
// This is the format specification's strictness; ClickHouse's decoder is not retained
|
|
92
|
+
// here and may accept malformed end sequences that this decoder rejects.
|
|
93
|
+
const walk = (output?: Uint8Array): void => {
|
|
94
|
+
let source = 0, target = 0, sequences = 0, finalLiterals = -1, lastMatchStart = -1;
|
|
95
|
+
const length = (initial: number): number => {
|
|
96
|
+
let result = initial;
|
|
97
|
+
if (initial === 15) { let value: number; do { if (source >= input.length) throw new RangeError('Truncated LZ4 length'); value = input[source++]!; result += value; } while (value === 255); }
|
|
98
|
+
return result;
|
|
99
|
+
};
|
|
100
|
+
while (source < input.length) {
|
|
101
|
+
sequences++;
|
|
102
|
+
const token = input[source++]!, literals = length(token >> 4);
|
|
103
|
+
if (literals > input.length - source || literals > size - target) throw new RangeError('Invalid LZ4 literal range');
|
|
104
|
+
output?.set(input.subarray(source, source + literals), target); source += literals; target += literals;
|
|
105
|
+
if (source === input.length) { finalLiterals = literals; break; }
|
|
106
|
+
if (source + 2 > input.length) throw new RangeError('Truncated LZ4 offset');
|
|
107
|
+
const offset = input[source++]! | (input[source++]! << 8); if (!offset || offset > target) throw new RangeError('Invalid LZ4 offset');
|
|
108
|
+
const match = length(token & 15) + 4; if (match > size - target) throw new RangeError('Invalid LZ4 match range');
|
|
109
|
+
lastMatchStart = target;
|
|
110
|
+
if (output) for (let i = 0; i < match; i++) { output[target] = output[target - offset]!; target++; }
|
|
111
|
+
else target += match;
|
|
112
|
+
}
|
|
113
|
+
if (finalLiterals < 0 || (size >= 5 && finalLiterals < 5) || (size < 5 && sequences !== 1) || (lastMatchStart >= 0 && size - lastMatchStart < 12))
|
|
114
|
+
throw new RangeError('Invalid LZ4 end sequence');
|
|
115
|
+
if (target !== size) throw new RangeError('LZ4 decoded size differs');
|
|
116
|
+
};
|
|
117
|
+
walk();
|
|
118
|
+
const output = new Uint8Array(size); walk(output); return output;
|
|
119
|
+
}
|
|
120
|
+
/** A valid literal-only LZ4 block. */
|
|
121
|
+
export function encodeLz4Literals(input: Uint8Array): Uint8Array {
|
|
122
|
+
if (input.length > MAX_DECOMPRESSED_BLOCK_SIZE) throw new RangeError('LZ4 block is too large');
|
|
123
|
+
const extension = input.length >= 15 ? Math.floor((input.length - 15) / 255) + 1 : 0;
|
|
124
|
+
const output = new Uint8Array(1 + extension + input.length); let at = 0;
|
|
125
|
+
output[at++] = Math.min(input.length, 15) << 4;
|
|
126
|
+
if (extension) { let left = input.length - 15; while (left >= 255) { output[at++] = 255; left -= 255; } output[at++] = left; }
|
|
127
|
+
output.set(input, at); return output;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export class ClickHouseCompressionError extends Error {
|
|
131
|
+
constructor(readonly code: number, message: string) { super(message); this.name = 'ClickHouseCompressionError'; }
|
|
132
|
+
}
|
|
133
|
+
/** ClickHouse compressed frame, including its sixteen-byte checksum.
|
|
134
|
+
* ICompressionCodec.cpp:62–69,102–116 (SHA2562cfbdbd7…) writes the9-byte header.
|
|
135
|
+
* CompressedReadBufferBase.cpp:193–194,255–256 (SHA2566f5af3a8…) reads low/high
|
|
136
|
+
* little-endian checksum words and verifies the complete header+payload before decompressing.
|
|
137
|
+
*/
|
|
138
|
+
export function encodeFrame(input: Uint8Array, method: 0x82 | 0x02 = 0x82): Uint8Array {
|
|
139
|
+
if (!input.length || input.length > MAX_DECOMPRESSED_BLOCK_SIZE) throw new ClickHouseCompressionError(39, 'Invalid decompressed block size');
|
|
140
|
+
if (method !== 0x82 && method !== 0x02) throw new ClickHouseCompressionError(89, 'Unknown compression method');
|
|
141
|
+
const bodyLength = method === 0x82 ? 1 + (input.length >= 15 ? Math.floor((input.length - 15) / 255) + 1 : 0) + input.length : input.length;
|
|
142
|
+
if (bodyLength + 9 > MAX_COMPRESSED_BLOCK_SIZE) throw new ClickHouseCompressionError(39, 'Compressed block is too large');
|
|
143
|
+
const body = method === 0x82 ? encodeLz4Literals(input) : input;
|
|
144
|
+
const output = new Uint8Array(25 + body.length), view = new DataView(output.buffer);
|
|
145
|
+
output[16] = method; view.setUint32(17, 9 + body.length, true); view.setUint32(21, input.length, true); output.set(body, 25);
|
|
146
|
+
const [low, high] = cityHash128(output.subarray(16)); view.setBigUint64(0, low, true); view.setBigUint64(8, high, true);
|
|
147
|
+
return output;
|
|
148
|
+
}
|
|
149
|
+
/** Decode one frame; undefined means the input has not yet supplied the whole frame.
|
|
150
|
+
* Sizes are validated from the header before waiting for its body, and the checksum
|
|
151
|
+
* precedes decompression. bytesRead allows the caller to retain subsequent frames.
|
|
152
|
+
*/
|
|
153
|
+
export function decodeFrame(input: Uint8Array): { data: Uint8Array; bytesRead: number } | undefined {
|
|
154
|
+
if (input.length < 25) return undefined;
|
|
155
|
+
const view = new DataView(input.buffer, input.byteOffset, input.byteLength), method = input[16]!;
|
|
156
|
+
// CompressedReadBufferBase.cpp:134–138 selects/refuses the codec before header sizes.
|
|
157
|
+
if (method !== 0x82 && method !== 0x02) throw new ClickHouseCompressionError(89, 'Unknown compression method');
|
|
158
|
+
const compressed = view.getUint32(17, true), size = view.getUint32(21, true);
|
|
159
|
+
if (compressed > MAX_COMPRESSED_BLOCK_SIZE || size > MAX_DECOMPRESSED_BLOCK_SIZE) throw new ClickHouseCompressionError(39, 'Compressed block size exceeds the vendor limit');
|
|
160
|
+
if (compressed < 9 || !size) throw new ClickHouseCompressionError(246, 'Corrupt compressed block header');
|
|
161
|
+
const bytesRead = 16 + compressed; if (input.length < bytesRead) return undefined;
|
|
162
|
+
const frame = input.subarray(16, bytesRead), [low, high] = cityHash128(frame);
|
|
163
|
+
if (low !== view.getBigUint64(0, true) || high !== view.getBigUint64(8, true)) throw new ClickHouseCompressionError(40, 'Checksum does not match');
|
|
164
|
+
const body = frame.subarray(9);
|
|
165
|
+
if (method === 0x02) {
|
|
166
|
+
if (body.length !== size) throw new ClickHouseCompressionError(246, 'Uncompressed block size differs');
|
|
167
|
+
// Own the returned bytes, as the LZ4 branch does.
|
|
168
|
+
return { data: body.slice(), bytesRead };
|
|
169
|
+
}
|
|
170
|
+
try { return { data: decodeLz4Block(body, size), bytesRead }; }
|
|
171
|
+
catch (error) { throw new ClickHouseCompressionError(246, error instanceof Error ? error.message : 'Invalid LZ4 block'); }
|
|
172
|
+
}
|