@peerbit/shared-log 13.2.4 → 13.2.5
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/exchange-heads.d.ts.map +1 -1
- package/dist/src/exchange-heads.js +12 -0
- package/dist/src/exchange-heads.js.map +1 -1
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +477 -13
- package/dist/src/index.js.map +1 -1
- package/package.json +17 -16
- package/src/exchange-heads.ts +13 -0
- package/src/index.ts +549 -15
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
getPublicKeyFromPeerId,
|
|
18
18
|
sha256Base64Sync,
|
|
19
19
|
sha256Sync,
|
|
20
|
+
toHexString,
|
|
20
21
|
} from "@peerbit/crypto";
|
|
21
22
|
import {
|
|
22
23
|
And,
|
|
@@ -368,6 +369,339 @@ const canUseOptionalNativeModuleImports = (): boolean => {
|
|
|
368
369
|
);
|
|
369
370
|
};
|
|
370
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Build the per-program coordinate persistence directory under a node's
|
|
374
|
+
* durable storage root: `<nodeDirectory>/coordinates/<fsSafeLogId>`. Uses
|
|
375
|
+
* forward-slash joining (accepted by Node's `fs` on every platform) so
|
|
376
|
+
* shared-log does not need to statically import `node:path`, which is not
|
|
377
|
+
* available in browser bundles. Trailing separators on the root are trimmed
|
|
378
|
+
* to avoid doubled slashes.
|
|
379
|
+
*/
|
|
380
|
+
const joinNativeCoordinateDirectory = (
|
|
381
|
+
nodeDirectory: string,
|
|
382
|
+
fsSafeLogId: string,
|
|
383
|
+
): string => `${nodeDirectory.replace(/[/\\]+$/, "")}/coordinates/${fsSafeLogId}`;
|
|
384
|
+
|
|
385
|
+
/** The native backbone's in-wasm-memory block store. */
|
|
386
|
+
type NativeBackboneBlocks = NativePeerbitBackbone["blocks"];
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Write-through block store bridging the native backbone's in-wasm-memory block
|
|
390
|
+
* store to a durable per-program {@link AnyBlockStore}.
|
|
391
|
+
*
|
|
392
|
+
* WHY: when the native backbone is active the log's entry blocks live only in
|
|
393
|
+
* the native wasm block map (`NativeBackboneBlockStore.persisted() === false`).
|
|
394
|
+
* On a restart that map is empty, so the native graph cannot reload heads the
|
|
395
|
+
* durable heads index still lists ("Failed to load entry from head"). This
|
|
396
|
+
* wrapper mirrors every write into the same durable `blocks` sublevel the
|
|
397
|
+
* non-native path uses. On a native miss, reads fall through to durable storage
|
|
398
|
+
* and lazily repopulate the wasm map so the native graph can walk the DAG again.
|
|
399
|
+
*
|
|
400
|
+
* The native store stays the authoritative hot store the native graph reads
|
|
401
|
+
* from: reads hit native first and only fall back to durable (repopulating
|
|
402
|
+
* native on a hit so subsequent native-graph reads succeed).
|
|
403
|
+
*
|
|
404
|
+
* METHOD SURFACE (see #1006): `RemoteBlocks` and the log feature-detect the
|
|
405
|
+
* optional batch methods (`putMany`/`putKnown`/`putKnownMany`/
|
|
406
|
+
* `putKnownManyColumns`/`rmMany`). To keep the receive-fusion / columnar fast
|
|
407
|
+
* paths engaged this wrapper exposes EXACTLY the methods the native store
|
|
408
|
+
* exposes — including `putKnownManyColumns`, which `AnyBlockStore` does not have
|
|
409
|
+
* — and delegates each. It deliberately does not add methods the native store
|
|
410
|
+
* lacks (`getBlockResponsePayload`/`getNativeLogBlockStoreHandle` stay absent so
|
|
411
|
+
* the optional-chained probes in `RemoteBlocks` behave exactly as today).
|
|
412
|
+
*/
|
|
413
|
+
class NativeBackboneWriteThroughBlockStore {
|
|
414
|
+
constructor(
|
|
415
|
+
private readonly native: NativeBackboneBlocks,
|
|
416
|
+
private readonly durable: AnyBlockStore,
|
|
417
|
+
) {}
|
|
418
|
+
|
|
419
|
+
// A durable mirror write that cannot be awaited at its call site (the
|
|
420
|
+
// columnar putKnownManyColumns fast path must return a synchronous string[]
|
|
421
|
+
// because RemoteBlocks.putKnownManyColumns treats the result as sync) is
|
|
422
|
+
// tracked here instead of being silently `void`ed. Its rejection is stored
|
|
423
|
+
// and re-thrown on the next awaited wrapper method (and on stop()), so a
|
|
424
|
+
// failed durable write (IO/disk-full) surfaces as an error rather than
|
|
425
|
+
// vanishing and leaving the block out of durable while native/log report
|
|
426
|
+
// success. `.catch` also prevents unhandled-rejection noise.
|
|
427
|
+
private readonly pendingDurableWrites = new Set<Promise<unknown>>();
|
|
428
|
+
private durableWriteError: unknown;
|
|
429
|
+
|
|
430
|
+
private trackDurable(result: unknown): void {
|
|
431
|
+
// The durable store may answer synchronously (an in-memory or already
|
|
432
|
+
// resolved store); only a real pending promise needs tracking. A sync
|
|
433
|
+
// success is already durable; a sync throw would have propagated already.
|
|
434
|
+
if (!isPromiseLike(result)) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const tracked = Promise.resolve(result).then(
|
|
438
|
+
() => {
|
|
439
|
+
this.pendingDurableWrites.delete(tracked);
|
|
440
|
+
},
|
|
441
|
+
(error) => {
|
|
442
|
+
this.pendingDurableWrites.delete(tracked);
|
|
443
|
+
if (this.durableWriteError === undefined) {
|
|
444
|
+
this.durableWriteError = error;
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
);
|
|
448
|
+
this.pendingDurableWrites.add(tracked);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Wait for every tracked (sync-path) durable write to settle, then surface
|
|
452
|
+
// the first failure. Awaited methods call this so a prior columnar durable
|
|
453
|
+
// failure propagates as back-pressure to the next caller.
|
|
454
|
+
private async drainDurable(): Promise<void> {
|
|
455
|
+
while (this.pendingDurableWrites.size > 0) {
|
|
456
|
+
await Promise.allSettled([...this.pendingDurableWrites]);
|
|
457
|
+
}
|
|
458
|
+
if (this.durableWriteError !== undefined) {
|
|
459
|
+
const error = this.durableWriteError;
|
|
460
|
+
this.durableWriteError = undefined;
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// --- lifecycle -------------------------------------------------------
|
|
466
|
+
// The native store's lifecycle hooks are no-ops; only the durable store
|
|
467
|
+
// needs starting/stopping. The wasm map is NOT eagerly rehydrated from disk:
|
|
468
|
+
// entry blocks are pulled back lazily on demand through the read fallback in
|
|
469
|
+
// getMany()/get() (durable hit -> repopulate the wasm map), which is what the
|
|
470
|
+
// log's DAG walk (EntryIndex.resolveMany -> store.getMany) exercises. Keeping
|
|
471
|
+
// the wasm map cold on open is required by the strict-native resident
|
|
472
|
+
// coordinate-state optimization: a reopened non-replicating native node must
|
|
473
|
+
// report hasBlock(head) === false and answer a same-signer append from the
|
|
474
|
+
// persisted coordinate + signer facts without resolving the entry block.
|
|
475
|
+
async start(): Promise<void> {
|
|
476
|
+
await this.durable.start();
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async stop(): Promise<void> {
|
|
480
|
+
// Surface any tracked (sync-path) durable write failure and ensure all
|
|
481
|
+
// mirror writes have settled before the durable store is torn down.
|
|
482
|
+
await this.drainDurable();
|
|
483
|
+
await this.durable.stop();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
status() {
|
|
487
|
+
return this.durable.status();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
waitFor(): Promise<string[]> {
|
|
491
|
+
return Promise.resolve([]);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Mirror a single already-committed block (present in the wasm map) to the
|
|
495
|
+
// durable store ONLY. Used by the native commit-only append fast path: the
|
|
496
|
+
// native prepare commits the entry block into the wasm map and returns no raw
|
|
497
|
+
// bytes, and the strict-native resident-coordinate path deliberately does NOT
|
|
498
|
+
// route the block through the log's finishBlocks/putKnown* (that would disturb
|
|
499
|
+
// the commit-only append path the RCS optimization depends on). Instead the
|
|
500
|
+
// caller reads the committed bytes back and calls this so the block lands in
|
|
501
|
+
// durable directly. Like putKnownManyColumns' durable mirror, the write is
|
|
502
|
+
// tracked (not fire-and-forget) so an IO/disk-full failure surfaces on the
|
|
503
|
+
// next awaited wrapper method / stop() rather than being swallowed.
|
|
504
|
+
mirrorToDurable(cid: string, bytes: Uint8Array): void {
|
|
505
|
+
this.trackDurable(this.durable.putKnown(cid, bytes));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// --- writes (apply to BOTH: native first for the hot path, then durable) ---
|
|
509
|
+
async put(
|
|
510
|
+
data: Uint8Array | { block: { bytes: Uint8Array }; cid: string },
|
|
511
|
+
): Promise<string> {
|
|
512
|
+
await this.drainDurable();
|
|
513
|
+
const cid = await this.native.put(data as any);
|
|
514
|
+
// The native store computes a raw-codec CID for a `Uint8Array`, storing
|
|
515
|
+
// the bytes verbatim (raw codec is identity), and stores `block.bytes`
|
|
516
|
+
// for the pre-CIDed object form. Either way the input bytes match what
|
|
517
|
+
// native stored, so feed durable the known cid+bytes without recomputing.
|
|
518
|
+
const value =
|
|
519
|
+
data instanceof Uint8Array
|
|
520
|
+
? data
|
|
521
|
+
: (data as { block: { bytes: Uint8Array } }).block.bytes;
|
|
522
|
+
await this.durable.putKnown(cid, value);
|
|
523
|
+
return cid;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async putMany(
|
|
527
|
+
blocks: Array<Uint8Array | { block: { bytes: Uint8Array }; cid: string }>,
|
|
528
|
+
): Promise<string[]> {
|
|
529
|
+
await this.drainDurable();
|
|
530
|
+
const cids = await this.native.putMany(blocks as any);
|
|
531
|
+
const durableBlocks: Array<readonly [string, Uint8Array]> = cids.map(
|
|
532
|
+
(cid, index) => {
|
|
533
|
+
const block = blocks[index]!;
|
|
534
|
+
const value =
|
|
535
|
+
block instanceof Uint8Array
|
|
536
|
+
? block
|
|
537
|
+
: (block as { block: { bytes: Uint8Array } }).block.bytes;
|
|
538
|
+
return [cid, value] as const;
|
|
539
|
+
},
|
|
540
|
+
);
|
|
541
|
+
await this.durable.putKnownMany(durableBlocks);
|
|
542
|
+
return cids;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Native put is synchronous (the authoritative hot store); the durable mirror
|
|
546
|
+
// is awaited so the returned promise resolves only after BOTH native and
|
|
547
|
+
// durable succeed and a durable IO/disk-full failure rejects here instead of
|
|
548
|
+
// being swallowed. RemoteBlocks.putKnown and the log's putKnownEntryBytesBatch
|
|
549
|
+
// both await this method, so returning a promise is compatible.
|
|
550
|
+
async putKnown(cid: string, bytes: Uint8Array): Promise<string> {
|
|
551
|
+
await this.drainDurable();
|
|
552
|
+
const stored = this.native.putKnown(cid, bytes);
|
|
553
|
+
await this.durable.putKnown(cid, bytes);
|
|
554
|
+
return stored;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async putKnownMany(
|
|
558
|
+
blocks: Array<readonly [cid: string, bytes: Uint8Array]>,
|
|
559
|
+
): Promise<string[]> {
|
|
560
|
+
await this.drainDurable();
|
|
561
|
+
const cids = this.native.putKnownMany(blocks);
|
|
562
|
+
await this.durable.putKnownMany(blocks);
|
|
563
|
+
return cids;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
putKnownManyColumns(cids: string[], bytes: Uint8Array[]): string[] {
|
|
567
|
+
const stored = this.native.putKnownManyColumns(cids, bytes);
|
|
568
|
+
// AnyBlockStore has no columnar method; mirror via putKnownMany, which
|
|
569
|
+
// takes [cid, bytes] tuples and hits the same batched store path.
|
|
570
|
+
// This method must return a synchronous string[] (RemoteBlocks.putKnownManyColumns
|
|
571
|
+
// consumes the result synchronously), so the durable write cannot be awaited
|
|
572
|
+
// inline. Track it instead of `void`ing it so a durable rejection surfaces
|
|
573
|
+
// on the next awaited wrapper method / stop() rather than being swallowed.
|
|
574
|
+
const durableBlocks = cids.map(
|
|
575
|
+
(cid, index) => [cid, bytes[index]!] as const,
|
|
576
|
+
);
|
|
577
|
+
this.trackDurable(this.durable.putKnownMany(durableBlocks));
|
|
578
|
+
return stored;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// --- reads (native/wasm first; on miss, durable fallback + repopulate) ---
|
|
582
|
+
// RemoteBlocks.get awaits this single-get, so on a native miss consult the
|
|
583
|
+
// durable store (like getMany does) and repopulate the native map, rather
|
|
584
|
+
// than returning undefined and only scheduling a background repopulate. That
|
|
585
|
+
// avoids a spurious miss (which would otherwise fall through to a remote
|
|
586
|
+
// read) for a block that is present on disk.
|
|
587
|
+
async get(cid: string, _options?: unknown): Promise<Uint8Array | undefined> {
|
|
588
|
+
const local = this.native.get(cid);
|
|
589
|
+
if (local != null) {
|
|
590
|
+
return local;
|
|
591
|
+
}
|
|
592
|
+
const durableValue = await this.durable.get(cid);
|
|
593
|
+
if (durableValue != null) {
|
|
594
|
+
// Repopulate the native map so the native graph reads it next time.
|
|
595
|
+
this.native.putKnownManyColumns([cid], [durableValue]);
|
|
596
|
+
return durableValue;
|
|
597
|
+
}
|
|
598
|
+
return undefined;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async getMany(cids: string[]): Promise<Array<Uint8Array | undefined>> {
|
|
602
|
+
const results = await this.native.getMany(cids);
|
|
603
|
+
const missing: string[] = [];
|
|
604
|
+
for (let i = 0; i < results.length; i++) {
|
|
605
|
+
if (results[i] == null) {
|
|
606
|
+
missing.push(cids[i]!);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (missing.length === 0) {
|
|
610
|
+
return results;
|
|
611
|
+
}
|
|
612
|
+
const durableValues = await this.durable.getMany(missing);
|
|
613
|
+
const repopulateCids: string[] = [];
|
|
614
|
+
const repopulateBytes: Uint8Array[] = [];
|
|
615
|
+
let missingIndex = 0;
|
|
616
|
+
for (let i = 0; i < results.length; i++) {
|
|
617
|
+
if (results[i] != null) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const value = durableValues[missingIndex++];
|
|
621
|
+
if (value != null) {
|
|
622
|
+
results[i] = value;
|
|
623
|
+
repopulateCids.push(cids[i]!);
|
|
624
|
+
repopulateBytes.push(value);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (repopulateCids.length > 0) {
|
|
628
|
+
// Repopulate the native map so the native graph sees these blocks.
|
|
629
|
+
this.native.putKnownManyColumns(repopulateCids, repopulateBytes);
|
|
630
|
+
}
|
|
631
|
+
return results;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async has(cid: string): Promise<boolean> {
|
|
635
|
+
if (this.native.has(cid)) {
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
// Mirror getMany/hasMany: a block absent from the native wasm map may still
|
|
639
|
+
// be present in the durable store (e.g. persisted on disk but not yet
|
|
640
|
+
// repopulated into wasm). Consult durable on a native miss so presence
|
|
641
|
+
// checks agree with the resolves that getMany/hasMany already durable-fall
|
|
642
|
+
// back on. `Blocks.has` is declared `MaybePromise<boolean>`, so returning a
|
|
643
|
+
// promise here is contract-compatible.
|
|
644
|
+
return this.durable.has(cid);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async hasMany(cids: string[]): Promise<boolean[]> {
|
|
648
|
+
const nativeHas = await this.native.hasMany(cids);
|
|
649
|
+
const missing: string[] = [];
|
|
650
|
+
for (let i = 0; i < nativeHas.length; i++) {
|
|
651
|
+
if (!nativeHas[i]) {
|
|
652
|
+
missing.push(cids[i]!);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (missing.length === 0) {
|
|
656
|
+
return nativeHas;
|
|
657
|
+
}
|
|
658
|
+
const durableHas = await this.durable.hasMany(missing);
|
|
659
|
+
let missingIndex = 0;
|
|
660
|
+
for (let i = 0; i < nativeHas.length; i++) {
|
|
661
|
+
if (!nativeHas[i]) {
|
|
662
|
+
nativeHas[i] = durableHas[missingIndex++]!;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return nativeHas;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// --- removes (apply to BOTH) ----------------------------------------
|
|
669
|
+
// Native rm is synchronous; the durable rm is awaited so the returned promise
|
|
670
|
+
// resolves only after both succeed and a durable failure rejects here rather
|
|
671
|
+
// than being swallowed. All rm callers (RemoteBlocks.rm, the log) await it.
|
|
672
|
+
async rm(cid: string): Promise<void> {
|
|
673
|
+
await this.drainDurable();
|
|
674
|
+
this.native.rm(cid);
|
|
675
|
+
await this.durable.rm(cid);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
del(cid: string): void {
|
|
679
|
+
void this.rm(cid);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async rmMany(cids: string[]): Promise<number> {
|
|
683
|
+
await this.drainDurable();
|
|
684
|
+
const removed = await this.native.rmMany(cids);
|
|
685
|
+
await this.durable.rmMany(cids);
|
|
686
|
+
return removed;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// --- misc ------------------------------------------------------------
|
|
690
|
+
async *iterator(): AsyncGenerator<[string, Uint8Array], void, void> {
|
|
691
|
+
yield* this.native.iterator();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
size(): number {
|
|
695
|
+
return this.native.size();
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
persisted(): boolean {
|
|
699
|
+
// The blocks are now mirrored to a durable store, so report persisted so
|
|
700
|
+
// callers that gate durable-only behavior on this flag behave correctly.
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
371
705
|
type LeaderMap = Map<string, { intersecting: boolean }>;
|
|
372
706
|
|
|
373
707
|
type LeaderSelectionOptions<R extends "u32" | "u64"> = {
|
|
@@ -6320,6 +6654,23 @@ export class SharedLog<
|
|
|
6320
6654
|
return undefined;
|
|
6321
6655
|
}
|
|
6322
6656
|
const backbone = this._nativeBackbone;
|
|
6657
|
+
// When the durable write-through wrapper is active the log's block store
|
|
6658
|
+
// (this.remoteBlocks.localStore) is the wrapper, NOT the raw wasm block map
|
|
6659
|
+
// (backbone.blocks), so this comparison is false. In that case the block
|
|
6660
|
+
// must be mirrored to durable directly; see the guarded handling in the
|
|
6661
|
+
// prepare callback below.
|
|
6662
|
+
const durableWrapperActive =
|
|
6663
|
+
this.remoteBlocks?.localStore !== backbone.blocks;
|
|
6664
|
+
// The write-through wrapper instance, captured only when active, so the
|
|
6665
|
+
// commit-only block can be mirrored to durable WITHOUT routing it through
|
|
6666
|
+
// the log's finishBlocks/putKnown* (which would disturb the strict-native
|
|
6667
|
+
// resident-coordinate append path). `mirrorToDurable` writes to the durable
|
|
6668
|
+
// side only and tracks the write for error-surfacing.
|
|
6669
|
+
const durableWrapper = durableWrapperActive
|
|
6670
|
+
? (this.remoteBlocks?.localStore as unknown as {
|
|
6671
|
+
mirrorToDurable?: (cid: string, bytes: Uint8Array) => void;
|
|
6672
|
+
})
|
|
6673
|
+
: undefined;
|
|
6323
6674
|
let nativeBackboneDocumentIndexCommitted = false;
|
|
6324
6675
|
let committedNativeBackboneDocumentIndex:
|
|
6325
6676
|
| NativeBackboneDocumentIndexCommitInput
|
|
@@ -6388,11 +6739,38 @@ export class SharedLog<
|
|
|
6388
6739
|
},
|
|
6389
6740
|
backbone.blocks,
|
|
6390
6741
|
);
|
|
6391
|
-
if (
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6742
|
+
if (prepared && !prepared.bytes) {
|
|
6743
|
+
if (durableWrapper) {
|
|
6744
|
+
// The durable write-through wrapper is active, so the block store
|
|
6745
|
+
// the log writes through (this.remoteBlocks.localStore) is the
|
|
6746
|
+
// wrapper, NOT the raw wasm block map. prepareEntryV0PlainEntryCommit
|
|
6747
|
+
// committed the block into the wasm map ONLY and returned no raw
|
|
6748
|
+
// bytes, so on its own the block would never reach durable (log's
|
|
6749
|
+
// finishBlocks only calls putKnown* when prepared.bytes is set) and
|
|
6750
|
+
// a non-replicating native node would lose it on restart.
|
|
6751
|
+
//
|
|
6752
|
+
// Mirror the just-committed block (read back from the wasm store)
|
|
6753
|
+
// straight to the DURABLE side of the wrapper. Crucially we do NOT
|
|
6754
|
+
// attach prepared.bytes: doing so would make the log's finishBlocks
|
|
6755
|
+
// call putKnown*, which changes the commit-only append path and
|
|
6756
|
+
// breaks the strict-native resident-coordinate optimization (the
|
|
6757
|
+
// reopen tests assert the append stays native and resolves no entry
|
|
6758
|
+
// block). Instead the prepared result is returned exactly as the
|
|
6759
|
+
// memory-only branch below (getBytes only, no bytes), so the log's
|
|
6760
|
+
// finishBlocks path is UNCHANGED, and the block is made durable
|
|
6761
|
+
// out-of-band here. mirrorToDurable tracks the write so an IO/
|
|
6762
|
+
// disk-full failure surfaces on a later awaited wrapper method.
|
|
6763
|
+
const preparedHash = prepared.cid ?? prepared.hash;
|
|
6764
|
+
const committedBytes = preparedHash
|
|
6765
|
+
? backbone.blocks.get(preparedHash)
|
|
6766
|
+
: undefined;
|
|
6767
|
+
if (committedBytes && durableWrapper.mirrorToDurable) {
|
|
6768
|
+
durableWrapper.mirrorToDurable(preparedHash!, committedBytes);
|
|
6769
|
+
}
|
|
6770
|
+
}
|
|
6771
|
+
// The block lives in the wasm map; expose getBytes for materialization
|
|
6772
|
+
// (identical to the memory-only native node path). When the durable
|
|
6773
|
+
// wrapper is active the durable copy was mirrored just above.
|
|
6396
6774
|
return {
|
|
6397
6775
|
...prepared,
|
|
6398
6776
|
getBytes: (hash: string) => backbone.blocks.get(hash),
|
|
@@ -6491,8 +6869,26 @@ export class SharedLog<
|
|
|
6491
6869
|
NativePeerbitBackbone["preparePlainCommittedNoNextStorageAppendTransaction"]
|
|
6492
6870
|
>
|
|
6493
6871
|
| undefined;
|
|
6872
|
+
// The write-through durable wrapper, when active, is captured here so a
|
|
6873
|
+
// just-committed block can be mirrored to durable without disturbing the
|
|
6874
|
+
// strict-native commit path. When the wrapper is active the log's block
|
|
6875
|
+
// store is the wrapper (not the raw wasm map), so `localStore ===
|
|
6876
|
+
// backbone.blocks` is false — but the store is still native-backed, so we
|
|
6877
|
+
// must commit blocks in the backbone (the committed native prepare variant
|
|
6878
|
+
// is the one that emits the document-signer journal record; the
|
|
6879
|
+
// block-deferring variant does not, which would otherwise leave
|
|
6880
|
+
// document-signers.wal unwritten and break same-signer facts after
|
|
6881
|
+
// reopen). The block is then mirrored to durable out-of-band below.
|
|
6882
|
+
const durableWrapperActive =
|
|
6883
|
+
this.remoteBlocks?.localStore !== backbone.blocks;
|
|
6884
|
+
const durableWrapper = durableWrapperActive
|
|
6885
|
+
? (this.remoteBlocks?.localStore as unknown as {
|
|
6886
|
+
mirrorToDurable?: (cid: string, bytes: Uint8Array) => void;
|
|
6887
|
+
})
|
|
6888
|
+
: undefined;
|
|
6494
6889
|
const commitBlocksInBackbone =
|
|
6495
|
-
this.remoteBlocks?.localStore === backbone.blocks
|
|
6890
|
+
this.remoteBlocks?.localStore === backbone.blocks ||
|
|
6891
|
+
durableWrapperActive;
|
|
6496
6892
|
let nativeBackboneDocumentIndexCommitted = false;
|
|
6497
6893
|
let nativeBackboneDocumentDeleteCommitted = false;
|
|
6498
6894
|
let committedNativeBackboneDocumentIndex:
|
|
@@ -6616,6 +7012,23 @@ export class SharedLog<
|
|
|
6616
7012
|
!!nativeBackboneDocumentDeleteKey;
|
|
6617
7013
|
const useTrimmedHashesOnly =
|
|
6618
7014
|
properties?.resolveTrimmedEntries === false;
|
|
7015
|
+
if (durableWrapper?.mirrorToDurable) {
|
|
7016
|
+
// The block was committed into the wasm map (commitBlocksInBackbone is
|
|
7017
|
+
// true) but not into durable, because the log's finishBlocks path is
|
|
7018
|
+
// left UNCHANGED for strict-native mode (getBytes only, no bytes, so
|
|
7019
|
+
// no putKnown* through the wrapper). Mirror the just-committed block
|
|
7020
|
+
// straight to the durable side so a non-replicating native node keeps
|
|
7021
|
+
// it across a restart. mirrorToDurable tracks the write for
|
|
7022
|
+
// error-surfacing.
|
|
7023
|
+
const committedHash =
|
|
7024
|
+
backboneAppend.entry.cid ?? backboneAppend.entry.hash;
|
|
7025
|
+
const committedBytes = committedHash
|
|
7026
|
+
? backbone.blocks.get(committedHash)
|
|
7027
|
+
: undefined;
|
|
7028
|
+
if (committedBytes) {
|
|
7029
|
+
durableWrapper.mirrorToDurable(committedHash!, committedBytes);
|
|
7030
|
+
}
|
|
7031
|
+
}
|
|
6619
7032
|
return {
|
|
6620
7033
|
...backboneAppend.entry,
|
|
6621
7034
|
gid: backboneAppend.coordinate.gid,
|
|
@@ -7178,12 +7591,25 @@ export class SharedLog<
|
|
|
7178
7591
|
options?.target !== "none" ||
|
|
7179
7592
|
options?.replicate === true ||
|
|
7180
7593
|
(options?.delivery !== undefined && options.delivery !== false) ||
|
|
7181
|
-
this.remoteBlocks?.localStore !== backbone.blocks ||
|
|
7182
7594
|
!this.canUseNativeBackboneResidentCoordinateState() ||
|
|
7183
7595
|
properties?.nexts?.some((nexts) => nexts.length > 0)
|
|
7184
7596
|
) {
|
|
7185
7597
|
return undefined;
|
|
7186
7598
|
}
|
|
7599
|
+
// When the durable write-through wrapper is active the log's block store is
|
|
7600
|
+
// the wrapper, not the raw wasm map, so `localStore === backbone.blocks` is
|
|
7601
|
+
// false. This batch path always commits blocks in the backbone (the
|
|
7602
|
+
// committed native batch prepare variants), so it is safe with the wrapper:
|
|
7603
|
+
// the blocks land in wasm and are mirrored to durable per-entry below. This
|
|
7604
|
+
// preserves the resident-coordinate fast batch path (and its meta.next
|
|
7605
|
+
// linking) after a reopen instead of bailing to a slow generic path.
|
|
7606
|
+
const durableWrapperActive =
|
|
7607
|
+
this.remoteBlocks?.localStore !== backbone.blocks;
|
|
7608
|
+
const durableWrapper = durableWrapperActive
|
|
7609
|
+
? (this.remoteBlocks?.localStore as unknown as {
|
|
7610
|
+
mirrorToDurable?: (cid: string, bytes: Uint8Array) => void;
|
|
7611
|
+
})
|
|
7612
|
+
: undefined;
|
|
7187
7613
|
const usesLatestDocumentContext = documentIndexes.every(
|
|
7188
7614
|
(index) => index.useLatestContext === true,
|
|
7189
7615
|
);
|
|
@@ -7310,6 +7736,21 @@ export class SharedLog<
|
|
|
7310
7736
|
for (let i = 0; i < appended.appendFacts.length; i++) {
|
|
7311
7737
|
const facts = appended.appendFacts[i]!;
|
|
7312
7738
|
const backboneAppend = backboneAppends[i]!;
|
|
7739
|
+
if (durableWrapper?.mirrorToDurable) {
|
|
7740
|
+
// The batch prepare committed each block into the wasm map; the log's
|
|
7741
|
+
// finishBlocks path is left unchanged (getBytes only) to preserve
|
|
7742
|
+
// strict-native mode, so mirror the committed block straight to durable
|
|
7743
|
+
// so a non-replicating native node keeps it across a restart. Tracked
|
|
7744
|
+
// for error-surfacing by mirrorToDurable.
|
|
7745
|
+
const committedHash =
|
|
7746
|
+
backboneAppend.entry.cid ?? backboneAppend.entry.hash;
|
|
7747
|
+
const committedBytes = committedHash
|
|
7748
|
+
? backbone.blocks.get(committedHash)
|
|
7749
|
+
: undefined;
|
|
7750
|
+
if (committedBytes) {
|
|
7751
|
+
durableWrapper.mirrorToDurable(committedHash!, committedBytes);
|
|
7752
|
+
}
|
|
7753
|
+
}
|
|
7313
7754
|
const coordinateFields = this.createCoordinateFieldsFromNativePlanFacts({
|
|
7314
7755
|
appendFacts: facts,
|
|
7315
7756
|
plan: backboneAppend.coordinate,
|
|
@@ -8629,9 +9070,28 @@ export class SharedLog<
|
|
|
8629
9070
|
this._wireSyncSession = wireSyncSession;
|
|
8630
9071
|
wireSyncSession.registerTopic(this.topic);
|
|
8631
9072
|
}
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
9073
|
+
// Block store selection:
|
|
9074
|
+
// - No native backbone: durable per-program cache (unchanged default).
|
|
9075
|
+
// - Native backbone WITHOUT a durable directory (memory-only node): the
|
|
9076
|
+
// wasm-memory native store only (unchanged prior behavior).
|
|
9077
|
+
// - Native backbone WITH a durable directory: a write-through wrapper that
|
|
9078
|
+
// mirrors the native wasm store to the SAME durable `blocks` sublevel the
|
|
9079
|
+
// default path uses, and rehydrates the wasm map from disk on open. This
|
|
9080
|
+
// is what makes native entry blocks survive a restart so heads reload.
|
|
9081
|
+
let localBlocks: NonNullable<RemoteBlocks["localStore"]>;
|
|
9082
|
+
if (this._nativeBackbone) {
|
|
9083
|
+
if (this.node.directory != null) {
|
|
9084
|
+
const durable = new AnyBlockStore(await storage.sublevel("blocks"));
|
|
9085
|
+
localBlocks = new NativeBackboneWriteThroughBlockStore(
|
|
9086
|
+
this._nativeBackbone.blocks,
|
|
9087
|
+
durable,
|
|
9088
|
+
) as unknown as NonNullable<RemoteBlocks["localStore"]>;
|
|
9089
|
+
} else {
|
|
9090
|
+
localBlocks = this._nativeBackbone.blocks;
|
|
9091
|
+
}
|
|
9092
|
+
} else {
|
|
9093
|
+
localBlocks = new AnyBlockStore(await storage.sublevel("blocks"));
|
|
9094
|
+
}
|
|
8635
9095
|
this.remoteBlocks = new RemoteBlocks({
|
|
8636
9096
|
local: localBlocks,
|
|
8637
9097
|
publish: (message, options) =>
|
|
@@ -9318,21 +9778,36 @@ export class SharedLog<
|
|
|
9318
9778
|
return undefined;
|
|
9319
9779
|
}
|
|
9320
9780
|
try {
|
|
9781
|
+
const nativeBackboneModule = await import(
|
|
9782
|
+
/* @vite-ignore */ "@peerbit/native-backbone"
|
|
9783
|
+
);
|
|
9321
9784
|
const {
|
|
9322
9785
|
createNativeBackboneCoordinatePersistence,
|
|
9323
9786
|
createNativePeerbitBackbone,
|
|
9324
|
-
} =
|
|
9787
|
+
} = nativeBackboneModule;
|
|
9325
9788
|
const backbone = await createNativePeerbitBackbone({
|
|
9326
9789
|
resolution: this.domain.resolution,
|
|
9327
9790
|
clockId: this.node.identity.publicKey.bytes,
|
|
9328
9791
|
privateKey: this.node.identity.privateKey.privateKey,
|
|
9329
9792
|
publicKey: this.node.identity.publicKey.publicKey,
|
|
9330
9793
|
});
|
|
9331
|
-
|
|
9332
|
-
|
|
9794
|
+
// Backward compatible: an explicitly supplied coordinate persistence
|
|
9795
|
+
// config always wins and is used unchanged. Otherwise, when the node
|
|
9796
|
+
// runs on durable on-disk storage, auto-derive a per-program store so
|
|
9797
|
+
// replication coordinates survive a clean stop -> restart without a
|
|
9798
|
+
// peer to re-derive from. Memory-only nodes (no directory) keep the
|
|
9799
|
+
// previous in-memory behavior.
|
|
9800
|
+
if (options.coordinatePersistence) {
|
|
9801
|
+
this._nativeBackboneCoordinatePersistence =
|
|
9802
|
+
createNativeBackboneCoordinatePersistence(
|
|
9333
9803
|
options.coordinatePersistence as RuntimeNativeBackboneCoordinatePersistenceConfig,
|
|
9334
|
-
)
|
|
9335
|
-
|
|
9804
|
+
);
|
|
9805
|
+
} else {
|
|
9806
|
+
this._nativeBackboneCoordinatePersistence =
|
|
9807
|
+
await this.createAutoDerivedCoordinatePersistence(
|
|
9808
|
+
nativeBackboneModule,
|
|
9809
|
+
);
|
|
9810
|
+
}
|
|
9336
9811
|
return backbone;
|
|
9337
9812
|
} catch (error) {
|
|
9338
9813
|
if (options.optional === false) {
|
|
@@ -9347,6 +9822,65 @@ export class SharedLog<
|
|
|
9347
9822
|
}
|
|
9348
9823
|
}
|
|
9349
9824
|
|
|
9825
|
+
/**
|
|
9826
|
+
* When the node has a durable on-disk storage directory, build a
|
|
9827
|
+
* per-program coordinate persistence store rooted under it so replication
|
|
9828
|
+
* coordinates auto-persist and survive a clean stop -> restart. Returns
|
|
9829
|
+
* `undefined` for memory-only nodes (no directory), preserving the prior
|
|
9830
|
+
* in-memory behavior.
|
|
9831
|
+
*
|
|
9832
|
+
* Namespacing: `<nodeDirectory>/coordinates/<fsSafe(log.id)>`. The log id
|
|
9833
|
+
* is the same identity used for `storage.sublevel`/`indexer.scope`
|
|
9834
|
+
* (see the `sha256Base64Sync(this.log.id)` above), but that base64 form is
|
|
9835
|
+
* not filesystem-path-safe, so the directory segment uses the hex encoding
|
|
9836
|
+
* of `this.log.id` (only `[0-9a-f]`, no `/`, `+`, or padding).
|
|
9837
|
+
*
|
|
9838
|
+
* Node vs OPFS is chosen with the same signal native-backbone uses to load
|
|
9839
|
+
* its wasm (`globalThis.process?.versions?.node`): Node gets the on-disk
|
|
9840
|
+
* store, browsers get the OPFS store.
|
|
9841
|
+
*/
|
|
9842
|
+
private async createAutoDerivedCoordinatePersistence(
|
|
9843
|
+
nativeBackboneModule: typeof import("@peerbit/native-backbone"),
|
|
9844
|
+
): Promise<NativeBackboneCoordinatePersistenceAdapter | undefined> {
|
|
9845
|
+
const directory = this.node.directory;
|
|
9846
|
+
if (directory == null) {
|
|
9847
|
+
// Memory-only node: keep prior in-memory behavior.
|
|
9848
|
+
return undefined;
|
|
9849
|
+
}
|
|
9850
|
+
const {
|
|
9851
|
+
createNativeBackboneCoordinatePersistence,
|
|
9852
|
+
NativeBackboneNodeCoordinatePersistenceStore,
|
|
9853
|
+
NativeBackboneOPFSCoordinatePersistenceStore,
|
|
9854
|
+
} = nativeBackboneModule;
|
|
9855
|
+
const namespace = toHexString(this.log.id);
|
|
9856
|
+
const isNode = !!(
|
|
9857
|
+
globalThis as { process?: { versions?: { node?: string } } }
|
|
9858
|
+
).process?.versions?.node;
|
|
9859
|
+
let store: NativeBackboneCoordinatePersistenceStore;
|
|
9860
|
+
if (isNode) {
|
|
9861
|
+
const coordinateDirectory = joinNativeCoordinateDirectory(
|
|
9862
|
+
directory,
|
|
9863
|
+
namespace,
|
|
9864
|
+
);
|
|
9865
|
+
store = new NativeBackboneNodeCoordinatePersistenceStore(
|
|
9866
|
+
coordinateDirectory,
|
|
9867
|
+
);
|
|
9868
|
+
} else {
|
|
9869
|
+
// OPFS stores address by directory parts relative to the OPFS root,
|
|
9870
|
+
// not by an absolute filesystem path, so the node `directory` only
|
|
9871
|
+
// gates activation; the per-program namespace segments keep programs
|
|
9872
|
+
// isolated within the browser's origin-private file system.
|
|
9873
|
+
store = await NativeBackboneOPFSCoordinatePersistenceStore.create({
|
|
9874
|
+
directory: ["coordinates", namespace],
|
|
9875
|
+
});
|
|
9876
|
+
}
|
|
9877
|
+
return createNativeBackboneCoordinatePersistence({
|
|
9878
|
+
store,
|
|
9879
|
+
buffered: true,
|
|
9880
|
+
flushOnAppend: true,
|
|
9881
|
+
});
|
|
9882
|
+
}
|
|
9883
|
+
|
|
9350
9884
|
private async updateTimestampOfOwnedReplicationRanges(
|
|
9351
9885
|
timestamp: number = +new Date(),
|
|
9352
9886
|
) {
|