@bjornpagen/bumbledb-log 0.20.0 → 0.20.2
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/braids.d.ts +28 -0
- package/dist/braids.d.ts.map +1 -0
- package/{src/braids.ts → dist/braids.js} +22 -29
- package/dist/braids.js.map +1 -0
- package/dist/bytes.d.ts +26 -0
- package/dist/bytes.d.ts.map +1 -0
- package/dist/bytes.js +86 -0
- package/dist/bytes.js.map +1 -0
- package/dist/chain.d.ts +56 -0
- package/dist/chain.d.ts.map +1 -0
- package/dist/chain.js +137 -0
- package/dist/chain.js.map +1 -0
- package/dist/codec.d.ts +71 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +179 -0
- package/dist/codec.js.map +1 -0
- package/dist/descriptor.d.ts +61 -0
- package/dist/descriptor.d.ts.map +1 -0
- package/dist/descriptor.js +160 -0
- package/dist/descriptor.js.map +1 -0
- package/dist/errors.d.ts +202 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +122 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +52 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +166 -0
- package/dist/keys.js.map +1 -0
- package/dist/manifest.d.ts +42 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +116 -0
- package/dist/manifest.js.map +1 -0
- package/dist/replica.d.ts +154 -0
- package/dist/replica.d.ts.map +1 -0
- package/dist/replica.js +820 -0
- package/dist/replica.js.map +1 -0
- package/dist/store-s3.d.ts +31 -0
- package/dist/store-s3.d.ts.map +1 -0
- package/dist/store-s3.js +299 -0
- package/dist/store-s3.js.map +1 -0
- package/dist/store.d.ts +129 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +577 -0
- package/dist/store.js.map +1 -0
- package/dist/tenants.d.ts +56 -0
- package/dist/tenants.d.ts.map +1 -0
- package/dist/tenants.js +333 -0
- package/dist/tenants.js.map +1 -0
- package/dist/tilde-family.json +21 -0
- package/dist/vector.d.ts +31 -0
- package/dist/vector.d.ts.map +1 -0
- package/dist/vector.js +87 -0
- package/dist/vector.js.map +1 -0
- package/dist/writer.d.ts +106 -0
- package/dist/writer.d.ts.map +1 -0
- package/dist/writer.js +677 -0
- package/dist/writer.js.map +1 -0
- package/package.json +12 -7
- package/src/bytes.ts +0 -115
- package/src/chain.ts +0 -181
- package/src/codec.ts +0 -252
- package/src/descriptor.ts +0 -240
- package/src/errors.ts +0 -309
- package/src/index.ts +0 -57
- package/src/keys.ts +0 -218
- package/src/manifest.ts +0 -161
- package/src/replica.ts +0 -1061
- package/src/store-s3.ts +0 -377
- package/src/store.ts +0 -727
- package/src/tenants.ts +0 -414
- package/src/vector.ts +0 -103
- package/src/writer.ts +0 -977
package/dist/replica.js
ADDED
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The replica: a local store that is a materialized view of the
|
|
3
|
+
* braids' prefixes, plus the loop that keeps it current. Disposable by
|
|
4
|
+
* construction — the sidecar is a floor cache with one wholeness
|
|
5
|
+
* check; recovery IS the catch-up loop (L10). Generation is a total
|
|
6
|
+
* function of the chain: Settled sums the vector, Pending is that sum
|
|
7
|
+
* plus one. A replica that finds no manifest refuses; only a writer
|
|
8
|
+
* births a store.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from "node:fs/promises";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { factOf, internalBlake3, Db as SdkDb } from "@bjornpagen/bumbledb";
|
|
13
|
+
import * as errors from "@superbuilders/errors";
|
|
14
|
+
import { parse } from "#braids.ts";
|
|
15
|
+
import { bytesEqual, digest32, hex32, saturatingAddU64 } from "#bytes.ts";
|
|
16
|
+
import { chainGeneration, chainSum, readSidecar, writeSidecar } from "#chain.ts";
|
|
17
|
+
import { decodeBatch, encodeBatch, verifyChain } from "#codec.ts";
|
|
18
|
+
import { descriptorOf } from "#descriptor.ts";
|
|
19
|
+
import { ErrRefused, ErrReplayDiverged, refuse, refuseManifestMissing, wrapStore } from "#errors.ts";
|
|
20
|
+
import { CKPT_SCRATCH_LEASE, checkpointMdbKey, ckptDocKey, generation, LEASE_NAMESPACE, logKey, manifestKey, parseCkptScratch, TEMP_NAMESPACE } from "#keys.ts";
|
|
21
|
+
import { auditCatalog, parseCheckpoint, parseManifest } from "#manifest.ts";
|
|
22
|
+
import { Vector } from "#vector.ts";
|
|
23
|
+
const ZERO_HASH = digest32(new Uint8Array(32));
|
|
24
|
+
/** The gc-safety heartbeat cadence: every N-th pass re-reads the manifest. */
|
|
25
|
+
const HEARTBEAT_PASSES = 16;
|
|
26
|
+
/** The re-poll cadence of waitFor, its one consumer — the
|
|
27
|
+
* machine-constants table's `wait_for_poll_ms` fact. */
|
|
28
|
+
const WAIT_FOR_POLL_MS = 10;
|
|
29
|
+
const cores = new WeakMap();
|
|
30
|
+
function coreOf(replica) {
|
|
31
|
+
const core = cores.get(replica);
|
|
32
|
+
if (core === undefined) {
|
|
33
|
+
throw errors.new("not a replica of this driver");
|
|
34
|
+
}
|
|
35
|
+
return core;
|
|
36
|
+
}
|
|
37
|
+
/** One asynchronous door per replica: refreshes, commits, and disposal serialize. */
|
|
38
|
+
function withGate(core, body) {
|
|
39
|
+
const run = core.gate.then(body, body);
|
|
40
|
+
core.gate = run.then(function absorb() {
|
|
41
|
+
return undefined;
|
|
42
|
+
}, function absorbFailure() {
|
|
43
|
+
return undefined;
|
|
44
|
+
});
|
|
45
|
+
return run;
|
|
46
|
+
}
|
|
47
|
+
function blake3Digest(bytes) {
|
|
48
|
+
return digest32(new Uint8Array(internalBlake3(bytes)));
|
|
49
|
+
}
|
|
50
|
+
function sidecarPath(core) {
|
|
51
|
+
return path.join(core.dir, "chain");
|
|
52
|
+
}
|
|
53
|
+
let storeSequence = 0;
|
|
54
|
+
/** LMDB registers environments per canonical path for the life of the
|
|
55
|
+
* process and the engine has no close verb, so a store path is never
|
|
56
|
+
* reused: every bootstrap gets a fresh name and discards leave the old
|
|
57
|
+
* environment to GC. */
|
|
58
|
+
function freshStoreName() {
|
|
59
|
+
storeSequence += 1;
|
|
60
|
+
return `store-${process.pid.toString(36)}-${Date.now().toString(36)}-${storeSequence}`;
|
|
61
|
+
}
|
|
62
|
+
function storePath(core) {
|
|
63
|
+
return path.join(core.dir, core.storeName);
|
|
64
|
+
}
|
|
65
|
+
/** A wire braid id is a braid of this theory or a typed refuse. */
|
|
66
|
+
function braidOf(theory, raw) {
|
|
67
|
+
const id = Number.parseInt(raw.slice(1), 16);
|
|
68
|
+
const parsed = parse(theory, id);
|
|
69
|
+
if (parsed === undefined) {
|
|
70
|
+
refuse({ kind: "UnknownBraid" }, `unknown braid ${raw}`);
|
|
71
|
+
}
|
|
72
|
+
return parsed;
|
|
73
|
+
}
|
|
74
|
+
function zeroChain(descriptor) {
|
|
75
|
+
const chain = new Map();
|
|
76
|
+
for (const id of descriptor.braidMembers.keys()) {
|
|
77
|
+
chain.set(id, { g: generation(0n), prev: ZERO_HASH, ts: 0n });
|
|
78
|
+
}
|
|
79
|
+
return chain;
|
|
80
|
+
}
|
|
81
|
+
function entriesOf(core) {
|
|
82
|
+
return core.chain.entries;
|
|
83
|
+
}
|
|
84
|
+
function chainEntry(core, braid) {
|
|
85
|
+
const id = braidOf(core.theory, braid);
|
|
86
|
+
const entry = entriesOf(core).get(id);
|
|
87
|
+
if (entry === undefined) {
|
|
88
|
+
throw errors.new(`braid ${id} is not derived from this theory`);
|
|
89
|
+
}
|
|
90
|
+
return entry;
|
|
91
|
+
}
|
|
92
|
+
function generationOf(core) {
|
|
93
|
+
return core.db.read(function readGeneration(instance) {
|
|
94
|
+
return instance.generation;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** The opened store's catalog digest — the engine handle's computed claim. */
|
|
98
|
+
function catalogDigestOf(core) {
|
|
99
|
+
return digest32(core.db.catalogDigest());
|
|
100
|
+
}
|
|
101
|
+
function holdPending(core, batch, ops, ts) {
|
|
102
|
+
const held = { braid: batch.braid, slot: batch.slot, bytes: batch.bytes, ops, ts };
|
|
103
|
+
core.chain = { tag: "pending", entries: entriesOf(core), batch: held };
|
|
104
|
+
}
|
|
105
|
+
function pendingOf(core) {
|
|
106
|
+
if (core.chain.tag !== "pending") {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return core.chain.batch;
|
|
110
|
+
}
|
|
111
|
+
function settleHeld(core) {
|
|
112
|
+
core.chain = { tag: "settled", entries: entriesOf(core) };
|
|
113
|
+
}
|
|
114
|
+
/** The local vector equals the published checkpoint vector and the
|
|
115
|
+
* chain is Settled — the seed/open floor the catalog claim is audited
|
|
116
|
+
* against. */
|
|
117
|
+
function atCheckpointFloor(core) {
|
|
118
|
+
if (core.checkpoint === null || core.chain.tag === "pending") {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
for (const [braid, head] of core.checkpoint.braids) {
|
|
122
|
+
if (chainEntry(core, braid).g !== head.g) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
async function persistSidecar(core) {
|
|
129
|
+
await writeSidecar(core.descriptor.codec, sidecarPath(core), core.chain);
|
|
130
|
+
}
|
|
131
|
+
/** Writes the checkpoint store file and fsyncs the file and its parent
|
|
132
|
+
* directory — the mdb is durable before the sidecar. */
|
|
133
|
+
async function writeCheckpointMdb(dir, bytes) {
|
|
134
|
+
const data = path.join(dir, "data.mdb");
|
|
135
|
+
const handle = await fs.open(data, "w");
|
|
136
|
+
const written = await errors.try((async function writeAll() {
|
|
137
|
+
await handle.writeFile(bytes);
|
|
138
|
+
await handle.sync();
|
|
139
|
+
})());
|
|
140
|
+
await handle.close();
|
|
141
|
+
if (written.error) {
|
|
142
|
+
throw errors.wrap(written.error, `write checkpoint store ${data}`);
|
|
143
|
+
}
|
|
144
|
+
const parent = await fs.open(dir, "r");
|
|
145
|
+
const synced = await errors.try(parent.sync());
|
|
146
|
+
await parent.close();
|
|
147
|
+
if (synced.error) {
|
|
148
|
+
throw errors.wrap(synced.error, `fsync checkpoint store directory ${dir}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function settle(core) {
|
|
152
|
+
settleHeld(core);
|
|
153
|
+
await persistSidecar(core);
|
|
154
|
+
}
|
|
155
|
+
async function clearPending(core) {
|
|
156
|
+
await settle(core);
|
|
157
|
+
}
|
|
158
|
+
function disposed(core) {
|
|
159
|
+
if (core.closed) {
|
|
160
|
+
throw errors.new("replica is disposed");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function adoptChain(core, chain) {
|
|
164
|
+
const entries = new Map();
|
|
165
|
+
for (const [raw, entry] of chain.entries) {
|
|
166
|
+
entries.set(braidOf(core.theory, raw), entry);
|
|
167
|
+
}
|
|
168
|
+
for (const id of core.descriptor.braidMembers.keys()) {
|
|
169
|
+
if (!entries.has(id)) {
|
|
170
|
+
entries.set(id, { g: generation(0n), prev: ZERO_HASH, ts: 0n });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (chain.tag === "pending") {
|
|
174
|
+
const held = {
|
|
175
|
+
braid: braidOf(core.theory, chain.batch.braid),
|
|
176
|
+
slot: chain.batch.slot,
|
|
177
|
+
bytes: chain.batch.bytes,
|
|
178
|
+
ops: null,
|
|
179
|
+
ts: null
|
|
180
|
+
};
|
|
181
|
+
core.chain = { tag: "pending", entries, batch: held };
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
core.chain = { tag: "settled", entries };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** One `db.write` applying ops in listed order, rows in listed order.
|
|
188
|
+
* The positional row → named fact lift is the engine's `factOf` —
|
|
189
|
+
* closed-ref handles included. */
|
|
190
|
+
function applyOps(core, ops) {
|
|
191
|
+
return core.db.write(function applyBatch(tx) {
|
|
192
|
+
for (const op of ops) {
|
|
193
|
+
const member = core.theory.relations[op.relation];
|
|
194
|
+
if (member === undefined) {
|
|
195
|
+
throw errors.new(`batch op cites unknown relation ${op.relation}`);
|
|
196
|
+
}
|
|
197
|
+
const relation = member;
|
|
198
|
+
const facts = op.rows.map(function liftRow(row) {
|
|
199
|
+
return factOf(relation, row);
|
|
200
|
+
});
|
|
201
|
+
if (op.op === "insert") {
|
|
202
|
+
tx.insert(relation, facts);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
tx.delete(relation, facts);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return 0;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Decode, verify the chain, `db.write` the batch, then refuse a
|
|
213
|
+
* first-applied no-op against the identity. The sidecar advances
|
|
214
|
+
* only after that check; a rejected replay is phase-scoped:
|
|
215
|
+
* discard before the store has proven itself whole,
|
|
216
|
+
* `ErrReplayDiverged` after.
|
|
217
|
+
*/
|
|
218
|
+
async function applySlot(core, braid, slot, bytes, phase) {
|
|
219
|
+
const decoded = decodeBatch(core.descriptor, bytes);
|
|
220
|
+
const entry = chainEntry(core, braid);
|
|
221
|
+
verifyChain(decoded.header, braid, slot, { g: entry.g, prev: entry.prev, ts: entry.ts });
|
|
222
|
+
const outcome = applyOps(core, decoded.ops);
|
|
223
|
+
if (outcome.tag === "rejected") {
|
|
224
|
+
if (phase === "open") {
|
|
225
|
+
return { tag: "discard" };
|
|
226
|
+
}
|
|
227
|
+
throw errors.wrap(ErrReplayDiverged, `braid ${braid} slot ${slot} writer ${decoded.header.writer}`);
|
|
228
|
+
}
|
|
229
|
+
const identity = chainSum(core.chain) - entry.g + slot;
|
|
230
|
+
if (outcome.value.generation < identity) {
|
|
231
|
+
refuse({ kind: "NoOpSlot", braid, slot, writer: decoded.header.writer }, `braid ${braid} slot ${slot}: a first-applied slot changed nothing — publish-law violation by writer ${decoded.header.writer}`);
|
|
232
|
+
}
|
|
233
|
+
entriesOf(core).set(braid, { g: slot, prev: blake3Digest(bytes), ts: decoded.header.timestamp });
|
|
234
|
+
await persistSidecar(core);
|
|
235
|
+
return { tag: "applied", generation: outcome.value.generation };
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The published checkpoint vector is the one floor: a slot at or
|
|
239
|
+
* below it is retired, and a create must not touch the store.
|
|
240
|
+
*/
|
|
241
|
+
function belowFloor(core, braid, slot) {
|
|
242
|
+
const id = braidOf(core.theory, braid);
|
|
243
|
+
if (core.checkpoint === null) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
const floor = core.checkpoint.braids.get(id);
|
|
247
|
+
if (floor === undefined) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
return slot <= floor.g;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* The gc floor rule: below the current checkpoint's vector a 404 is a
|
|
254
|
+
* collected hole, at or above it the honest tip.
|
|
255
|
+
*/
|
|
256
|
+
function holeAt(core, braid, next) {
|
|
257
|
+
return belowFloor(core, braid, next);
|
|
258
|
+
}
|
|
259
|
+
function foldPending(sum, generation, occupant, pendingBytes, covered) {
|
|
260
|
+
if (covered) {
|
|
261
|
+
return { tag: "below-floor" };
|
|
262
|
+
}
|
|
263
|
+
if (occupant !== null && bytesEqual(occupant, pendingBytes)) {
|
|
264
|
+
return { tag: "ours" };
|
|
265
|
+
}
|
|
266
|
+
if (occupant !== null) {
|
|
267
|
+
return generation === sum ? { tag: "theirs-unapplied" } : { tag: "theirs-applied" };
|
|
268
|
+
}
|
|
269
|
+
if (generation === sum) {
|
|
270
|
+
return { tag: "absent-unapplied" };
|
|
271
|
+
}
|
|
272
|
+
if (generation === saturatingAddU64(sum, 1n)) {
|
|
273
|
+
return { tag: "absent-applied" };
|
|
274
|
+
}
|
|
275
|
+
return { tag: "phantom" };
|
|
276
|
+
}
|
|
277
|
+
function isCorruption(error) {
|
|
278
|
+
if (!(error instanceof Error)) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
return errors.is(error, ErrReplayDiverged) || errors.is(error, ErrRefused);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* One braid, one slot. Catch-up, refresh, waitFor, and open all call
|
|
285
|
+
* this; a hot braid cannot drain past one step per round.
|
|
286
|
+
*/
|
|
287
|
+
async function stepBraid(core, braid, phase) {
|
|
288
|
+
disposed(core);
|
|
289
|
+
const id = braidOf(core.theory, braid);
|
|
290
|
+
const wedged = core.wedged.get(id);
|
|
291
|
+
if (wedged !== undefined) {
|
|
292
|
+
return { tag: "wedged", braid: id, cause: wedged };
|
|
293
|
+
}
|
|
294
|
+
const next = generation(chainEntry(core, id).g + 1n);
|
|
295
|
+
const fetched = await core.store.get(logKey(core.prefix, id, next));
|
|
296
|
+
if (fetched === null) {
|
|
297
|
+
if (holeAt(core, id, next)) {
|
|
298
|
+
return { tag: "reseed", cause: "gap-below-floor" };
|
|
299
|
+
}
|
|
300
|
+
return { tag: "tip" };
|
|
301
|
+
}
|
|
302
|
+
const chain = core.chain;
|
|
303
|
+
if (chain.tag === "pending" && id === chain.batch.braid && next === chain.batch.slot) {
|
|
304
|
+
if (!bytesEqual(fetched.bytes, chain.batch.bytes)) {
|
|
305
|
+
return { tag: "reseed", cause: "lost-pending-fork" };
|
|
306
|
+
}
|
|
307
|
+
await settle(core);
|
|
308
|
+
}
|
|
309
|
+
const applied = await errors.try(applySlot(core, id, next, fetched.bytes, phase));
|
|
310
|
+
if (applied.error) {
|
|
311
|
+
if (phase === "steady" && isCorruption(applied.error)) {
|
|
312
|
+
core.wedged.set(id, applied.error.message);
|
|
313
|
+
return { tag: "wedged", braid: id, cause: applied.error.message };
|
|
314
|
+
}
|
|
315
|
+
throw applied.error;
|
|
316
|
+
}
|
|
317
|
+
if (applied.data.tag === "discard") {
|
|
318
|
+
return { tag: "reseed", cause: "rejected-in-open" };
|
|
319
|
+
}
|
|
320
|
+
return { tag: "applied" };
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* One pass: heartbeat, one slot per braid, wholeness, disposed. The
|
|
324
|
+
* same function refresh, waitFor, catch-up, and open execute.
|
|
325
|
+
*/
|
|
326
|
+
async function runPass(core, braids, phase) {
|
|
327
|
+
disposed(core);
|
|
328
|
+
core.passes += 1;
|
|
329
|
+
if (core.passes % HEARTBEAT_PASSES === 0) {
|
|
330
|
+
await refreshManifest(core);
|
|
331
|
+
}
|
|
332
|
+
const remaining = new Set(braids);
|
|
333
|
+
while (remaining.size > 0) {
|
|
334
|
+
disposed(core);
|
|
335
|
+
let progressed = false;
|
|
336
|
+
for (const braid of braids) {
|
|
337
|
+
if (!remaining.has(braid)) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const step = await stepBraid(core, braid, phase);
|
|
341
|
+
if (step.tag === "applied") {
|
|
342
|
+
progressed = true;
|
|
343
|
+
}
|
|
344
|
+
else if (step.tag === "tip" || step.tag === "wedged") {
|
|
345
|
+
remaining.delete(braid);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
return { tag: "reseed", cause: step.cause };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (!progressed) {
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (!wholenessHolds(core)) {
|
|
356
|
+
return { tag: "reseed", cause: "wholeness" };
|
|
357
|
+
}
|
|
358
|
+
for (const [braid, cause] of core.wedged) {
|
|
359
|
+
return { tag: "wedged", braid, cause };
|
|
360
|
+
}
|
|
361
|
+
return { tag: "advanced", vector: vectorOf(core) };
|
|
362
|
+
}
|
|
363
|
+
function allBraids(core) {
|
|
364
|
+
return [...core.descriptor.braidMembers.keys()];
|
|
365
|
+
}
|
|
366
|
+
function wholenessHolds(core) {
|
|
367
|
+
return generationOf(core) === chainGeneration(core.chain);
|
|
368
|
+
}
|
|
369
|
+
async function adoptManifest(core, bytes, etag) {
|
|
370
|
+
const manifest = parseManifest(bytes);
|
|
371
|
+
if (!bytesEqual(manifest.fingerprint, digest32(core.descriptor.fingerprintBytes))) {
|
|
372
|
+
refuse({
|
|
373
|
+
kind: "FingerprintMismatch",
|
|
374
|
+
carried: hex32(manifest.fingerprint),
|
|
375
|
+
expected: core.descriptor.fingerprint
|
|
376
|
+
}, "the store's manifest names a different theory");
|
|
377
|
+
}
|
|
378
|
+
if (manifest.checkpoint === null) {
|
|
379
|
+
core.checkpoint = null;
|
|
380
|
+
core.checkpointDigest = null;
|
|
381
|
+
}
|
|
382
|
+
else if (core.checkpointDigest === null || !bytesEqual(manifest.checkpoint, core.checkpointDigest)) {
|
|
383
|
+
const facts = await core.store.get(ckptDocKey(core.prefix, manifest.checkpoint));
|
|
384
|
+
if (facts === null) {
|
|
385
|
+
throw errors.new(`manifest points at absent checkpoint ${hex32(manifest.checkpoint)}`);
|
|
386
|
+
}
|
|
387
|
+
// The codec-backed parseCheckpoint judges the braid set against the
|
|
388
|
+
// sealed handle — an unknown or drifted braid refuses at parse.
|
|
389
|
+
core.checkpoint = parseCheckpoint(core.descriptor.codec, facts.bytes);
|
|
390
|
+
core.checkpointDigest = manifest.checkpoint;
|
|
391
|
+
}
|
|
392
|
+
// The pointer is adopted only after the checkpoint it names is in
|
|
393
|
+
// hand. A failed fetch leaves the old etag and the old floor (40/67).
|
|
394
|
+
core.manifestEtag = etag;
|
|
395
|
+
}
|
|
396
|
+
/** A replica never births a manifest. Absence is ManifestMissing. */
|
|
397
|
+
async function refreshManifest(core) {
|
|
398
|
+
if (core.manifestEtag !== null) {
|
|
399
|
+
const poll = await core.store.getIfChanged(manifestKey(core.prefix), core.manifestEtag);
|
|
400
|
+
if (poll.tag === "unchanged") {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
await adoptManifest(core, poll.fetched.bytes, poll.fetched.etag);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const fetched = await core.store.get(manifestKey(core.prefix));
|
|
407
|
+
if (fetched === null) {
|
|
408
|
+
refuseManifestMissing("the store has no manifest");
|
|
409
|
+
}
|
|
410
|
+
await adoptManifest(core, fetched.bytes, fetched.etag);
|
|
411
|
+
}
|
|
412
|
+
/** Bootstraps a fresh local store: from the current checkpoint when one
|
|
413
|
+
* exists, else `Db.create` at the zero vector. Always holds Settled —
|
|
414
|
+
* a prior Pending arm cannot survive beside the new vector. */
|
|
415
|
+
async function initializeStore(core) {
|
|
416
|
+
core.storeName = freshStoreName();
|
|
417
|
+
const target = storePath(core);
|
|
418
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
419
|
+
await fs.mkdir(core.dir, { recursive: true });
|
|
420
|
+
if (core.checkpoint !== null && core.checkpointDigest !== null) {
|
|
421
|
+
const mdb = await core.store.get(checkpointMdbKey(core.prefix, core.checkpointDigest));
|
|
422
|
+
if (mdb === null) {
|
|
423
|
+
throw errors.new(`checkpoint ${hex32(core.checkpointDigest)} names an absent .mdb`);
|
|
424
|
+
}
|
|
425
|
+
await fs.mkdir(target, { recursive: true });
|
|
426
|
+
await writeCheckpointMdb(target, mdb.bytes);
|
|
427
|
+
core.db = await SdkDb.open(target, core.theory);
|
|
428
|
+
core.chain = {
|
|
429
|
+
tag: "settled",
|
|
430
|
+
entries: new Map([...core.checkpoint.braids.entries()].map(function seed([raw, head]) {
|
|
431
|
+
const braid = braidOf(core.theory, raw);
|
|
432
|
+
return [braid, { g: head.g, prev: head.hash, ts: head.ts }];
|
|
433
|
+
}))
|
|
434
|
+
};
|
|
435
|
+
const opened = generationOf(core);
|
|
436
|
+
if (opened !== chainGeneration(core.chain)) {
|
|
437
|
+
throw errors.new(`checkpoint store opened at generation ${opened}, chain generation is ${chainGeneration(core.chain)}`);
|
|
438
|
+
}
|
|
439
|
+
auditCatalog(core.checkpoint, catalogDigestOf(core));
|
|
440
|
+
core.provenance = { tag: "checkpoint-seeded", catalog: core.checkpointDigest };
|
|
441
|
+
await persistSidecar(core);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const created = await SdkDb.create(target, core.theory);
|
|
445
|
+
if (created.tag === "rejected") {
|
|
446
|
+
throw errors.new("the theory's ground axioms were rejected at bootstrap");
|
|
447
|
+
}
|
|
448
|
+
core.db = created.value;
|
|
449
|
+
core.chain = { tag: "settled", entries: zeroChain(core.descriptor) };
|
|
450
|
+
core.provenance = { tag: "bootstrapped" };
|
|
451
|
+
await persistSidecar(core);
|
|
452
|
+
}
|
|
453
|
+
/** The scream tracks the set of repair signatures; a recurrence alarms. */
|
|
454
|
+
function screamOf(context) {
|
|
455
|
+
const seen = new Set();
|
|
456
|
+
let attempts = 0;
|
|
457
|
+
return {
|
|
458
|
+
attempt(signature) {
|
|
459
|
+
attempts += 1;
|
|
460
|
+
if (seen.has(signature)) {
|
|
461
|
+
console.error(`bumbledb-log alarm: ${context} repair signature recurs: ${signature}`);
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
seen.add(signature);
|
|
465
|
+
}
|
|
466
|
+
if (attempts % 8 === 0) {
|
|
467
|
+
console.error(`bumbledb-log warning: ${context} repair attempt ${attempts}: ${signature}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
/** The disposable law: the directory is cache, never truth. The local
|
|
473
|
+
* LMDB path rotates because the engine has no close verb — the old
|
|
474
|
+
* environment is left for GC while the fresh pull takes a new path.
|
|
475
|
+
* initializeStore writes Settled, so the open-identity is
|
|
476
|
+
* chainGeneration(core.chain) — there is no addend a reader can skip. */
|
|
477
|
+
async function discardAndReopen(core) {
|
|
478
|
+
const scream = screamOf("replica discard-and-re-pull");
|
|
479
|
+
for (;;) {
|
|
480
|
+
const old = storePath(core);
|
|
481
|
+
await fs.rm(old, { recursive: true, force: true });
|
|
482
|
+
core.wedged.clear();
|
|
483
|
+
await initializeStore(core);
|
|
484
|
+
const outcome = await runPass(core, allBraids(core), "open");
|
|
485
|
+
if (outcome.tag === "reseed") {
|
|
486
|
+
scream.attempt(outcome.cause);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (outcome.tag === "refused") {
|
|
490
|
+
throw errors.wrap(ErrRefused, outcome.detail);
|
|
491
|
+
}
|
|
492
|
+
if (generationOf(core) === chainGeneration(core.chain)) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
scream.attempt("wholeness");
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async function newestStoreDir(dir) {
|
|
499
|
+
const listed = await errors.try(fs.readdir(dir));
|
|
500
|
+
if (listed.error) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
let newest = null;
|
|
504
|
+
let newestAt = -1;
|
|
505
|
+
for (const name of listed.data) {
|
|
506
|
+
if (!name.startsWith("store-")) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const stat = await errors.try(fs.stat(path.join(dir, name)));
|
|
510
|
+
if (stat.error === undefined && stat.data.mtimeMs > newestAt) {
|
|
511
|
+
newestAt = stat.data.mtimeMs;
|
|
512
|
+
newest = name;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return newest;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Open reclaims the reserved `~tmp`/`~lease` namespace. The known
|
|
519
|
+
* document `{dir}/~lease/ckpt-scratch` names a crash-stranded
|
|
520
|
+
* candidate; when that digest is not the live head the ckpt pair is
|
|
521
|
+
* deleted with the lease.
|
|
522
|
+
*/
|
|
523
|
+
async function sweepReservedKeys(core) {
|
|
524
|
+
const lease = path.join(core.dir, LEASE_NAMESPACE, CKPT_SCRATCH_LEASE);
|
|
525
|
+
const read = await errors.try(fs.readFile(lease));
|
|
526
|
+
if (read.error === undefined) {
|
|
527
|
+
const digest = parseCkptScratch(read.data);
|
|
528
|
+
if (digest !== null && (core.checkpointDigest === null || !bytesEqual(digest, core.checkpointDigest))) {
|
|
529
|
+
await core.store.delete(ckptDocKey(core.prefix, digest));
|
|
530
|
+
await core.store.delete(checkpointMdbKey(core.prefix, digest));
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
await fs.rm(path.join(core.dir, TEMP_NAMESPACE), { recursive: true, force: true });
|
|
534
|
+
await fs.rm(path.join(core.dir, LEASE_NAMESPACE), { recursive: true, force: true });
|
|
535
|
+
}
|
|
536
|
+
/** The disposable law says cache directories do not hoard corpses: every
|
|
537
|
+
* rotated `store-*` LMDB dir except the adopted one is dead — left by a
|
|
538
|
+
* crashed process or a prior rotation — and is swept at open. */
|
|
539
|
+
async function sweepRotations(core) {
|
|
540
|
+
const listed = await errors.try(fs.readdir(core.dir));
|
|
541
|
+
if (listed.error) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
for (const name of listed.data) {
|
|
545
|
+
if (!name.startsWith("store-") || name === core.storeName) {
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
await fs.rm(path.join(core.dir, name), { recursive: true, force: true });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Pending recovery: apply the resurrected batch. A batch rejected here
|
|
553
|
+
* was never acked; a born-no-op settles; anything else stays Pending
|
|
554
|
+
* so generation(chain) accounts for the unpublished apply.
|
|
555
|
+
*/
|
|
556
|
+
async function resolvePendingAtOpen(core) {
|
|
557
|
+
const chain = core.chain;
|
|
558
|
+
if (chain.tag === "settled") {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
// A slot the floor already covers is published (`Clear`), not re-judged (46).
|
|
562
|
+
if (foldPending(chainSum(core.chain), generationOf(core), null, chain.batch.bytes, belowFloor(core, chain.batch.braid, chain.batch.slot)).tag === "below-floor") {
|
|
563
|
+
await settle(core);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const decoded = errors.trySync(function decodePending() {
|
|
567
|
+
return decodeBatch(core.descriptor, chain.batch.bytes);
|
|
568
|
+
});
|
|
569
|
+
if (decoded.error) {
|
|
570
|
+
await settle(core);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const outcome = applyOps(core, decoded.data.ops);
|
|
574
|
+
if (outcome.tag === "rejected") {
|
|
575
|
+
await settle(core);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
// Store matches chainGeneration of the held Pending (sum+1) when the
|
|
579
|
+
// apply is in the db — just now, or already. A born-no-op leaves the
|
|
580
|
+
// store at the Settled generation, so the identity fails and we settle.
|
|
581
|
+
if (generationOf(core) !== chainGeneration(core.chain)) {
|
|
582
|
+
await settle(core);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
holdPending(core, chain.batch, decoded.data.ops, decoded.data.header.timestamp);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Pending resurrection through a cold re-pull: the store directory was
|
|
589
|
+
* unopenable (or discarded), so the sidecar's pending is resolved
|
|
590
|
+
* against a freshly caught-up store — a byte-equal published slot
|
|
591
|
+
* absorbs it (L10's idempotent replay), and everything else takes the
|
|
592
|
+
* one loss path's re-judgment at the tip. A slot the floor already
|
|
593
|
+
* covers is published (`Clear`), not re-judged (46).
|
|
594
|
+
*/
|
|
595
|
+
async function resolveColdPending(core, pending) {
|
|
596
|
+
const braid = braidOf(core.theory, pending.braid);
|
|
597
|
+
if (foldPending(chainSum(core.chain), generationOf(core), null, pending.bytes, belowFloor(core, braid, pending.slot))
|
|
598
|
+
.tag === "below-floor") {
|
|
599
|
+
await settle(core);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const decoded = errors.trySync(function decodePending() {
|
|
603
|
+
return decodeBatch(core.descriptor, pending.bytes);
|
|
604
|
+
});
|
|
605
|
+
if (decoded.error) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const slot = decoded.data.header.braidGen;
|
|
609
|
+
if (entriesOf(core).has(braid) && chainEntry(core, braid).g >= slot) {
|
|
610
|
+
const published = await core.store.get(logKey(core.prefix, braid, slot));
|
|
611
|
+
if (published !== null && bytesEqual(published.bytes, pending.bytes)) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const before = generationOf(core);
|
|
616
|
+
const outcome = applyOps(core, decoded.data.ops);
|
|
617
|
+
if (outcome.tag === "accepted" && outcome.value.generation > before) {
|
|
618
|
+
holdPending(core, { ...pending, braid }, decoded.data.ops, decoded.data.header.timestamp);
|
|
619
|
+
await readdressPending(core, decoded.data.ops, decoded.data.header.writer);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async function openCore(options) {
|
|
623
|
+
const descriptor = descriptorOf(options.theory);
|
|
624
|
+
const core = {
|
|
625
|
+
store: options.store,
|
|
626
|
+
prefix: options.prefix,
|
|
627
|
+
dir: path.resolve(options.dir),
|
|
628
|
+
theory: options.theory,
|
|
629
|
+
descriptor,
|
|
630
|
+
db: undefined,
|
|
631
|
+
chain: { tag: "settled", entries: zeroChain(descriptor) },
|
|
632
|
+
manifestEtag: null,
|
|
633
|
+
checkpoint: null,
|
|
634
|
+
checkpointDigest: null,
|
|
635
|
+
passes: 0,
|
|
636
|
+
closed: false,
|
|
637
|
+
storeName: freshStoreName(),
|
|
638
|
+
gate: Promise.resolve(),
|
|
639
|
+
wedged: new Map(),
|
|
640
|
+
provenance: { tag: "bootstrapped" }
|
|
641
|
+
};
|
|
642
|
+
await fs.mkdir(core.dir, { recursive: true });
|
|
643
|
+
const fetched = await core.store.get(manifestKey(core.prefix));
|
|
644
|
+
if (fetched === null) {
|
|
645
|
+
refuseManifestMissing("the store has no manifest");
|
|
646
|
+
}
|
|
647
|
+
await adoptManifest(core, fetched.bytes, fetched.etag);
|
|
648
|
+
const existing = await newestStoreDir(core.dir);
|
|
649
|
+
const sidecar = await readSidecar(descriptor.codec, sidecarPath(core));
|
|
650
|
+
let opened = false;
|
|
651
|
+
let coldPending = null;
|
|
652
|
+
if (sidecar.tag === "fault") {
|
|
653
|
+
throw wrapStore(sidecar.io, `read sidecar ${sidecarPath(core)}`);
|
|
654
|
+
}
|
|
655
|
+
// The codec-backed readSidecar judges every braid against the sealed
|
|
656
|
+
// handle — a foreign braid is a corrupt sidecar, and a corrupt
|
|
657
|
+
// sidecar is discarded cache (the disposable law), never adopted.
|
|
658
|
+
if (existing !== null && sidecar.tag === "read") {
|
|
659
|
+
core.storeName = existing;
|
|
660
|
+
const openedDb = await errors.try(SdkDb.open(storePath(core), options.theory));
|
|
661
|
+
if (openedDb.error === undefined) {
|
|
662
|
+
core.db = openedDb.data;
|
|
663
|
+
adoptChain(core, sidecar.chain);
|
|
664
|
+
core.provenance = { tag: "sidecar-resumed", floor: vectorOf(core) };
|
|
665
|
+
opened = true;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (!opened) {
|
|
669
|
+
if (sidecar.tag === "read" && sidecar.chain.tag === "pending") {
|
|
670
|
+
coldPending = sidecar.chain.batch;
|
|
671
|
+
}
|
|
672
|
+
await initializeStore(core);
|
|
673
|
+
}
|
|
674
|
+
if (opened) {
|
|
675
|
+
await resolvePendingAtOpen(core);
|
|
676
|
+
}
|
|
677
|
+
const outcome = await runPass(core, allBraids(core), "open");
|
|
678
|
+
if (outcome.tag === "reseed" || outcome.tag === "refused" || !wholenessHolds(core)) {
|
|
679
|
+
coldPending = core.chain.tag === "pending" ? core.chain.batch : coldPending;
|
|
680
|
+
await discardAndReopen(core);
|
|
681
|
+
}
|
|
682
|
+
if (coldPending !== null) {
|
|
683
|
+
await resolveColdPending(core, coldPending);
|
|
684
|
+
}
|
|
685
|
+
if (atCheckpointFloor(core)) {
|
|
686
|
+
auditCatalog(core.checkpoint, catalogDigestOf(core));
|
|
687
|
+
}
|
|
688
|
+
await sweepRotations(core);
|
|
689
|
+
await sweepReservedKeys(core);
|
|
690
|
+
return core;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* The steady-state discard route: a contested pending slot surrenders
|
|
694
|
+
* the directory. The pending batch rides in memory — the sidecar is
|
|
695
|
+
* not settled ahead of the re-judgment — and takes the one loss path
|
|
696
|
+
* at the fresh tip.
|
|
697
|
+
*/
|
|
698
|
+
async function repairDiscard(core) {
|
|
699
|
+
const coldPending = core.chain.tag === "pending" ? core.chain.batch : null;
|
|
700
|
+
await discardAndReopen(core);
|
|
701
|
+
if (coldPending !== null) {
|
|
702
|
+
await resolveColdPending(core, coldPending);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Re-addresses an applied-but-unpublished batch at the braid's current
|
|
707
|
+
* tip: fresh slot, `prev` citing the new predecessor, timestamp
|
|
708
|
+
* re-clamped — the ops are exactly the recorded ops the re-judgment
|
|
709
|
+
* just accepted. Publication itself retries on the next commit (60).
|
|
710
|
+
*/
|
|
711
|
+
async function readdressPending(core, ops, writerId) {
|
|
712
|
+
const first = ops[0];
|
|
713
|
+
if (first === undefined) {
|
|
714
|
+
await settle(core);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const relation = core.descriptor.relationByName.get(first.relation);
|
|
718
|
+
const raw = relation === undefined ? undefined : core.descriptor.braidOfRelation.get(relation.id);
|
|
719
|
+
if (raw === undefined) {
|
|
720
|
+
await settle(core);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const braid = braidOf(core.theory, raw);
|
|
724
|
+
const entry = chainEntry(core, braid);
|
|
725
|
+
const timestamp = maxBigint(BigInt(Date.now()), entry.ts);
|
|
726
|
+
const bytes = encodeBatch(core.descriptor, {
|
|
727
|
+
braid,
|
|
728
|
+
braidGen: generation(entry.g + 1n),
|
|
729
|
+
prev: entry.prev,
|
|
730
|
+
writer: writerId,
|
|
731
|
+
timestamp
|
|
732
|
+
}, ops);
|
|
733
|
+
holdPending(core, { braid, slot: generation(entry.g + 1n), bytes }, ops, timestamp);
|
|
734
|
+
await persistSidecar(core);
|
|
735
|
+
}
|
|
736
|
+
function maxBigint(a, b) {
|
|
737
|
+
return a > b ? a : b;
|
|
738
|
+
}
|
|
739
|
+
function vectorOf(core) {
|
|
740
|
+
const vector = new Map();
|
|
741
|
+
for (const [id, entry] of core.chain.entries) {
|
|
742
|
+
vector.set(id, entry.g);
|
|
743
|
+
}
|
|
744
|
+
return vector;
|
|
745
|
+
}
|
|
746
|
+
async function refreshPass(core, braid) {
|
|
747
|
+
const braids = braid === undefined ? allBraids(core) : [braidOf(core.theory, braid)];
|
|
748
|
+
const outcome = await runPass(core, braids, "steady");
|
|
749
|
+
if (outcome.tag === "reseed") {
|
|
750
|
+
await repairDiscard(core);
|
|
751
|
+
return { tag: "reseed", cause: outcome.cause };
|
|
752
|
+
}
|
|
753
|
+
return outcome;
|
|
754
|
+
}
|
|
755
|
+
/** waitFor is refresh with a verdict: the same pass, then the full
|
|
756
|
+
* Waited sum. A braid the target needs that is wedged below it returns
|
|
757
|
+
* Wedged — no refresh will ever reach the target — and a heartbeat
|
|
758
|
+
* refusal returns Refused. */
|
|
759
|
+
async function waitForVector(core, target) {
|
|
760
|
+
for (;;) {
|
|
761
|
+
disposed(core);
|
|
762
|
+
const have = vectorOf(core);
|
|
763
|
+
for (const [braid, wanted] of target) {
|
|
764
|
+
const cause = core.wedged.get(braid);
|
|
765
|
+
const at = have.get(braid);
|
|
766
|
+
if (cause !== undefined && at !== undefined && at < wanted) {
|
|
767
|
+
return { tag: "wedged", braid, cause };
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (Vector.from(have).dominates(Vector.from(target))) {
|
|
771
|
+
return { tag: "reached", vector: have };
|
|
772
|
+
}
|
|
773
|
+
const outcome = await withGate(core, async function waitPass() {
|
|
774
|
+
disposed(core);
|
|
775
|
+
return refreshPass(core);
|
|
776
|
+
});
|
|
777
|
+
if (outcome.tag === "refused") {
|
|
778
|
+
return { tag: "refused", detail: outcome.detail };
|
|
779
|
+
}
|
|
780
|
+
await new Promise(function later(resolve) {
|
|
781
|
+
setTimeout(resolve, WAIT_FOR_POLL_MS);
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
async function openReplica(options) {
|
|
786
|
+
const core = await openCore(options);
|
|
787
|
+
const replica = {
|
|
788
|
+
get db() {
|
|
789
|
+
disposed(core);
|
|
790
|
+
return core.db;
|
|
791
|
+
},
|
|
792
|
+
get vector() {
|
|
793
|
+
return vectorOf(core);
|
|
794
|
+
},
|
|
795
|
+
async refresh(braid) {
|
|
796
|
+
return withGate(core, async function refreshBody() {
|
|
797
|
+
disposed(core);
|
|
798
|
+
await refreshPass(core, braid);
|
|
799
|
+
return vectorOf(core);
|
|
800
|
+
});
|
|
801
|
+
},
|
|
802
|
+
async waitFor(vector) {
|
|
803
|
+
const target = new Map();
|
|
804
|
+
for (const [braid, wanted] of vector) {
|
|
805
|
+
target.set(braidOf(core.theory, braid), wanted);
|
|
806
|
+
}
|
|
807
|
+
return waitForVector(core, target);
|
|
808
|
+
},
|
|
809
|
+
async [Symbol.asyncDispose]() {
|
|
810
|
+
await withGate(core, async function disposeBody() {
|
|
811
|
+
core.closed = true;
|
|
812
|
+
await persistSidecar(core);
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
cores.set(replica, core);
|
|
817
|
+
return replica;
|
|
818
|
+
}
|
|
819
|
+
export { applyOps, belowFloor, chainEntry, clearPending, coreOf, discardAndReopen, entriesOf, foldPending, generationOf, holdPending, maxBigint, openReplica, pendingOf, persistSidecar, readdressPending, withGate, ZERO_HASH };
|
|
820
|
+
//# sourceMappingURL=replica.js.map
|