@peerbit/shared-log 13.2.24 → 13.2.26
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/src/checked-prune.d.ts +12 -0
- package/dist/src/checked-prune.d.ts.map +1 -1
- package/dist/src/checked-prune.js +33 -0
- package/dist/src/checked-prune.js.map +1 -1
- package/dist/src/coordinate-persistence.d.ts +211 -0
- package/dist/src/coordinate-persistence.d.ts.map +1 -0
- package/dist/src/coordinate-persistence.js +1133 -0
- package/dist/src/coordinate-persistence.js.map +1 -0
- package/dist/src/errors.d.ts +1 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +19 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/index.d.ts +127 -133
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1400 -4269
- package/dist/src/index.js.map +1 -1
- package/dist/src/instance-lifecycle.d.ts +117 -0
- package/dist/src/instance-lifecycle.d.ts.map +1 -0
- package/dist/src/instance-lifecycle.js +254 -0
- package/dist/src/instance-lifecycle.js.map +1 -0
- package/dist/src/join-warmup.d.ts +86 -0
- package/dist/src/join-warmup.d.ts.map +1 -0
- package/dist/src/join-warmup.js +351 -0
- package/dist/src/join-warmup.js.map +1 -0
- package/dist/src/native-write-through-block-store.d.ts +123 -0
- package/dist/src/native-write-through-block-store.d.ts.map +1 -0
- package/dist/src/native-write-through-block-store.js +989 -0
- package/dist/src/native-write-through-block-store.js.map +1 -0
- package/dist/src/peer-session.d.ts +117 -0
- package/dist/src/peer-session.d.ts.map +1 -0
- package/dist/src/peer-session.js +259 -0
- package/dist/src/peer-session.js.map +1 -0
- package/dist/src/replication-announcement.d.ts +116 -0
- package/dist/src/replication-announcement.d.ts.map +1 -0
- package/dist/src/replication-announcement.js +515 -0
- package/dist/src/replication-announcement.js.map +1 -0
- package/dist/src/replicator-liveness.d.ts +59 -0
- package/dist/src/replicator-liveness.d.ts.map +1 -0
- package/dist/src/replicator-liveness.js +304 -0
- package/dist/src/replicator-liveness.js.map +1 -0
- package/dist/src/sync/dispatch-lifecycle.d.ts +48 -0
- package/dist/src/sync/dispatch-lifecycle.d.ts.map +1 -0
- package/dist/src/sync/dispatch-lifecycle.js +141 -0
- package/dist/src/sync/dispatch-lifecycle.js.map +1 -0
- package/dist/src/sync/factory.d.ts +29 -0
- package/dist/src/sync/factory.d.ts.map +1 -0
- package/dist/src/sync/factory.js +87 -0
- package/dist/src/sync/factory.js.map +1 -0
- package/dist/src/sync/pending-sync-store.d.ts +105 -0
- package/dist/src/sync/pending-sync-store.d.ts.map +1 -0
- package/dist/src/sync/pending-sync-store.js +877 -0
- package/dist/src/sync/pending-sync-store.js.map +1 -0
- package/dist/src/sync/rateless-iblt.d.ts +4 -1
- package/dist/src/sync/rateless-iblt.d.ts.map +1 -1
- package/dist/src/sync/rateless-iblt.js +102 -106
- package/dist/src/sync/rateless-iblt.js.map +1 -1
- package/dist/src/sync/simple.d.ts +10 -41
- package/dist/src/sync/simple.d.ts.map +1 -1
- package/dist/src/sync/simple.js +259 -952
- package/dist/src/sync/simple.js.map +1 -1
- package/dist/src/sync/sync-peer-state.d.ts +16 -0
- package/dist/src/sync/sync-peer-state.d.ts.map +1 -0
- package/dist/src/sync/sync-peer-state.js +82 -0
- package/dist/src/sync/sync-peer-state.js.map +1 -0
- package/package.json +12 -12
- package/src/checked-prune.ts +35 -0
- package/src/coordinate-persistence.ts +1796 -0
- package/src/errors.ts +21 -0
- package/src/index.ts +4014 -7858
- package/src/instance-lifecycle.ts +351 -0
- package/src/join-warmup.ts +507 -0
- package/src/native-write-through-block-store.ts +1170 -0
- package/src/peer-session.ts +336 -0
- package/src/replication-announcement.ts +744 -0
- package/src/replicator-liveness.ts +439 -0
- package/src/sync/dispatch-lifecycle.ts +230 -0
- package/src/sync/factory.ts +154 -0
- package/src/sync/pending-sync-store.ts +1085 -0
- package/src/sync/rateless-iblt.ts +122 -132
- package/src/sync/simple.ts +334 -1144
- package/src/sync/sync-peer-state.ts +107 -0
|
@@ -0,0 +1,1170 @@
|
|
|
1
|
+
import type { AnyBlockStore } from "@peerbit/blocks";
|
|
2
|
+
import { logger as loggerFn } from "@peerbit/logger";
|
|
3
|
+
import type { NativePeerbitBackbone } from "@peerbit/native-backbone";
|
|
4
|
+
import { NativeDurableCommitError } from "./errors.js";
|
|
5
|
+
|
|
6
|
+
const warn = loggerFn("peerbit:shared-log").newScope("warn");
|
|
7
|
+
|
|
8
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
9
|
+
|
|
10
|
+
const isPromiseLike = <T>(value: MaybePromise<T>): value is Promise<T> =>
|
|
11
|
+
!!value && typeof (value as Promise<T>).then === "function";
|
|
12
|
+
|
|
13
|
+
/** The native backbone's in-wasm-memory block store. */
|
|
14
|
+
type NativeBackboneBlocks = NativePeerbitBackbone["blocks"];
|
|
15
|
+
|
|
16
|
+
type NativeCommitOwnershipToken = {
|
|
17
|
+
id: number;
|
|
18
|
+
rows: Map<
|
|
19
|
+
string,
|
|
20
|
+
{
|
|
21
|
+
generation: number;
|
|
22
|
+
durableExistedBefore?: boolean;
|
|
23
|
+
shared: boolean;
|
|
24
|
+
}
|
|
25
|
+
>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Write-through block store bridging the native backbone's in-wasm-memory block
|
|
30
|
+
* store to a durable per-program {@link AnyBlockStore}.
|
|
31
|
+
*
|
|
32
|
+
* WHY: when the native backbone is active the log's entry blocks live only in
|
|
33
|
+
* the native wasm block map (`NativeBackboneBlockStore.persisted() === false`).
|
|
34
|
+
* On a restart that map is empty, so the native graph cannot reload heads the
|
|
35
|
+
* durable heads index still lists ("Failed to load entry from head"). This
|
|
36
|
+
* wrapper mirrors every write into the same durable `blocks` sublevel the
|
|
37
|
+
* non-native path uses. On a native miss, reads fall through to durable storage
|
|
38
|
+
* and lazily repopulate the wasm map so the native graph can walk the DAG again.
|
|
39
|
+
*
|
|
40
|
+
* The native store stays the authoritative hot store the native graph reads
|
|
41
|
+
* from: reads hit native first and only fall back to durable (repopulating
|
|
42
|
+
* native on a hit so subsequent native-graph reads succeed).
|
|
43
|
+
*
|
|
44
|
+
* METHOD SURFACE (see #1006): `RemoteBlocks` and the log feature-detect the
|
|
45
|
+
* optional batch methods (`putMany`/`putKnown`/`putKnownMany`/
|
|
46
|
+
* `putKnownManyColumns`/`rmMany`). To keep the receive-fusion / columnar fast
|
|
47
|
+
* paths engaged this wrapper preserves the native store's optional write
|
|
48
|
+
* methods — including `putKnownManyColumns`, which `AnyBlockStore` does not have
|
|
49
|
+
* — and delegates each. It adds only local durability/trim coordination hooks
|
|
50
|
+
* consumed by `RemoteBlocks` and the log; protocol/native-handle capabilities
|
|
51
|
+
* such as `getBlockResponsePayload`/`getNativeLogBlockStoreHandle` stay absent,
|
|
52
|
+
* so their optional-chained probes keep the existing fallback behavior.
|
|
53
|
+
*/
|
|
54
|
+
export class NativeBackboneWriteThroughBlockStore {
|
|
55
|
+
constructor(
|
|
56
|
+
private readonly native: NativeBackboneBlocks,
|
|
57
|
+
private readonly durable: AnyBlockStore,
|
|
58
|
+
) {}
|
|
59
|
+
|
|
60
|
+
// A durable mirror write that cannot be awaited at its call site (the
|
|
61
|
+
// columnar putKnownManyColumns fast path must return a synchronous string[]
|
|
62
|
+
// because RemoteBlocks.putKnownManyColumns treats the result as sync) is
|
|
63
|
+
// tracked here instead of being silently `void`ed. Its rejection is stored
|
|
64
|
+
// and re-thrown on the next awaited wrapper method (and on stop()), so a
|
|
65
|
+
// failed durable write (IO/disk-full) surfaces as an error rather than
|
|
66
|
+
// vanishing and leaving the block out of durable while native/log report
|
|
67
|
+
// success. `.catch` also prevents unhandled-rejection noise.
|
|
68
|
+
private readonly pendingDurableWrites = new Set<Promise<unknown>>();
|
|
69
|
+
private nativeDurableCommitFailure?: NativeDurableCommitError;
|
|
70
|
+
private stopCompleted = false;
|
|
71
|
+
private readonly nativeDeleteTombstones = new Map<string, number>();
|
|
72
|
+
private nativeDeleteEpoch = 0;
|
|
73
|
+
private readonly nativeBlockWriteGenerations = new Map<string, number>();
|
|
74
|
+
private readonly pendingNativeDeleteCleanup = new Map<string, number>();
|
|
75
|
+
private readonly stagedNativeDeleteCleanups = new Map<
|
|
76
|
+
number,
|
|
77
|
+
Map<string, number>
|
|
78
|
+
>();
|
|
79
|
+
private nextNativeDeleteCleanupToken = 0;
|
|
80
|
+
private nativeDeleteCleanupRunning: Promise<void> | undefined;
|
|
81
|
+
private nextNativeCommitOwnershipToken = 0;
|
|
82
|
+
private readonly nativeCommitOwnerships = new Map<
|
|
83
|
+
number,
|
|
84
|
+
NativeCommitOwnershipToken
|
|
85
|
+
>();
|
|
86
|
+
private readonly nativeCommitOwnershipsByCid = new Map<string, Set<number>>();
|
|
87
|
+
|
|
88
|
+
getNativeDurableCommitFailure(): NativeDurableCommitError | undefined {
|
|
89
|
+
return this.nativeDurableCommitFailure;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private recordNativeDurableCommitFailure(
|
|
93
|
+
cause: unknown,
|
|
94
|
+
options?: {
|
|
95
|
+
committedCids?: Iterable<string>;
|
|
96
|
+
failedCids?: Iterable<string>;
|
|
97
|
+
},
|
|
98
|
+
): NativeDurableCommitError {
|
|
99
|
+
if (this.nativeDurableCommitFailure) {
|
|
100
|
+
this.nativeDurableCommitFailure.addCommitContext(options);
|
|
101
|
+
return this.nativeDurableCommitFailure;
|
|
102
|
+
}
|
|
103
|
+
this.nativeDurableCommitFailure =
|
|
104
|
+
cause instanceof NativeDurableCommitError
|
|
105
|
+
? cause
|
|
106
|
+
: new NativeDurableCommitError(cause, options);
|
|
107
|
+
if (cause instanceof NativeDurableCommitError) {
|
|
108
|
+
cause.addCommitContext(options);
|
|
109
|
+
}
|
|
110
|
+
return this.nativeDurableCommitFailure;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private throwIfNativeDurableCommitFailed(): void {
|
|
114
|
+
if (this.nativeDurableCommitFailure) {
|
|
115
|
+
throw this.nativeDurableCommitFailure;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
throwIfDurableWritesFailed(): void {
|
|
120
|
+
this.throwIfNativeDurableCommitFailed();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async commitDurableMutation<T>(
|
|
124
|
+
operation: () => MaybePromise<T>,
|
|
125
|
+
committedCids: Iterable<string>,
|
|
126
|
+
failedCids?: Iterable<string>,
|
|
127
|
+
): Promise<T> {
|
|
128
|
+
this.throwIfNativeDurableCommitFailed();
|
|
129
|
+
const committedCidList = [...committedCids];
|
|
130
|
+
const failedCidList = failedCids ? [...failedCids] : committedCidList;
|
|
131
|
+
let result: T;
|
|
132
|
+
const operationResult = Promise.resolve().then(operation);
|
|
133
|
+
this.trackAwaitedDurable(operationResult);
|
|
134
|
+
try {
|
|
135
|
+
result = await operationResult;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw this.recordNativeDurableCommitFailure(error, {
|
|
138
|
+
committedCids: committedCidList,
|
|
139
|
+
failedCids: failedCidList,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
// A different concurrent native mutation may have poisoned the wrapper
|
|
143
|
+
// while this durable call was in flight. Include this operation among the
|
|
144
|
+
// native-applied facts, but not among durable calls that actually failed.
|
|
145
|
+
if (this.nativeDurableCommitFailure) {
|
|
146
|
+
this.nativeDurableCommitFailure.addCommitContext({
|
|
147
|
+
committedCids: committedCidList,
|
|
148
|
+
failedCids: [],
|
|
149
|
+
});
|
|
150
|
+
throw this.nativeDurableCommitFailure;
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private beginNativeDelete(cids: string[]): void {
|
|
156
|
+
this.nativeDeleteEpoch++;
|
|
157
|
+
for (const cid of cids) {
|
|
158
|
+
this.nativeDeleteTombstones.set(
|
|
159
|
+
cid,
|
|
160
|
+
(this.nativeDeleteTombstones.get(cid) ?? 0) + 1,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private endNativeDelete(cids: string[]): void {
|
|
166
|
+
for (const cid of cids) {
|
|
167
|
+
const remaining = (this.nativeDeleteTombstones.get(cid) ?? 1) - 1;
|
|
168
|
+
if (remaining <= 0) {
|
|
169
|
+
this.nativeDeleteTombstones.delete(cid);
|
|
170
|
+
} else {
|
|
171
|
+
this.nativeDeleteTombstones.set(cid, remaining);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private isNativeDeletePending(cid: string): boolean {
|
|
177
|
+
return this.nativeDeleteTombstones.has(cid);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// A CID can be legitimately re-added after a native trim (content addressing
|
|
181
|
+
// makes the bytes identical, but its liveness is new). Cancel any queued trim
|
|
182
|
+
// for that CID and advance its generation before the write is exposed. An
|
|
183
|
+
// already-running cleanup uses the generation/pending map to avoid deleting
|
|
184
|
+
// the new native value; synchronous columnar writes also chain their durable
|
|
185
|
+
// mirror behind that cleanup below.
|
|
186
|
+
private noteNativeBlockWrite(cids: string[]): Map<string, number> {
|
|
187
|
+
const generations = new Map<string, number>();
|
|
188
|
+
for (const cid of new Set(cids)) {
|
|
189
|
+
const generation = (this.nativeBlockWriteGenerations.get(cid) ?? 0) + 1;
|
|
190
|
+
this.nativeBlockWriteGenerations.set(cid, generation);
|
|
191
|
+
generations.set(cid, generation);
|
|
192
|
+
if (this.pendingNativeDeleteCleanup.delete(cid)) {
|
|
193
|
+
this.endNativeDelete([cid]);
|
|
194
|
+
}
|
|
195
|
+
for (const staged of this.stagedNativeDeleteCleanups.values()) {
|
|
196
|
+
if (staged.delete(cid)) {
|
|
197
|
+
this.endNativeDelete([cid]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return generations;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private beginNativeCommitOwnership(
|
|
205
|
+
generations: Map<string, number>,
|
|
206
|
+
): NativeCommitOwnershipToken | undefined {
|
|
207
|
+
if (generations.size === 0) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
const token: NativeCommitOwnershipToken = {
|
|
211
|
+
id: ++this.nextNativeCommitOwnershipToken,
|
|
212
|
+
rows: new Map(),
|
|
213
|
+
};
|
|
214
|
+
for (const [cid, generation] of generations) {
|
|
215
|
+
const owners = this.nativeCommitOwnershipsByCid.get(cid) ?? new Set();
|
|
216
|
+
const shared = owners.size > 0;
|
|
217
|
+
for (const ownerId of owners) {
|
|
218
|
+
const owner = this.nativeCommitOwnerships.get(ownerId);
|
|
219
|
+
const row = owner?.rows.get(cid);
|
|
220
|
+
if (row) row.shared = true;
|
|
221
|
+
}
|
|
222
|
+
owners.add(token.id);
|
|
223
|
+
this.nativeCommitOwnershipsByCid.set(cid, owners);
|
|
224
|
+
token.rows.set(cid, { generation, shared });
|
|
225
|
+
}
|
|
226
|
+
this.nativeCommitOwnerships.set(token.id, token);
|
|
227
|
+
return token;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private releaseNativeCommitOwnership(token: unknown): void {
|
|
231
|
+
if (
|
|
232
|
+
!token ||
|
|
233
|
+
typeof token !== "object" ||
|
|
234
|
+
typeof (token as NativeCommitOwnershipToken).id !== "number"
|
|
235
|
+
) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const owned = this.nativeCommitOwnerships.get(
|
|
239
|
+
(token as NativeCommitOwnershipToken).id,
|
|
240
|
+
);
|
|
241
|
+
if (owned !== token) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
this.nativeCommitOwnerships.delete(owned.id);
|
|
245
|
+
for (const cid of owned.rows.keys()) {
|
|
246
|
+
const owners = this.nativeCommitOwnershipsByCid.get(cid);
|
|
247
|
+
owners?.delete(owned.id);
|
|
248
|
+
if (owners?.size === 0) {
|
|
249
|
+
this.nativeCommitOwnershipsByCid.delete(cid);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
acknowledgeNativeCommitOwnership(token: unknown): void {
|
|
255
|
+
this.releaseNativeCommitOwnership(token);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private enqueueNativeDeleteCleanup(cids: string[]): void {
|
|
259
|
+
for (const cid of new Set(cids)) {
|
|
260
|
+
if (!this.pendingNativeDeleteCleanup.has(cid)) {
|
|
261
|
+
this.pendingNativeDeleteCleanup.set(
|
|
262
|
+
cid,
|
|
263
|
+
this.nativeBlockWriteGenerations.get(cid) ?? 0,
|
|
264
|
+
);
|
|
265
|
+
this.beginNativeDelete([cid]);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private releaseStagedNativeDeleteCleanup(token: number): boolean {
|
|
271
|
+
const staged = this.stagedNativeDeleteCleanups.get(token);
|
|
272
|
+
if (!staged) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
this.stagedNativeDeleteCleanups.delete(token);
|
|
276
|
+
for (const [cid] of staged) {
|
|
277
|
+
this.endNativeDelete([cid]);
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private discardStagedNativeDeleteCleanups(): void {
|
|
283
|
+
for (const token of [...this.stagedNativeDeleteCleanups.keys()]) {
|
|
284
|
+
this.releaseStagedNativeDeleteCleanup(token);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Native commit callbacks call this immediately after the native transaction
|
|
289
|
+
// returns, before awaiting the new block's durable mirror. This stages read
|
|
290
|
+
// tombstones only. Durable deletion is promoted later by the exact EntryIndex
|
|
291
|
+
// consume token, so an unacknowledged/failed append cannot delete the old
|
|
292
|
+
// durable head that the lower log still publishes.
|
|
293
|
+
beginNativeDeleteCleanup(cids: string[]): number | undefined {
|
|
294
|
+
this.throwIfNativeDurableCommitFailed();
|
|
295
|
+
const uniqueCids = [...new Set(cids)];
|
|
296
|
+
if (uniqueCids.length === 0) {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
const token = ++this.nextNativeDeleteCleanupToken;
|
|
300
|
+
const staged = new Map(
|
|
301
|
+
uniqueCids.map((cid) => [
|
|
302
|
+
cid,
|
|
303
|
+
this.nativeBlockWriteGenerations.get(cid) ?? 0,
|
|
304
|
+
]),
|
|
305
|
+
);
|
|
306
|
+
this.stagedNativeDeleteCleanups.set(token, staged);
|
|
307
|
+
this.beginNativeDelete(uniqueCids);
|
|
308
|
+
return token;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
cancelNativeDeleteCleanup(cleanupToken: unknown): void {
|
|
312
|
+
if (typeof cleanupToken === "number") {
|
|
313
|
+
this.releaseStagedNativeDeleteCleanup(cleanupToken);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private async waitForNativeDeleteCleanup(): Promise<void> {
|
|
318
|
+
while (this.nativeDeleteCleanupRunning) {
|
|
319
|
+
await this.nativeDeleteCleanupRunning;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private async waitForTrackedDurableWrites(): Promise<void> {
|
|
324
|
+
while (this.pendingDurableWrites.size > 0) {
|
|
325
|
+
await Promise.allSettled([...this.pendingDurableWrites]);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private async retryNativeDeleteCleanup(options?: {
|
|
330
|
+
allowPoisoned?: boolean;
|
|
331
|
+
throwOnFailure?: boolean;
|
|
332
|
+
}): Promise<void> {
|
|
333
|
+
await this.waitForNativeDeleteCleanup();
|
|
334
|
+
if (this.nativeDurableCommitFailure && !options?.allowPoisoned) {
|
|
335
|
+
throw this.nativeDurableCommitFailure;
|
|
336
|
+
}
|
|
337
|
+
// A synchronous columnar write may already have scheduled its durable
|
|
338
|
+
// mirror. Let it settle before deleting queued CIDs, but leave any recorded
|
|
339
|
+
// error for drainDurable() to surface to its owning operation/stop.
|
|
340
|
+
await this.waitForTrackedDurableWrites();
|
|
341
|
+
await this.waitForNativeDeleteCleanup();
|
|
342
|
+
// A tracked mirror or the cleanup we just waited for may have poisoned the
|
|
343
|
+
// wrapper. Do not begin another durable mutation on the ordinary path after
|
|
344
|
+
// that asynchronous boundary. stop() alone opts into one cleanup retry so a
|
|
345
|
+
// transient delete failure can still release its tombstones and resources.
|
|
346
|
+
if (this.nativeDurableCommitFailure && !options?.allowPoisoned) {
|
|
347
|
+
throw this.nativeDurableCommitFailure;
|
|
348
|
+
}
|
|
349
|
+
if (this.pendingNativeDeleteCleanup.size === 0) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const cleanupEntries = [...this.pendingNativeDeleteCleanup].filter(
|
|
353
|
+
([cid, generation]) =>
|
|
354
|
+
(this.nativeBlockWriteGenerations.get(cid) ?? 0) === generation,
|
|
355
|
+
);
|
|
356
|
+
if (cleanupEntries.length === 0) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const cids = cleanupEntries.map(([cid]) => cid);
|
|
360
|
+
let cleanupFailure: unknown;
|
|
361
|
+
const running = (async () => {
|
|
362
|
+
let durableRemoved = false;
|
|
363
|
+
let nativeRemoved = false;
|
|
364
|
+
try {
|
|
365
|
+
await this.durable.rmMany(cids);
|
|
366
|
+
durableRemoved = true;
|
|
367
|
+
} catch (error) {
|
|
368
|
+
cleanupFailure = error;
|
|
369
|
+
// The new entry blocks are already durable at this point and the native
|
|
370
|
+
// transaction has selected the new graph/index state. Old content-addressed
|
|
371
|
+
// blocks are therefore harmless unreachable orphans. Keep this cleanup as
|
|
372
|
+
// retryable debt instead of poisoning/rolling back a fully durable append;
|
|
373
|
+
// a partial rmMany is safe for the same reason.
|
|
374
|
+
warn(
|
|
375
|
+
`Failed durable native-trim cleanup; retaining retry debt: ${String(error)}`,
|
|
376
|
+
);
|
|
377
|
+
} finally {
|
|
378
|
+
// A read that began in the native-transaction -> cleanup-hook gap may
|
|
379
|
+
// have repopulated native. Always repeat the native removal, even when
|
|
380
|
+
// durable rm failed. Exclude CIDs re-added while durable IO was pending;
|
|
381
|
+
// their generation change cancelled the queued delete.
|
|
382
|
+
const stillDeleted = cids.filter((cid) =>
|
|
383
|
+
this.pendingNativeDeleteCleanup.has(cid),
|
|
384
|
+
);
|
|
385
|
+
try {
|
|
386
|
+
if (stillDeleted.length > 0) {
|
|
387
|
+
await this.native.rmMany(stillDeleted);
|
|
388
|
+
}
|
|
389
|
+
nativeRemoved = true;
|
|
390
|
+
} catch (error) {
|
|
391
|
+
cleanupFailure ??= error;
|
|
392
|
+
warn(
|
|
393
|
+
`Failed to repeat native-trim hot block removal: ${String(error)}`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (durableRemoved && nativeRemoved) {
|
|
398
|
+
for (const [cid, generation] of cleanupEntries) {
|
|
399
|
+
if (this.pendingNativeDeleteCleanup.get(cid) === generation) {
|
|
400
|
+
this.pendingNativeDeleteCleanup.delete(cid);
|
|
401
|
+
this.nativeBlockWriteGenerations.delete(cid);
|
|
402
|
+
this.endNativeDelete([cid]);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
})();
|
|
407
|
+
this.nativeDeleteCleanupRunning = running;
|
|
408
|
+
try {
|
|
409
|
+
await running;
|
|
410
|
+
} finally {
|
|
411
|
+
if (this.nativeDeleteCleanupRunning === running) {
|
|
412
|
+
this.nativeDeleteCleanupRunning = undefined;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (options?.throwOnFailure && cleanupFailure !== undefined) {
|
|
416
|
+
throw cleanupFailure;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private trackDurable(result: unknown, cids: string[]): void {
|
|
421
|
+
// The durable store may answer synchronously (an in-memory or already
|
|
422
|
+
// resolved store); only a real pending promise needs tracking. A sync
|
|
423
|
+
// success is already durable; a sync throw would have propagated already.
|
|
424
|
+
if (!isPromiseLike(result)) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const tracked = Promise.resolve(result).then(
|
|
428
|
+
() => {
|
|
429
|
+
this.pendingDurableWrites.delete(tracked);
|
|
430
|
+
},
|
|
431
|
+
(error) => {
|
|
432
|
+
this.pendingDurableWrites.delete(tracked);
|
|
433
|
+
this.recordNativeDurableCommitFailure(error, {
|
|
434
|
+
committedCids: cids,
|
|
435
|
+
failedCids: cids,
|
|
436
|
+
});
|
|
437
|
+
},
|
|
438
|
+
);
|
|
439
|
+
this.pendingDurableWrites.add(tracked);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Awaited mirror writes surface their own rejection to the append that
|
|
443
|
+
// created them. Track a non-rejecting settlement companion as well so stop()
|
|
444
|
+
// and later wrapper operations cannot close/use durable storage while that
|
|
445
|
+
// append barrier is still in flight.
|
|
446
|
+
private trackAwaitedDurable(result: Promise<unknown>): void {
|
|
447
|
+
const tracked = result.then(
|
|
448
|
+
() => {
|
|
449
|
+
this.pendingDurableWrites.delete(tracked);
|
|
450
|
+
},
|
|
451
|
+
() => {
|
|
452
|
+
this.pendingDurableWrites.delete(tracked);
|
|
453
|
+
},
|
|
454
|
+
);
|
|
455
|
+
this.pendingDurableWrites.add(tracked);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Wait for every tracked (sync-path) durable write to settle, then surface
|
|
459
|
+
// the first failure. Awaited methods call this so a prior columnar durable
|
|
460
|
+
// failure propagates as back-pressure to the next caller.
|
|
461
|
+
private async drainDurable(): Promise<void> {
|
|
462
|
+
await this.waitForTrackedDurableWrites();
|
|
463
|
+
this.throwIfNativeDurableCommitFailed();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// --- lifecycle -------------------------------------------------------
|
|
467
|
+
// The native store's lifecycle hooks are no-ops; only the durable store
|
|
468
|
+
// needs starting/stopping. The wasm map is NOT eagerly rehydrated from disk:
|
|
469
|
+
// entry blocks are pulled back lazily on demand through the read fallback in
|
|
470
|
+
// getMany()/get() (durable hit -> repopulate the wasm map), which is what the
|
|
471
|
+
// log's DAG walk (EntryIndex.resolveMany -> store.getMany) exercises. Keeping
|
|
472
|
+
// the wasm map cold on open is required by the strict-native resident
|
|
473
|
+
// coordinate-state optimization: a reopened non-replicating native node must
|
|
474
|
+
// report hasBlock(head) === false and answer a same-signer append from the
|
|
475
|
+
// persisted coordinate + signer facts without resolving the entry block.
|
|
476
|
+
async start(): Promise<void> {
|
|
477
|
+
await this.durable.start();
|
|
478
|
+
this.stopCompleted = false;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async stop(): Promise<void> {
|
|
482
|
+
if (this.stopCompleted) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
// Surface any tracked (sync-path) durable write failure and ensure all
|
|
486
|
+
// mirror writes have settled before the durable store is torn down. Closing
|
|
487
|
+
// the durable store is unconditional: a prior columnar mirror failure must
|
|
488
|
+
// not leak the store lifecycle resource.
|
|
489
|
+
let firstError: unknown;
|
|
490
|
+
let shutdownError: unknown;
|
|
491
|
+
try {
|
|
492
|
+
await this.drainDurable();
|
|
493
|
+
} catch (error) {
|
|
494
|
+
firstError = error;
|
|
495
|
+
}
|
|
496
|
+
// Tokens not consumed by EntryIndex belong to native prepares that never
|
|
497
|
+
// published their lower-log trim. Release their read tombstones, but never
|
|
498
|
+
// promote them to durable deletion during shutdown.
|
|
499
|
+
this.discardStagedNativeDeleteCleanups();
|
|
500
|
+
try {
|
|
501
|
+
await this.retryNativeDeleteCleanup({ allowPoisoned: true });
|
|
502
|
+
} catch (error) {
|
|
503
|
+
firstError ??= error;
|
|
504
|
+
shutdownError ??= error;
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
await this.durable.stop();
|
|
508
|
+
} catch (error) {
|
|
509
|
+
firstError ??= error;
|
|
510
|
+
shutdownError ??= error;
|
|
511
|
+
}
|
|
512
|
+
if (shutdownError === undefined) {
|
|
513
|
+
// A durable poison belongs to the generation being closed. Report it once
|
|
514
|
+
// so the owning terminal call observes the failed append, but remember that
|
|
515
|
+
// every mandatory shutdown stage completed. Conservative content-addressed
|
|
516
|
+
// trim debt may remain in this retired wrapper after best-effort cleanup; a
|
|
517
|
+
// fresh generation gets a new wrapper and must not be wedged by that debt.
|
|
518
|
+
// The exact terminal retry may therefore finish parent bookkeeping without
|
|
519
|
+
// rethrowing the same latched poison forever.
|
|
520
|
+
this.stopCompleted = true;
|
|
521
|
+
}
|
|
522
|
+
if (firstError !== undefined) {
|
|
523
|
+
throw firstError;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
status() {
|
|
528
|
+
return this.durable.status();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
waitFor(): Promise<string[]> {
|
|
532
|
+
return Promise.resolve([]);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Native commit APIs must keep their block-store callback synchronous. The
|
|
536
|
+
// lower log calls this barrier after that callback reports committed blocks
|
|
537
|
+
// and before publishing index/head facts.
|
|
538
|
+
waitForDurableWrites(): Promise<void> {
|
|
539
|
+
return this.drainDurable();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Mirror a single already-committed block (present in the wasm map) to the
|
|
543
|
+
// durable store ONLY. Used by the native commit-only append fast path: the
|
|
544
|
+
// native prepare commits the entry block into the wasm map and returns no raw
|
|
545
|
+
// bytes, and the strict-native resident-coordinate path deliberately does NOT
|
|
546
|
+
// route the block through the log's finishBlocks/putKnown* (that would disturb
|
|
547
|
+
// the commit-only append path the RCS optimization depends on). Instead the
|
|
548
|
+
// caller reads the committed bytes back and calls this so the block lands in
|
|
549
|
+
// durable directly. The caller awaits this method before acknowledging its
|
|
550
|
+
// append, so a failed write rejects the append that produced the block.
|
|
551
|
+
async mirrorToDurable(
|
|
552
|
+
cid: string,
|
|
553
|
+
bytes: Uint8Array,
|
|
554
|
+
options?: { nativeTrimmed?: boolean },
|
|
555
|
+
): Promise<unknown> {
|
|
556
|
+
this.throwIfNativeDurableCommitFailed();
|
|
557
|
+
// The native commit happened before this call. Mark the CID live before
|
|
558
|
+
// awaiting anything so a concurrent retry of an older trim cannot remove
|
|
559
|
+
// the newly committed hot block.
|
|
560
|
+
let ownership: NativeCommitOwnershipToken | undefined;
|
|
561
|
+
if (options?.nativeTrimmed !== true) {
|
|
562
|
+
ownership = this.beginNativeCommitOwnership(
|
|
563
|
+
this.noteNativeBlockWrite([cid]),
|
|
564
|
+
);
|
|
565
|
+
await this.waitForNativeDeleteCleanup();
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
await this.drainDurable();
|
|
569
|
+
if (ownership) {
|
|
570
|
+
const existed = await this.durable.hasMany([...ownership.rows.keys()]);
|
|
571
|
+
let index = 0;
|
|
572
|
+
for (const row of ownership.rows.values()) {
|
|
573
|
+
row.durableExistedBefore = existed[index++] === true;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const result = this.commitDurableMutation(
|
|
577
|
+
() => this.durable.putKnown(cid, bytes),
|
|
578
|
+
[cid],
|
|
579
|
+
);
|
|
580
|
+
await result;
|
|
581
|
+
await this.retryNativeDeleteCleanup();
|
|
582
|
+
this.throwIfNativeDurableCommitFailed();
|
|
583
|
+
return ownership;
|
|
584
|
+
} catch (error) {
|
|
585
|
+
// The caller never receives an ownership token for an indeterminate write.
|
|
586
|
+
// Release the in-memory claim and preserve any bytes the backend may have
|
|
587
|
+
// applied before rejecting.
|
|
588
|
+
this.releaseNativeCommitOwnership(ownership);
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async mirrorManyToDurable(
|
|
594
|
+
blocks: Array<readonly [cid: string, bytes: Uint8Array]>,
|
|
595
|
+
options?: { nativeTrimmedCids?: ReadonlySet<string> },
|
|
596
|
+
): Promise<unknown> {
|
|
597
|
+
this.throwIfNativeDurableCommitFailed();
|
|
598
|
+
const cids = blocks.map(([cid]) => cid);
|
|
599
|
+
// Rows trimmed later in the same native batch deliberately remain owned by
|
|
600
|
+
// that batch's staged cleanup; only mark surviving rows live.
|
|
601
|
+
const liveCids = cids.filter(
|
|
602
|
+
(cid) => !options?.nativeTrimmedCids?.has(cid),
|
|
603
|
+
);
|
|
604
|
+
const ownership = this.beginNativeCommitOwnership(
|
|
605
|
+
this.noteNativeBlockWrite(liveCids),
|
|
606
|
+
);
|
|
607
|
+
if (liveCids.length > 0) {
|
|
608
|
+
await this.waitForNativeDeleteCleanup();
|
|
609
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
await this.drainDurable();
|
|
612
|
+
if (ownership) {
|
|
613
|
+
const existed = await this.durable.hasMany([...ownership.rows.keys()]);
|
|
614
|
+
let index = 0;
|
|
615
|
+
for (const row of ownership.rows.values()) {
|
|
616
|
+
row.durableExistedBefore = existed[index++] === true;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (blocks.length > 0) {
|
|
620
|
+
await this.commitDurableMutation(
|
|
621
|
+
() => this.durable.putKnownMany(blocks),
|
|
622
|
+
cids,
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
await this.retryNativeDeleteCleanup();
|
|
626
|
+
this.throwIfNativeDurableCommitFailed();
|
|
627
|
+
return ownership;
|
|
628
|
+
} catch (error) {
|
|
629
|
+
this.releaseNativeCommitOwnership(ownership);
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async rollbackFailedNativeCommits(
|
|
635
|
+
cids: string[],
|
|
636
|
+
restoreNativeCids: string[] = [],
|
|
637
|
+
ownershipToken?: unknown,
|
|
638
|
+
): Promise<void> {
|
|
639
|
+
// This is the sole mutation allowed after poison: it removes a native
|
|
640
|
+
// transaction that the lower log never published. Do not route through the
|
|
641
|
+
// guarded rmMany path, and settle the failed mirror before compensating it.
|
|
642
|
+
await this.waitForTrackedDurableWrites();
|
|
643
|
+
// A failed replacement never published its trim. Reassert the restored CIDs
|
|
644
|
+
// as live before any compensation IO so staged/pending delete intents cannot
|
|
645
|
+
// remove the last acknowledged blocks during stop or reopen.
|
|
646
|
+
const ownership =
|
|
647
|
+
ownershipToken && typeof ownershipToken === "object"
|
|
648
|
+
? this.nativeCommitOwnerships.get(
|
|
649
|
+
(ownershipToken as NativeCommitOwnershipToken).id,
|
|
650
|
+
)
|
|
651
|
+
: undefined;
|
|
652
|
+
const verifiedOwnership =
|
|
653
|
+
ownership && ownership === ownershipToken ? ownership : undefined;
|
|
654
|
+
const restoreSet = new Set(restoreNativeCids);
|
|
655
|
+
const safeDurableDeletes = verifiedOwnership
|
|
656
|
+
? [...new Set(cids)].filter((cid) => {
|
|
657
|
+
const row = verifiedOwnership.rows.get(cid);
|
|
658
|
+
const owners = this.nativeCommitOwnershipsByCid.get(cid);
|
|
659
|
+
return (
|
|
660
|
+
!restoreSet.has(cid) &&
|
|
661
|
+
row?.durableExistedBefore === false &&
|
|
662
|
+
row.shared === false &&
|
|
663
|
+
(this.nativeBlockWriteGenerations.get(cid) ?? 0) ===
|
|
664
|
+
row.generation &&
|
|
665
|
+
owners?.size === 1 &&
|
|
666
|
+
owners.has(verifiedOwnership.id)
|
|
667
|
+
);
|
|
668
|
+
})
|
|
669
|
+
: [];
|
|
670
|
+
this.noteNativeBlockWrite(restoreNativeCids);
|
|
671
|
+
let firstError: unknown;
|
|
672
|
+
try {
|
|
673
|
+
if (safeDurableDeletes.length > 0) {
|
|
674
|
+
await this.durable.rmMany(safeDurableDeletes);
|
|
675
|
+
}
|
|
676
|
+
} catch (error) {
|
|
677
|
+
firstError = error;
|
|
678
|
+
}
|
|
679
|
+
// A native prepare runs before ownership can observe the hot map, so it cannot
|
|
680
|
+
// prove that a CID was absent there before this operation. Keep native bytes as
|
|
681
|
+
// unreachable orphans rather than deleting acknowledged/shared/restored data.
|
|
682
|
+
if (restoreNativeCids.length > 0) {
|
|
683
|
+
try {
|
|
684
|
+
const values = await this.durable.getMany(restoreNativeCids);
|
|
685
|
+
const restore: Array<readonly [string, Uint8Array]> = [];
|
|
686
|
+
for (let index = 0; index < restoreNativeCids.length; index++) {
|
|
687
|
+
const value = values[index];
|
|
688
|
+
if (value) restore.push([restoreNativeCids[index]!, value]);
|
|
689
|
+
}
|
|
690
|
+
if (restore.length > 0) {
|
|
691
|
+
this.native.putKnownMany(restore);
|
|
692
|
+
}
|
|
693
|
+
} catch (error) {
|
|
694
|
+
firstError ??= error;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
this.releaseNativeCommitOwnership(ownershipToken);
|
|
698
|
+
if (firstError !== undefined) throw firstError;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Compensate a native prepare that failed before its durable mirror began.
|
|
703
|
+
* Durable presence proves a same-CID acknowledged owner; an active ownership
|
|
704
|
+
* token proves a concurrent mirror. Only an unowned, non-durable hot block is
|
|
705
|
+
* exclusively attributable to the failed prepare and safe to remove.
|
|
706
|
+
*/
|
|
707
|
+
async rollbackUnmirroredNativeCommits(
|
|
708
|
+
cids: string[],
|
|
709
|
+
restoreNativeCids: string[] = [],
|
|
710
|
+
): Promise<void> {
|
|
711
|
+
const unique = [...new Set(cids)];
|
|
712
|
+
// Native prepares bypass this wrapper. Any observed wrapper generation is
|
|
713
|
+
// therefore evidence of a generic/same-CID writer, not of the failed prepare.
|
|
714
|
+
// Snapshot before the first await and require both absence and stability so a
|
|
715
|
+
// write starting before or during durable.hasMany cannot lose its hot value to
|
|
716
|
+
// a stale `false` result.
|
|
717
|
+
const genericWriteGenerations = new Map(
|
|
718
|
+
unique.map((cid) => [cid, this.nativeBlockWriteGenerations.get(cid)]),
|
|
719
|
+
);
|
|
720
|
+
await this.rollbackFailedNativeCommits(cids, restoreNativeCids);
|
|
721
|
+
const durablePresence = await this.durable.hasMany(unique);
|
|
722
|
+
const restore = new Set(restoreNativeCids);
|
|
723
|
+
const safeNativeDeletes = unique.filter(
|
|
724
|
+
(cid, index) =>
|
|
725
|
+
!restore.has(cid) &&
|
|
726
|
+
durablePresence[index] !== true &&
|
|
727
|
+
genericWriteGenerations.get(cid) === undefined &&
|
|
728
|
+
this.nativeBlockWriteGenerations.get(cid) === undefined &&
|
|
729
|
+
(this.nativeCommitOwnershipsByCid.get(cid)?.size ?? 0) === 0,
|
|
730
|
+
);
|
|
731
|
+
if (safeNativeDeletes.length > 0) {
|
|
732
|
+
await this.native.rmMany(safeNativeDeletes);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// --- writes (apply to BOTH: native first for the hot path, then durable) ---
|
|
737
|
+
async put(
|
|
738
|
+
data: Uint8Array | { block: { bytes: Uint8Array }; cid: string },
|
|
739
|
+
): Promise<string> {
|
|
740
|
+
await this.drainDurable();
|
|
741
|
+
const cid = await this.native.put(data as any);
|
|
742
|
+
// The native store computes a raw-codec CID for a `Uint8Array`, storing
|
|
743
|
+
// the bytes verbatim (raw codec is identity), and stores `block.bytes`
|
|
744
|
+
// for the pre-CIDed object form. Either way the input bytes match what
|
|
745
|
+
// native stored, so feed durable the known cid+bytes without recomputing.
|
|
746
|
+
const value =
|
|
747
|
+
data instanceof Uint8Array
|
|
748
|
+
? data
|
|
749
|
+
: (data as { block: { bytes: Uint8Array } }).block.bytes;
|
|
750
|
+
this.noteNativeBlockWrite([cid]);
|
|
751
|
+
await this.waitForNativeDeleteCleanup();
|
|
752
|
+
// put() may have yielded while calculating the CID. Restore the hot value
|
|
753
|
+
// after any older cleanup that was already in flight.
|
|
754
|
+
this.throwIfNativeDurableCommitFailed();
|
|
755
|
+
this.native.putKnown(cid, value);
|
|
756
|
+
await this.commitDurableMutation(
|
|
757
|
+
() => this.durable.putKnown(cid, value),
|
|
758
|
+
[cid],
|
|
759
|
+
);
|
|
760
|
+
return cid;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
async putMany(
|
|
764
|
+
blocks: Array<Uint8Array | { block: { bytes: Uint8Array }; cid: string }>,
|
|
765
|
+
): Promise<string[]> {
|
|
766
|
+
await this.drainDurable();
|
|
767
|
+
const cids = await this.native.putMany(blocks as any);
|
|
768
|
+
const durableBlocks: Array<readonly [string, Uint8Array]> = cids.map(
|
|
769
|
+
(cid, index) => {
|
|
770
|
+
const block = blocks[index]!;
|
|
771
|
+
const value =
|
|
772
|
+
block instanceof Uint8Array
|
|
773
|
+
? block
|
|
774
|
+
: (block as { block: { bytes: Uint8Array } }).block.bytes;
|
|
775
|
+
return [cid, value] as const;
|
|
776
|
+
},
|
|
777
|
+
);
|
|
778
|
+
this.noteNativeBlockWrite(cids);
|
|
779
|
+
await this.waitForNativeDeleteCleanup();
|
|
780
|
+
this.throwIfNativeDurableCommitFailed();
|
|
781
|
+
this.native.putKnownMany(durableBlocks);
|
|
782
|
+
await this.commitDurableMutation(
|
|
783
|
+
() => this.durable.putKnownMany(durableBlocks),
|
|
784
|
+
cids,
|
|
785
|
+
);
|
|
786
|
+
return cids;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Native put is synchronous (the authoritative hot store); the durable mirror
|
|
790
|
+
// is awaited so the returned promise resolves only after BOTH native and
|
|
791
|
+
// durable succeed and a durable IO/disk-full failure rejects here instead of
|
|
792
|
+
// being swallowed. RemoteBlocks.putKnown and the log's putKnownEntryBytesBatch
|
|
793
|
+
// both await this method, so returning a promise is compatible.
|
|
794
|
+
async putKnown(cid: string, bytes: Uint8Array): Promise<string> {
|
|
795
|
+
await this.drainDurable();
|
|
796
|
+
await this.waitForNativeDeleteCleanup();
|
|
797
|
+
this.throwIfNativeDurableCommitFailed();
|
|
798
|
+
const stored = this.native.putKnown(cid, bytes);
|
|
799
|
+
this.noteNativeBlockWrite([cid]);
|
|
800
|
+
await this.commitDurableMutation(
|
|
801
|
+
() => this.durable.putKnown(cid, bytes),
|
|
802
|
+
[cid],
|
|
803
|
+
);
|
|
804
|
+
return stored;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
async putKnownMany(
|
|
808
|
+
blocks: Array<readonly [cid: string, bytes: Uint8Array]>,
|
|
809
|
+
): Promise<string[]> {
|
|
810
|
+
await this.drainDurable();
|
|
811
|
+
await this.waitForNativeDeleteCleanup();
|
|
812
|
+
this.throwIfNativeDurableCommitFailed();
|
|
813
|
+
const cids = this.native.putKnownMany(blocks);
|
|
814
|
+
this.noteNativeBlockWrite(cids);
|
|
815
|
+
await this.commitDurableMutation(
|
|
816
|
+
() => this.durable.putKnownMany(blocks),
|
|
817
|
+
cids,
|
|
818
|
+
);
|
|
819
|
+
return cids;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
putKnownManyColumns(cids: string[], bytes: Uint8Array[]): string[] {
|
|
823
|
+
this.throwIfNativeDurableCommitFailed();
|
|
824
|
+
if (cids.length !== bytes.length) {
|
|
825
|
+
throw new Error("Expected equal block column lengths");
|
|
826
|
+
}
|
|
827
|
+
const cleanupBarrier = this.nativeDeleteCleanupRunning;
|
|
828
|
+
const stored = this.native.putKnownManyColumns(cids, bytes);
|
|
829
|
+
this.noteNativeBlockWrite(cids);
|
|
830
|
+
// AnyBlockStore has no columnar method; mirror via putKnownMany, which
|
|
831
|
+
// takes [cid, bytes] tuples and hits the same batched store path.
|
|
832
|
+
// This method must return a synchronous string[] (RemoteBlocks.putKnownManyColumns
|
|
833
|
+
// consumes the result synchronously), so the durable write cannot be awaited
|
|
834
|
+
// inline. Track it instead of `void`ing it so a durable rejection surfaces
|
|
835
|
+
// on the next awaited wrapper method / stop() rather than being swallowed.
|
|
836
|
+
const durableBlocks = cids.map(
|
|
837
|
+
(cid, index) => [cid, bytes[index]!] as const,
|
|
838
|
+
);
|
|
839
|
+
let durableResult: unknown;
|
|
840
|
+
try {
|
|
841
|
+
durableResult = cleanupBarrier
|
|
842
|
+
? cleanupBarrier.then(() => this.durable.putKnownMany(durableBlocks))
|
|
843
|
+
: this.durable.putKnownMany(durableBlocks);
|
|
844
|
+
} catch (error) {
|
|
845
|
+
throw this.recordNativeDurableCommitFailure(error, {
|
|
846
|
+
committedCids: cids,
|
|
847
|
+
failedCids: cids,
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
this.trackDurable(durableResult, cids);
|
|
851
|
+
return stored;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// --- reads (native/wasm first; on miss, durable fallback + repopulate) ---
|
|
855
|
+
// RemoteBlocks.get awaits this single-get, so on a native miss consult the
|
|
856
|
+
// durable store (like getMany does) and repopulate the native map, rather
|
|
857
|
+
// than returning undefined and only scheduling a background repopulate. That
|
|
858
|
+
// avoids a spurious miss (which would otherwise fall through to a remote
|
|
859
|
+
// read) for a block that is present on disk.
|
|
860
|
+
async get(cid: string, _options?: unknown): Promise<Uint8Array | undefined> {
|
|
861
|
+
if (this.nativeDurableCommitFailure) {
|
|
862
|
+
// After poison, durable storage is the last acknowledged authority. Do not
|
|
863
|
+
// repopulate or otherwise mutate the native store until reopen.
|
|
864
|
+
return this.isNativeDeletePending(cid)
|
|
865
|
+
? undefined
|
|
866
|
+
: this.durable.get(cid);
|
|
867
|
+
}
|
|
868
|
+
const deleteEpoch = this.nativeDeleteEpoch;
|
|
869
|
+
if (this.isNativeDeletePending(cid)) {
|
|
870
|
+
return undefined;
|
|
871
|
+
}
|
|
872
|
+
const local = this.native.get(cid);
|
|
873
|
+
if (local != null) {
|
|
874
|
+
return deleteEpoch === this.nativeDeleteEpoch ? local : this.get(cid);
|
|
875
|
+
}
|
|
876
|
+
const durableValue = await this.durable.get(cid);
|
|
877
|
+
if (this.nativeDurableCommitFailure) {
|
|
878
|
+
return this.isNativeDeletePending(cid) ? undefined : durableValue;
|
|
879
|
+
}
|
|
880
|
+
if (deleteEpoch !== this.nativeDeleteEpoch) {
|
|
881
|
+
return this.get(cid);
|
|
882
|
+
}
|
|
883
|
+
if (durableValue != null) {
|
|
884
|
+
// Repopulate the native map so the native graph reads it next time.
|
|
885
|
+
this.native.putKnownManyColumns([cid], [durableValue]);
|
|
886
|
+
return durableValue;
|
|
887
|
+
}
|
|
888
|
+
return undefined;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
async getMany(cids: string[]): Promise<Array<Uint8Array | undefined>> {
|
|
892
|
+
if (this.nativeDurableCommitFailure) {
|
|
893
|
+
const values = await this.durable.getMany(cids);
|
|
894
|
+
for (let index = 0; index < cids.length; index++) {
|
|
895
|
+
if (this.isNativeDeletePending(cids[index]!)) {
|
|
896
|
+
values[index] = undefined;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return values;
|
|
900
|
+
}
|
|
901
|
+
const deleteEpoch = this.nativeDeleteEpoch;
|
|
902
|
+
const results = await this.native.getMany(cids);
|
|
903
|
+
if (deleteEpoch !== this.nativeDeleteEpoch) {
|
|
904
|
+
return this.getMany(cids);
|
|
905
|
+
}
|
|
906
|
+
const missing: string[] = [];
|
|
907
|
+
const missingIndexes: number[] = [];
|
|
908
|
+
for (let i = 0; i < results.length; i++) {
|
|
909
|
+
if (this.isNativeDeletePending(cids[i]!)) {
|
|
910
|
+
results[i] = undefined;
|
|
911
|
+
} else if (results[i] == null) {
|
|
912
|
+
missing.push(cids[i]!);
|
|
913
|
+
missingIndexes.push(i);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (missing.length === 0) {
|
|
917
|
+
return results;
|
|
918
|
+
}
|
|
919
|
+
const durableValues = await this.durable.getMany(missing);
|
|
920
|
+
if (this.nativeDurableCommitFailure) {
|
|
921
|
+
const values = await this.durable.getMany(cids);
|
|
922
|
+
for (let index = 0; index < cids.length; index++) {
|
|
923
|
+
if (this.isNativeDeletePending(cids[index]!)) {
|
|
924
|
+
values[index] = undefined;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return values;
|
|
928
|
+
}
|
|
929
|
+
if (deleteEpoch !== this.nativeDeleteEpoch) {
|
|
930
|
+
return this.getMany(cids);
|
|
931
|
+
}
|
|
932
|
+
const repopulateCids: string[] = [];
|
|
933
|
+
const repopulateBytes: Uint8Array[] = [];
|
|
934
|
+
for (
|
|
935
|
+
let missingIndex = 0;
|
|
936
|
+
missingIndex < missingIndexes.length;
|
|
937
|
+
missingIndex++
|
|
938
|
+
) {
|
|
939
|
+
const i = missingIndexes[missingIndex]!;
|
|
940
|
+
const value = durableValues[missingIndex];
|
|
941
|
+
if (value != null) {
|
|
942
|
+
results[i] = value;
|
|
943
|
+
repopulateCids.push(cids[i]!);
|
|
944
|
+
repopulateBytes.push(value);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (repopulateCids.length > 0) {
|
|
948
|
+
// Repopulate the native map so the native graph sees these blocks.
|
|
949
|
+
this.native.putKnownManyColumns(repopulateCids, repopulateBytes);
|
|
950
|
+
}
|
|
951
|
+
return results;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
async has(cid: string): Promise<boolean> {
|
|
955
|
+
if (this.nativeDurableCommitFailure) {
|
|
956
|
+
return this.isNativeDeletePending(cid) ? false : this.durable.has(cid);
|
|
957
|
+
}
|
|
958
|
+
const deleteEpoch = this.nativeDeleteEpoch;
|
|
959
|
+
if (this.isNativeDeletePending(cid)) {
|
|
960
|
+
return false;
|
|
961
|
+
}
|
|
962
|
+
if (this.native.has(cid)) {
|
|
963
|
+
return deleteEpoch === this.nativeDeleteEpoch ? true : this.has(cid);
|
|
964
|
+
}
|
|
965
|
+
// Mirror getMany/hasMany: a block absent from the native wasm map may still
|
|
966
|
+
// be present in the durable store (e.g. persisted on disk but not yet
|
|
967
|
+
// repopulated into wasm). Consult durable on a native miss so presence
|
|
968
|
+
// checks agree with the resolves that getMany/hasMany already durable-fall
|
|
969
|
+
// back on. `Blocks.has` is declared `MaybePromise<boolean>`, so returning a
|
|
970
|
+
// promise here is contract-compatible.
|
|
971
|
+
const durableHas = await this.durable.has(cid);
|
|
972
|
+
return deleteEpoch === this.nativeDeleteEpoch ? durableHas : this.has(cid);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
async hasMany(cids: string[]): Promise<boolean[]> {
|
|
976
|
+
if (this.nativeDurableCommitFailure) {
|
|
977
|
+
const values = await this.durable.hasMany(cids);
|
|
978
|
+
for (let index = 0; index < cids.length; index++) {
|
|
979
|
+
if (this.isNativeDeletePending(cids[index]!)) {
|
|
980
|
+
values[index] = false;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return values;
|
|
984
|
+
}
|
|
985
|
+
const deleteEpoch = this.nativeDeleteEpoch;
|
|
986
|
+
const nativeHas = await this.native.hasMany(cids);
|
|
987
|
+
if (deleteEpoch !== this.nativeDeleteEpoch) {
|
|
988
|
+
return this.hasMany(cids);
|
|
989
|
+
}
|
|
990
|
+
const missing: string[] = [];
|
|
991
|
+
const missingIndexes: number[] = [];
|
|
992
|
+
for (let i = 0; i < nativeHas.length; i++) {
|
|
993
|
+
if (this.isNativeDeletePending(cids[i]!)) {
|
|
994
|
+
nativeHas[i] = false;
|
|
995
|
+
} else if (!nativeHas[i]) {
|
|
996
|
+
missing.push(cids[i]!);
|
|
997
|
+
missingIndexes.push(i);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (missing.length === 0) {
|
|
1001
|
+
return nativeHas;
|
|
1002
|
+
}
|
|
1003
|
+
const durableHas = await this.durable.hasMany(missing);
|
|
1004
|
+
if (deleteEpoch !== this.nativeDeleteEpoch) {
|
|
1005
|
+
return this.hasMany(cids);
|
|
1006
|
+
}
|
|
1007
|
+
for (
|
|
1008
|
+
let missingIndex = 0;
|
|
1009
|
+
missingIndex < missingIndexes.length;
|
|
1010
|
+
missingIndex++
|
|
1011
|
+
) {
|
|
1012
|
+
nativeHas[missingIndexes[missingIndex]!] = durableHas[missingIndex]!;
|
|
1013
|
+
}
|
|
1014
|
+
return nativeHas;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// --- removes (apply to BOTH) ----------------------------------------
|
|
1018
|
+
// Native rm is synchronous; the durable rm is awaited so the returned promise
|
|
1019
|
+
// resolves only after both succeed and a durable failure rejects here rather
|
|
1020
|
+
// than being swallowed. All rm callers (RemoteBlocks.rm, the log) await it.
|
|
1021
|
+
async rm(cid: string): Promise<void> {
|
|
1022
|
+
this.throwIfNativeDurableCommitFailed();
|
|
1023
|
+
const writeGeneration = this.nativeBlockWriteGenerations.get(cid);
|
|
1024
|
+
this.beginNativeDelete([cid]);
|
|
1025
|
+
try {
|
|
1026
|
+
await this.drainDurable();
|
|
1027
|
+
this.native.rm(cid);
|
|
1028
|
+
await this.commitDurableMutation(() => this.durable.rm(cid), [cid]);
|
|
1029
|
+
// A durable read that began before the tombstone may have repopulated
|
|
1030
|
+
// native while durable rm was pending. Remove it idempotently again.
|
|
1031
|
+
this.native.rm(cid);
|
|
1032
|
+
if (
|
|
1033
|
+
this.nativeBlockWriteGenerations.get(cid) === writeGeneration &&
|
|
1034
|
+
!this.pendingNativeDeleteCleanup.has(cid)
|
|
1035
|
+
) {
|
|
1036
|
+
this.nativeBlockWriteGenerations.delete(cid);
|
|
1037
|
+
}
|
|
1038
|
+
} finally {
|
|
1039
|
+
this.endNativeDelete([cid]);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
del(cid: string): Promise<void> {
|
|
1044
|
+
return this.rm(cid);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async rmMany(cids: string[]): Promise<number> {
|
|
1048
|
+
this.throwIfNativeDurableCommitFailed();
|
|
1049
|
+
const writeGenerations = new Map(
|
|
1050
|
+
cids.map((cid) => [cid, this.nativeBlockWriteGenerations.get(cid)]),
|
|
1051
|
+
);
|
|
1052
|
+
this.beginNativeDelete(cids);
|
|
1053
|
+
try {
|
|
1054
|
+
await this.drainDurable();
|
|
1055
|
+
const removed = await this.native.rmMany(cids);
|
|
1056
|
+
await this.commitDurableMutation(() => this.durable.rmMany(cids), cids);
|
|
1057
|
+
await this.native.rmMany(cids);
|
|
1058
|
+
for (const cid of cids) {
|
|
1059
|
+
if (
|
|
1060
|
+
this.nativeBlockWriteGenerations.get(cid) ===
|
|
1061
|
+
writeGenerations.get(cid) &&
|
|
1062
|
+
!this.pendingNativeDeleteCleanup.has(cid)
|
|
1063
|
+
) {
|
|
1064
|
+
this.nativeBlockWriteGenerations.delete(cid);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
return removed;
|
|
1068
|
+
} finally {
|
|
1069
|
+
this.endNativeDelete(cids);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Native trim may already have removed the hot wasm blocks. Queue the durable
|
|
1074
|
+
// copy for cleanup, retaining read tombstones until removal succeeds; a
|
|
1075
|
+
// cleanup failure is retried and never fed into ordinary append rollback.
|
|
1076
|
+
// EntryIndex feature-detects this hook.
|
|
1077
|
+
async rmManyAfterNativeDelete(
|
|
1078
|
+
cids: string[],
|
|
1079
|
+
cleanupToken?: unknown,
|
|
1080
|
+
): Promise<void> {
|
|
1081
|
+
if (this.nativeDurableCommitFailure) {
|
|
1082
|
+
this.cancelNativeDeleteCleanup(cleanupToken);
|
|
1083
|
+
throw this.nativeDurableCommitFailure;
|
|
1084
|
+
}
|
|
1085
|
+
let preannounced = false;
|
|
1086
|
+
if (typeof cleanupToken === "number") {
|
|
1087
|
+
const staged = this.stagedNativeDeleteCleanups.get(cleanupToken);
|
|
1088
|
+
if (staged) {
|
|
1089
|
+
preannounced = true;
|
|
1090
|
+
this.stagedNativeDeleteCleanups.delete(cleanupToken);
|
|
1091
|
+
for (const [cid, generation] of staged) {
|
|
1092
|
+
if ((this.nativeBlockWriteGenerations.get(cid) ?? 0) !== generation) {
|
|
1093
|
+
this.endNativeDelete([cid]);
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
if (this.pendingNativeDeleteCleanup.has(cid)) {
|
|
1097
|
+
// Another delete already owns a tombstone for this CID.
|
|
1098
|
+
this.endNativeDelete([cid]);
|
|
1099
|
+
} else {
|
|
1100
|
+
// Transfer the staged tombstone to the now-published cleanup.
|
|
1101
|
+
this.pendingNativeDeleteCleanup.set(cid, generation);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
if (!preannounced) {
|
|
1107
|
+
this.enqueueNativeDeleteCleanup(cids);
|
|
1108
|
+
}
|
|
1109
|
+
await this.retryNativeDeleteCleanup();
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/** Finish committed trim GC before its durable recovery intent is retired. */
|
|
1113
|
+
async completeCommittedNativeDeleteCleanup(
|
|
1114
|
+
cids: string[],
|
|
1115
|
+
options?: { reconstructMissing?: boolean },
|
|
1116
|
+
): Promise<void> {
|
|
1117
|
+
this.throwIfNativeDurableCommitFailed();
|
|
1118
|
+
const uniqueCids = [...new Set(cids.filter(Boolean))];
|
|
1119
|
+
if (uniqueCids.length === 0) {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
// Only restart recovery reconstructs missing debt. On the live path, an
|
|
1123
|
+
// absent row may mean the CID was legitimately re-added after its original
|
|
1124
|
+
// generation-owned trim completed or was cancelled; re-enqueueing it here
|
|
1125
|
+
// would capture the new generation and delete live content.
|
|
1126
|
+
if (options?.reconstructMissing) {
|
|
1127
|
+
this.enqueueNativeDeleteCleanup(uniqueCids);
|
|
1128
|
+
}
|
|
1129
|
+
if (!uniqueCids.some((cid) => this.pendingNativeDeleteCleanup.has(cid))) {
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
await this.retryNativeDeleteCleanup({ throwOnFailure: true });
|
|
1133
|
+
const remaining = uniqueCids.filter((cid) =>
|
|
1134
|
+
this.pendingNativeDeleteCleanup.has(cid),
|
|
1135
|
+
);
|
|
1136
|
+
if (remaining.length > 0) {
|
|
1137
|
+
throw new Error(
|
|
1138
|
+
`Committed native trim cleanup remains incomplete: ${remaining.join(", ")}`,
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// --- misc ------------------------------------------------------------
|
|
1144
|
+
async *iterator(): AsyncGenerator<[string, Uint8Array], void, void> {
|
|
1145
|
+
if (this.nativeDurableCommitFailure) {
|
|
1146
|
+
for await (const block of this.durable.iterator()) {
|
|
1147
|
+
if (!this.isNativeDeletePending(block[0])) {
|
|
1148
|
+
yield block;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
yield* this.native.iterator();
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
async size(): Promise<number> {
|
|
1157
|
+
// The hot wasm map is intentionally cold after reopen, so it cannot be the
|
|
1158
|
+
// storage-budget authority. Settle synchronous columnar mirrors first, then
|
|
1159
|
+
// report durable bytes; pending trim cleanup remains conservatively counted
|
|
1160
|
+
// until its durable deletion succeeds.
|
|
1161
|
+
await this.drainDurable();
|
|
1162
|
+
return this.durable.size();
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
persisted(): boolean {
|
|
1166
|
+
// The blocks are now mirrored to a durable store, so report persisted so
|
|
1167
|
+
// callers that gate durable-only behavior on this flag behave correctly.
|
|
1168
|
+
return true;
|
|
1169
|
+
}
|
|
1170
|
+
}
|