@serfab/cadre-core 1.18.0 → 1.19.1
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/cadre-node.d.ts +65 -2
- package/dist/cadre-node.js +120 -4
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-read-retry.d.ts +19 -16
- package/dist/control-read-retry.js +22 -54
- package/dist/control-read-retry.js.map +1 -1
- package/dist/control-retry.d.ts +50 -6
- package/dist/control-retry.js +53 -6
- package/dist/control-retry.js.map +1 -1
- package/dist/control-write-retry.d.ts +24 -16
- package/dist/control-write-retry.js +58 -30
- package/dist/control-write-retry.js.map +1 -1
- package/dist/edge-node/config.d.ts +8 -2
- package/dist/edge-node/config.js +5 -1
- package/dist/edge-node/config.js.map +1 -1
- package/dist/edge-node/edge-join.js +2 -0
- package/dist/edge-node/edge-join.js.map +1 -1
- package/dist/edge-node/edge-node.d.ts +10 -0
- package/dist/edge-node/edge-node.js +52 -8
- package/dist/edge-node/edge-node.js.map +1 -1
- package/dist/edge-node/options.d.ts +11 -2
- package/dist/edge-node/options.js +11 -4
- package/dist/edge-node/options.js.map +1 -1
- package/dist/edge-node/platform.d.ts +15 -3
- package/dist/edge-node/strands.js +3 -2
- package/dist/edge-node/strands.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +8 -11
- package/dist/peer-join-backfill.js +22 -35
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/self-address-update.d.ts +13 -0
- package/dist/self-address-update.js +19 -0
- package/dist/self-address-update.js.map +1 -0
- package/dist/strand-database.d.ts +56 -0
- package/dist/strand-database.js +87 -0
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +67 -2
- package/dist/strand-instance-manager.js +105 -4
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +13 -1
- package/dist/strand-membership-reconciler.js +23 -14
- package/dist/strand-membership-reconciler.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +16 -2
- package/dist/strand-membership-writer.js +54 -8
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-offline.d.ts +131 -0
- package/dist/strand-offline.js +142 -0
- package/dist/strand-offline.js.map +1 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.js.map +1 -1
- package/package.json +8 -8
- package/src/cadre-node.ts +11456 -11326
- package/src/control-read-retry.ts +22 -58
- package/src/control-retry.ts +54 -6
- package/src/control-write-retry.ts +58 -30
- package/src/edge-node/config.ts +11 -2
- package/src/edge-node/edge-join.ts +1 -0
- package/src/edge-node/edge-node.ts +55 -6
- package/src/edge-node/options.ts +20 -4
- package/src/edge-node/platform.ts +15 -3
- package/src/edge-node/strands.ts +3 -2
- package/src/index.ts +917 -899
- package/src/peer-join-backfill.ts +33 -43
- package/src/self-address-update.ts +19 -0
- package/src/strand-database.ts +466 -352
- package/src/strand-instance-manager.ts +2210 -2092
- package/src/strand-membership-reconciler.ts +29 -14
- package/src/strand-membership-writer.ts +61 -14
- package/src/strand-offline.ts +247 -0
- package/src/types.ts +2960 -2926
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import debug from 'debug';
|
|
2
2
|
import type { IdentifyResult, Libp2p, PeerId } from '@libp2p/interface';
|
|
3
|
-
import type
|
|
3
|
+
import { jsonEncodedBytes, MAX_BLOCK_BYTES, type ActionId, type ActionRev, type BlockId, type IBlock, type IPeerNetwork } from '@optimystic/db-core';
|
|
4
4
|
import { BlockTransferClient, buildBlockTransferProtocol, type BlockCommitProof, type IRawStorage } from '@optimystic/db-p2p';
|
|
5
5
|
import { peerJoinPushBudget } from './link-budget.js';
|
|
6
6
|
import type { Libp2pNodeWithRepo } from './types.js';
|
|
@@ -182,15 +182,6 @@ import type { Libp2pNodeWithRepo } from './types.js';
|
|
|
182
182
|
// clear the pending timer in that handler.
|
|
183
183
|
|
|
184
184
|
const log = debug('sereus:cadre:peer-join-backfill');
|
|
185
|
-
const blockEncoder = new TextEncoder();
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* The block-transfer protocol's hard cap on one length-prefixed message. Mirrors
|
|
189
|
-
* `MAX_BLOCK_MESSAGE_BYTES` in `@optimystic/db-p2p`'s `protocol-limits.ts` (8 MiB), which
|
|
190
|
-
* that package does not re-export from its index — keep the two in sync if upstream ever
|
|
191
|
-
* changes it or starts exporting it.
|
|
192
|
-
*/
|
|
193
|
-
export const MAX_BLOCK_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
194
185
|
|
|
195
186
|
/**
|
|
196
187
|
* Whether an identified peer's protocol list names this network's own block-transfer
|
|
@@ -222,7 +213,11 @@ export interface PeerJoinBackfillConfig {
|
|
|
222
213
|
debounceMs?: number;
|
|
223
214
|
/** Ceiling on blocks copied in one catch-up. Default 10_000. Reaching it is LOGGED, never silent. */
|
|
224
215
|
maxBlocks?: number;
|
|
225
|
-
/**
|
|
216
|
+
/**
|
|
217
|
+
* Soft byte budget per push message, as the sum of its blocks' `jsonEncodedBytes` (the form they
|
|
218
|
+
* travel in). Default 1 MiB. A single block over it still ships alone; one over `MAX_BLOCK_BYTES`
|
|
219
|
+
* never ships (see {@link PeerJoinBackfillResult.oversized}).
|
|
220
|
+
*/
|
|
226
221
|
maxChunkBytes?: number;
|
|
227
222
|
/** Max blocks per push message. Default 64. */
|
|
228
223
|
maxChunkBlocks?: number;
|
|
@@ -328,7 +323,7 @@ export interface PeerJoinBackfillResult {
|
|
|
328
323
|
unmaterialized: number;
|
|
329
324
|
/** Not attempted because `maxBlocks` was reached. */
|
|
330
325
|
capped: number;
|
|
331
|
-
/** Skipped: a
|
|
326
|
+
/** Skipped: a block whose `jsonEncodedBytes` exceeds db-core's per-block limit, `MAX_BLOCK_BYTES`. */
|
|
332
327
|
oversized: string[];
|
|
333
328
|
/** True when the `authorizePeer` gate refused (or threw) — nothing was pushed, peer not memoized. */
|
|
334
329
|
denied: boolean;
|
|
@@ -384,8 +379,8 @@ function recordReply(memo: PeerMemo, revs: Readonly<Record<string, { rev: number
|
|
|
384
379
|
|
|
385
380
|
/** One in-flight push message being accumulated. */
|
|
386
381
|
interface Chunk {
|
|
387
|
-
|
|
388
|
-
|
|
382
|
+
/** Each block as read from this store, keyed by its id — the form `pushBlocks` sends. */
|
|
383
|
+
blocks: Record<BlockId, IBlock>;
|
|
389
384
|
meta: Record<string, { rev: number; actionId: ActionId }>;
|
|
390
385
|
/**
|
|
391
386
|
* The cohort commit proof retained for each block's pushed revision, where one exists.
|
|
@@ -400,15 +395,12 @@ interface Chunk {
|
|
|
400
395
|
* `latest` in one place.
|
|
401
396
|
*/
|
|
402
397
|
proofs: Record<string, BlockCommitProof>;
|
|
398
|
+
/** The sum of the blocks' `jsonEncodedBytes`, judged against `maxChunkBytes`. */
|
|
403
399
|
bytes: number;
|
|
404
400
|
}
|
|
405
401
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
* request, so the protocol cap must be judged against the encoded size, not the raw bytes.
|
|
409
|
-
*/
|
|
410
|
-
function base64WireBytes(rawBytes: number): number {
|
|
411
|
-
return Math.ceil(rawBytes / 3) * 4;
|
|
402
|
+
function emptyChunk(): Chunk {
|
|
403
|
+
return { blocks: {}, meta: {}, proofs: {}, bytes: 0 };
|
|
412
404
|
}
|
|
413
405
|
|
|
414
406
|
/**
|
|
@@ -828,7 +820,7 @@ export class PeerJoinBackfill {
|
|
|
828
820
|
|
|
829
821
|
const client = this.createPushClient(peerId);
|
|
830
822
|
const memo = this.memoFor(key);
|
|
831
|
-
let chunk
|
|
823
|
+
let chunk = emptyChunk();
|
|
832
824
|
let chunkFailed = false;
|
|
833
825
|
let anyChunkDelivered = false;
|
|
834
826
|
/**
|
|
@@ -840,13 +832,14 @@ export class PeerJoinBackfill {
|
|
|
840
832
|
const peerUnreachable = (): boolean => chunkFailed && !anyChunkDelivered;
|
|
841
833
|
|
|
842
834
|
const flush = async (): Promise<void> => {
|
|
843
|
-
|
|
844
|
-
const
|
|
845
|
-
|
|
835
|
+
const { blocks, meta, proofs } = chunk;
|
|
836
|
+
const ids = Object.keys(blocks);
|
|
837
|
+
if (ids.length === 0) return;
|
|
838
|
+
chunk = emptyChunk();
|
|
846
839
|
try {
|
|
847
840
|
// ONE certification value rather than two arguments, so a caller cannot pair a proof
|
|
848
841
|
// for one revision with meta for another — both were written from the same `latest`.
|
|
849
|
-
const response = await client.pushBlocks(
|
|
842
|
+
const response = await client.pushBlocks(blocks, 'replication',
|
|
850
843
|
{ blockMeta: meta, blockProofs: proofs }, {
|
|
851
844
|
dialTimeoutMs: this.config.dialTimeoutMs,
|
|
852
845
|
responseTimeoutMs: this.config.responseTimeoutMs
|
|
@@ -886,18 +879,18 @@ export class PeerJoinBackfill {
|
|
|
886
879
|
|
|
887
880
|
// Flush before adding when this block would overflow either budget. A single block
|
|
888
881
|
// larger than maxChunkBytes still ships — alone, in its own chunk.
|
|
889
|
-
|
|
890
|
-
|
|
882
|
+
const inChunk = Object.keys(chunk.blocks).length;
|
|
883
|
+
if (inChunk > 0
|
|
884
|
+
&& (inChunk + 1 > this.config.maxChunkBlocks || chunk.bytes + offer.bytes > this.config.maxChunkBytes)) {
|
|
891
885
|
await flush();
|
|
892
886
|
if (this.stopped || peerUnreachable()) break;
|
|
893
887
|
}
|
|
894
|
-
chunk.
|
|
895
|
-
chunk.buffers.push(offer.buffer);
|
|
888
|
+
chunk.blocks[blockId] = offer.block;
|
|
896
889
|
chunk.meta[blockId] = { rev: offer.latest.rev, actionId: offer.latest.actionId };
|
|
897
890
|
if (offer.proof) {
|
|
898
891
|
chunk.proofs[blockId] = offer.proof;
|
|
899
892
|
}
|
|
900
|
-
chunk.bytes += offer.
|
|
893
|
+
chunk.bytes += offer.bytes;
|
|
901
894
|
result.offered += 1;
|
|
902
895
|
}
|
|
903
896
|
|
|
@@ -916,7 +909,7 @@ export class PeerJoinBackfill {
|
|
|
916
909
|
/**
|
|
917
910
|
* Read one block for the push to `key`, or count why it is skipped in `result` and return
|
|
918
911
|
* nothing: not committed, already held by the peer (memo or its own commit signature), not
|
|
919
|
-
* materialized, or
|
|
912
|
+
* materialized, or over the per-block size limit. The proof is read for EXACTLY the revision the
|
|
920
913
|
* meta will name, from the same `latest`; absent is normal (nothing retained a proof for that
|
|
921
914
|
* revision), and the block still ships with its meta for the receiver to decide.
|
|
922
915
|
*/
|
|
@@ -925,7 +918,7 @@ export class PeerJoinBackfill {
|
|
|
925
918
|
key: string,
|
|
926
919
|
memo: PeerMemo,
|
|
927
920
|
result: PeerJoinBackfillResult
|
|
928
|
-
): Promise<{ latest: ActionRev;
|
|
921
|
+
): Promise<{ latest: ActionRev; block: IBlock; bytes: number; proof: BlockCommitProof | undefined } | undefined> {
|
|
929
922
|
const { storage } = this.deps;
|
|
930
923
|
const latest = (await storage.getMetadata(blockId))?.latest;
|
|
931
924
|
if (!latest) {
|
|
@@ -953,19 +946,16 @@ export class PeerJoinBackfill {
|
|
|
953
946
|
log('[%s] block=%s has latest rev %d but no materialized content; skipped', this.deps.label, blockId, latest.rev);
|
|
954
947
|
return undefined;
|
|
955
948
|
}
|
|
956
|
-
|
|
957
|
-
//
|
|
958
|
-
//
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
// block comes close today; if one ever can, subtract a measured envelope allowance
|
|
962
|
-
// here rather than raising the cap.
|
|
963
|
-
if (base64WireBytes(buffer.length) >= MAX_BLOCK_MESSAGE_BYTES) {
|
|
949
|
+
// A writer refuses a block over MAX_BLOCK_BYTES, and Optimystic sizes the push frame to carry
|
|
950
|
+
// one block at that limit with its meta and proof. A block past the limit grew some other way
|
|
951
|
+
// and may not fit the frame, and a push that does not fit fails on every run.
|
|
952
|
+
const bytes = jsonEncodedBytes(block);
|
|
953
|
+
if (bytes > MAX_BLOCK_BYTES) {
|
|
964
954
|
result.oversized.push(blockId);
|
|
965
|
-
log('[%s] block=%s
|
|
966
|
-
this.deps.label, blockId,
|
|
955
|
+
log('[%s] block=%s encodes to %d bytes, over the %d-byte per-block limit; skipped',
|
|
956
|
+
this.deps.label, blockId, bytes, MAX_BLOCK_BYTES);
|
|
967
957
|
return undefined;
|
|
968
958
|
}
|
|
969
|
-
return { latest,
|
|
959
|
+
return { latest, block, bytes, proof };
|
|
970
960
|
}
|
|
971
961
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PeerUpdate } from '@libp2p/interface';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether a libp2p `self:peer:update` changed the node's own addresses — the only part of
|
|
5
|
+
* the update the self record (`CadrePeer.Multiaddr`) carries.
|
|
6
|
+
*
|
|
7
|
+
* libp2p emits the event for any change to the node's own peer-store entry, and registering
|
|
8
|
+
* or unregistering a protocol handler is one (the registrar merges the node's protocol list).
|
|
9
|
+
* Cadre registers handlers on a running control node (member redemption, strand formation,
|
|
10
|
+
* the seed listener), so reacting to every event would make each of those a consensus write
|
|
11
|
+
* of an unchanged record, racing whatever write the cohort is making to `CadrePeer` at the
|
|
12
|
+
* time. An update with no `previous` is treated as a change: there is nothing to compare.
|
|
13
|
+
*/
|
|
14
|
+
export function selfAddressesChanged(update: PeerUpdate): boolean {
|
|
15
|
+
if (update.previous === undefined) return true;
|
|
16
|
+
const before = new Set(update.previous.addresses.map((address) => address.multiaddr.toString()));
|
|
17
|
+
const after = update.peer.addresses.map((address) => address.multiaddr.toString());
|
|
18
|
+
return after.length !== before.size || after.some((address) => !before.has(address));
|
|
19
|
+
}
|