@optimystic/db-p2p 1.0.0-beta.3 → 1.0.0
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/cluster/block-transfer-service.d.ts +73 -1
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +70 -3
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +16 -1
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +73 -72
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/client.d.ts +4 -6
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +8 -13
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-policy.d.ts +2 -1
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +126 -6
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +378 -50
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-proof.js +2 -2
- package/dist/src/cluster/race-resolution.d.ts +51 -7
- package/dist/src/cluster/race-resolution.d.ts.map +1 -1
- package/dist/src/cluster/race-resolution.js +71 -7
- package/dist/src/cluster/race-resolution.js.map +1 -1
- package/dist/src/cluster/rebalance-monitor.d.ts +67 -5
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +120 -26
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +34 -47
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +8 -6
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/membership-source.js +1 -1
- package/dist/src/cohort-topic/membership-source.js.map +1 -1
- package/dist/src/cohort-topic/peer-codec.d.ts.map +1 -1
- package/dist/src/cohort-topic/peer-codec.js +5 -2
- package/dist/src/cohort-topic/peer-codec.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +24 -3
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +36 -3
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/cohort-topic/topic-router.d.ts +9 -0
- package/dist/src/cohort-topic/topic-router.d.ts.map +1 -1
- package/dist/src/cohort-topic/topic-router.js +12 -2
- package/dist/src/cohort-topic/topic-router.js.map +1 -1
- package/dist/src/dispute/client.d.ts.map +1 -1
- package/dist/src/dispute/client.js +15 -3
- package/dist/src/dispute/client.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +78 -17
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +234 -147
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +35 -1
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +151 -26
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/libp2p-node-rn.d.ts +2 -2
- package/dist/src/libp2p-node-rn.d.ts.map +1 -1
- package/dist/src/libp2p-node-rn.js.map +1 -1
- package/dist/src/libp2p-node.d.ts +2 -2
- package/dist/src/libp2p-node.d.ts.map +1 -1
- package/dist/src/libp2p-node.js.map +1 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +6 -1
- package/dist/src/logger.js.map +1 -1
- package/dist/src/matchmaking/query-transport.d.ts +3 -4
- package/dist/src/matchmaking/query-transport.d.ts.map +1 -1
- package/dist/src/matchmaking/query-transport.js +9 -9
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +10 -5
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +11 -2
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/network/relay-reservation.d.ts +170 -0
- package/dist/src/network/relay-reservation.d.ts.map +1 -0
- package/dist/src/network/relay-reservation.js +527 -0
- package/dist/src/network/relay-reservation.js.map +1 -0
- package/dist/src/pend-validation.d.ts +3 -2
- package/dist/src/pend-validation.d.ts.map +1 -1
- package/dist/src/pend-validation.js +3 -2
- package/dist/src/pend-validation.js.map +1 -1
- package/dist/src/protocol-client.d.ts +8 -1
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +11 -0
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/reactivity/forwarder-host.d.ts +2 -1
- package/dist/src/reactivity/forwarder-host.d.ts.map +1 -1
- package/dist/src/reactivity/forwarder-host.js +2 -1
- package/dist/src/reactivity/forwarder-host.js.map +1 -1
- package/dist/src/reactivity/origination-manager.d.ts +3 -3
- package/dist/src/reactivity/origination-manager.js +3 -3
- package/dist/src/reactivity/recover-transport.d.ts +22 -14
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
- package/dist/src/reactivity/recover-transport.js +32 -19
- package/dist/src/reactivity/recover-transport.js.map +1 -1
- package/dist/src/reactivity/subscription-manager.d.ts +2 -2
- package/dist/src/reactivity/topic-bytes.d.ts +6 -7
- package/dist/src/reactivity/topic-bytes.d.ts.map +1 -1
- package/dist/src/reactivity/topic-bytes.js +8 -9
- package/dist/src/reactivity/topic-bytes.js.map +1 -1
- package/dist/src/repo/client.d.ts +5 -2
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +49 -40
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +77 -5
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +131 -20
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +238 -122
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +625 -472
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/i-under-replication-ledger.d.ts +87 -0
- package/dist/src/repo/i-under-replication-ledger.d.ts.map +1 -0
- package/dist/src/repo/i-under-replication-ledger.js +2 -0
- package/dist/src/repo/i-under-replication-ledger.js.map +1 -0
- package/dist/src/repo/kv-under-replication-ledger.d.ts +91 -0
- package/dist/src/repo/kv-under-replication-ledger.d.ts.map +1 -0
- package/dist/src/repo/kv-under-replication-ledger.js +287 -0
- package/dist/src/repo/kv-under-replication-ledger.js.map +1 -0
- package/dist/src/repo/responsibility.d.ts +37 -0
- package/dist/src/repo/responsibility.d.ts.map +1 -0
- package/dist/src/repo/responsibility.js +37 -0
- package/dist/src/repo/responsibility.js.map +1 -0
- package/dist/src/repo/service.d.ts +44 -15
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +64 -27
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/repo/stuck-reservation.d.ts +127 -0
- package/dist/src/repo/stuck-reservation.d.ts.map +1 -0
- package/dist/src/repo/stuck-reservation.js +179 -0
- package/dist/src/repo/stuck-reservation.js.map +1 -0
- package/dist/src/repo/under-replication-drain.d.ts +208 -0
- package/dist/src/repo/under-replication-drain.d.ts.map +1 -0
- package/dist/src/repo/under-replication-drain.js +461 -0
- package/dist/src/repo/under-replication-drain.js.map +1 -0
- package/dist/src/rn.d.ts +5 -0
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +5 -0
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/block-latch.d.ts +3 -2
- package/dist/src/storage/block-latch.d.ts.map +1 -1
- package/dist/src/storage/block-latch.js +7 -4
- package/dist/src/storage/block-latch.js.map +1 -1
- package/dist/src/storage/block-storage.d.ts +45 -3
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +156 -9
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +57 -5
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-block-storage.js.map +1 -1
- package/dist/src/storage/pending-claim.d.ts +167 -0
- package/dist/src/storage/pending-claim.d.ts.map +1 -0
- package/dist/src/storage/pending-claim.js +156 -0
- package/dist/src/storage/pending-claim.js.map +1 -0
- package/dist/src/storage/raw-store-codec.d.ts +13 -0
- package/dist/src/storage/raw-store-codec.d.ts.map +1 -1
- package/dist/src/storage/raw-store-codec.js +24 -3
- package/dist/src/storage/raw-store-codec.js.map +1 -1
- package/dist/src/storage/restoration-coordinator.d.ts.map +1 -1
- package/dist/src/storage/restoration-coordinator.js +2 -1
- package/dist/src/storage/restoration-coordinator.js.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.d.ts.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.js +3 -3
- package/dist/src/storage/ring-shift-coordinator.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +122 -5
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +245 -68
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/storage/struct.d.ts +58 -0
- package/dist/src/storage/struct.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +77 -11
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +226 -100
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +27 -3
- package/src/cluster/block-transfer-service.ts +132 -4
- package/src/cluster/block-transfer.ts +79 -84
- package/src/cluster/client.ts +115 -119
- package/src/cluster/cluster-policy.ts +2 -1
- package/src/cluster/cluster-repo.ts +422 -56
- package/src/cluster/commit-proof.ts +2 -2
- package/src/cluster/race-resolution.ts +76 -7
- package/src/cluster/rebalance-monitor.ts +677 -539
- package/src/cluster/spread-on-churn.ts +34 -54
- package/src/cohort-topic/host.ts +8 -6
- package/src/cohort-topic/membership-source.ts +1 -1
- package/src/cohort-topic/peer-codec.ts +34 -31
- package/src/cohort-topic/stream-util.ts +39 -4
- package/src/cohort-topic/topic-router.ts +12 -2
- package/src/dispute/client.ts +17 -7
- package/src/index.ts +64 -59
- package/src/libp2p-key-network.ts +1377 -1260
- package/src/libp2p-node-base.ts +186 -27
- package/src/libp2p-node-rn.ts +31 -30
- package/src/libp2p-node.ts +37 -36
- package/src/logger.ts +6 -1
- package/src/matchmaking/query-transport.ts +9 -9
- package/src/network/network-manager-service.ts +16 -5
- package/src/network/relay-reservation.ts +636 -0
- package/src/pend-validation.ts +3 -2
- package/src/protocol-client.ts +13 -1
- package/src/reactivity/forwarder-host.ts +2 -1
- package/src/reactivity/origination-manager.ts +3 -3
- package/src/reactivity/recover-transport.ts +39 -22
- package/src/reactivity/subscription-manager.ts +2 -2
- package/src/reactivity/topic-bytes.ts +35 -37
- package/src/repo/client.ts +172 -173
- package/src/repo/cluster-coordinator.ts +145 -19
- package/src/repo/coordinator-repo.ts +652 -505
- package/src/repo/i-under-replication-ledger.ts +89 -0
- package/src/repo/kv-under-replication-ledger.ts +302 -0
- package/src/repo/responsibility.ts +48 -0
- package/src/repo/service.ts +76 -39
- package/src/repo/stuck-reservation.ts +224 -0
- package/src/repo/under-replication-drain.ts +582 -0
- package/src/rn.ts +74 -69
- package/src/storage/block-latch.ts +7 -4
- package/src/storage/block-storage.ts +161 -10
- package/src/storage/i-block-storage.ts +60 -5
- package/src/storage/pending-claim.ts +200 -0
- package/src/storage/raw-store-codec.ts +26 -3
- package/src/storage/restoration-coordinator.ts +212 -211
- package/src/storage/ring-shift-coordinator.ts +271 -271
- package/src/storage/storage-repo.ts +280 -72
- package/src/storage/struct.ts +58 -0
- package/src/testing/mesh-harness.ts +293 -112
package/readme.md
CHANGED
|
@@ -390,7 +390,10 @@ const node = await createLibp2pNode({
|
|
|
390
390
|
dropped from it, and `appendAnnounceAddrs` is ignored while it is non-empty. Use
|
|
391
391
|
`appendAnnounceAddrs` alone when the automatically discovered addresses are still worth advertising.
|
|
392
392
|
Both are straight passthroughs to libp2p's `addresses.announce` / `addresses.appendAnnounce`; an
|
|
393
|
-
empty array means "unset".
|
|
393
|
+
empty array means "unset". Because it hides relayed addresses, `announceAddrs` cannot be combined
|
|
394
|
+
with a listen address naming a relay (the circuit address would never be advertised, so the
|
|
395
|
+
reservation could never be confirmed); node creation rejects that combination before anything
|
|
396
|
+
starts. Use `appendAnnounceAddrs` for such a node.
|
|
394
397
|
|
|
395
398
|
## Usage Examples
|
|
396
399
|
|
|
@@ -608,6 +611,13 @@ const node = await createLibp2pNode({
|
|
|
608
611
|
});
|
|
609
612
|
```
|
|
610
613
|
|
|
614
|
+
To be reachable through a relay instead, listen on `<relay address>/p2p-circuit` (the relay's
|
|
615
|
+
full dial address, `/p2p/<relay id>` included). The node reserves a slot on that relay at start and
|
|
616
|
+
keeps it itself afterwards, re-reserving after the relay restarts, after the connection to it drops,
|
|
617
|
+
and across libp2p's routine renewal (`superviseRelayReservation` in `src/network/relay-reservation.ts`);
|
|
618
|
+
node creation still rejects if the relay cannot be reserved at all. A bare `/p2p-circuit` listen
|
|
619
|
+
address is left to the host, which then owns that reservation.
|
|
620
|
+
|
|
611
621
|
#### React Native
|
|
612
622
|
|
|
613
623
|
For React Native, import from `@optimystic/db-p2p/rn` instead of the root entrypoint.
|
|
@@ -640,12 +650,13 @@ before any other imports).
|
|
|
640
650
|
|-----|-------------|-------|
|
|
641
651
|
| `crypto.getRandomValues()` | @noble/hashes, @libp2p/crypto | RN 0.76+ New Architecture provides natively; fallback via e.g. `react-native-get-random-values` |
|
|
642
652
|
| `crypto.subtle.digest()` | multiformats/hashes/sha2-browser | Async SHA-256/SHA-512 — implement via @noble/hashes |
|
|
643
|
-
| `structuredClone()` | @optimystic/db-core | JSON round-trip is sufficient |
|
|
653
|
+
| `structuredClone()` | @optimystic/db-core | JSON round-trip is sufficient. Still required: db-core calls it on ordinary read and write paths (applying an update to a block, for one), though never while a module loads |
|
|
644
654
|
| `Promise.withResolvers()` | @libp2p/utils, @chainsafe/libp2p-yamux, it-queue | ES2024 — simple shim |
|
|
645
655
|
| `AbortSignal.prototype.throwIfAborted()` | libp2p, @libp2p/circuit-relay-v2, it-pushable | DOM spec addition — simple shim |
|
|
646
656
|
| Timer `.ref()` / `.unref()` | @optimystic/db-p2p, undici | Wrap Hermes numeric timer IDs in objects with no-op `.ref()`/`.unref()` methods; patch `clearTimeout`/`clearInterval` to unwrap |
|
|
647
657
|
| `Event`, `CustomEvent`, `EventTarget` | libp2p, @libp2p/interface | Custom shim or npm `event-target-polyfill` |
|
|
648
658
|
| `Intl.PluralRules` | moat-maker | English-only ordinal/cardinal shim is sufficient |
|
|
659
|
+
| `TextDecoder` | multiformats, cborg, @optimystic/db-core, @optimystic/db-p2p, @optimystic/db-p2p-storage-rn | **multiformats and cborg construct it at module load, so without it the app fails at startup.** Optimystic's own packages construct it only on first use (see below), which does not make it optional. Expo SDK 52+ provides it; bare React Native's Hermes does not. A UTF-8-only shim is sufficient |
|
|
649
660
|
| `WebSocket.prototype.bufferedAmount` | @libp2p/websockets | **RN declares the field but never assigns it, so it reads `undefined`.** See below — without this every WebSocket write hangs. |
|
|
650
661
|
|
|
651
662
|
**`WebSocket.bufferedAmount` deserves its own note, because the symptom does not look like a
|
|
@@ -675,11 +686,24 @@ mechanism.
|
|
|
675
686
|
| `stream` / `node:stream` | libp2p | `readable-stream` (npm) |
|
|
676
687
|
| `buffer` / `node:buffer` | libp2p, multiformats | `buffer` (npm) |
|
|
677
688
|
|
|
689
|
+
These four rows are known to be enough for a React Native build to accept the code: `yarn check:rn`
|
|
690
|
+
(the private `packages/rn-bundle-check` workspace) bundles the `/rn` entry with exactly these aliases
|
|
691
|
+
and the React Native 0.83 toolchain, then compiles the bundle with legacy Hermes. Its
|
|
692
|
+
`test/shim-table-parity.test.mjs` fails if its aliases and this table drift apart, so change them
|
|
693
|
+
together. It never runs the bundle, so the global polyfills above are not verified by it.
|
|
694
|
+
|
|
678
695
|
**Built-in (no polyfill needed):**
|
|
679
696
|
- `TextEncoder` — built-in to Hermes
|
|
680
|
-
- `TextDecoder` — built-in to Expo SDK 52+ (UTF-8 only)
|
|
681
697
|
- `BigInt` — built-in to Hermes since RN 0.70
|
|
682
698
|
|
|
699
|
+
Optimystic's own code does not require a global `Buffer` — it encodes with `uint8arrays`, and lint (`no-restricted-globals` in `eslint.config.js`) keeps it that way.
|
|
700
|
+
|
|
701
|
+
Optimystic's own code never touches `TextDecoder` or `structuredClone` while a module is loading: each `TextDecoder` is built the first time something decodes, and then reused. So importing `@optimystic/db-p2p/rn` (or db-core, or db-p2p-storage-rn) does not throw *because of Optimystic* on a runtime whose polyfill is not installed yet. **Both are still required polyfills**, and should still be installed before any other import: the third-party dependencies in the table construct `TextDecoder` at load and fail startup without it, and Optimystic needs both as soon as it decodes or applies an update. `NO_MODULE_SCOPE_TEXT_DECODER` in `eslint.config.js` keeps `TextDecoder` construction out of module scope in every package, and `test/module-load-globals.spec.ts` checks both globals at runtime (for the `/rn` entry's load path only, which includes db-core but not db-p2p-storage-rn). The spec loads the `/rn` entry in a fresh process and fails if any first-party module uses either global during the load.
|
|
702
|
+
|
|
703
|
+
Optimystic's own code also does not call `AbortSignal.timeout` or `AbortSignal.any`, call `Promise.withResolvers`, or construct `DOMException` — none of those are guaranteed under Hermes. (It does call `signal.throwIfAborted()`, which stays a required host polyfill per the table above because libp2p needs it anyway.) Every deadline is an explicit `AbortController` plus a timer, cleared on every exit path (see `dialRelay` in `src/network/relay-reservation.ts` and `RepoClient.processRepoMessage` in `src/repo/client.ts`, which hand-rolls the "abort on any of several signals" case rather than using `AbortSignal.any` or the `any-signal` package — the code comment there explains why). `no-restricted-syntax` in `eslint.config.js` keeps it that way.
|
|
704
|
+
|
|
705
|
+
**Dormant, not yet reached:** `crypto.subtle` on React Native provides only `digest`. `@libp2p/crypto`'s ECDSA/RSA import-export and `@libp2p/keychain`'s AES-GCM call other `crypto.subtle` methods and would throw if a host application reached them; nothing in Optimystic's own code path does — peer identity uses Ed25519 through `@noble/curves`/`@noble/hashes`, not the platform `crypto.subtle`. Revisit if a host ever adds a feature that reaches `@libp2p/keychain` or a non-Ed25519 key type on React Native.
|
|
706
|
+
|
|
683
707
|
See the [Sereus reference-app-rn](https://github.com/gotchoices/sereus/tree/master/packages/reference-app-rn/polyfills) for working polyfill implementations.
|
|
684
708
|
|
|
685
709
|
### Ring Transitions
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Connection, Startable, Stream } from '@libp2p/interface';
|
|
2
|
-
import type { IRepo, PeerId, IPeerNetwork, ActionId, ActionRev, GetBlockResult, IBlock, BlockId } from '@optimystic/db-core';
|
|
2
|
+
import type { IRepo, PeerId, IPeerNetwork, ActionId, ActionRev, GetBlockResult, IBlock, BlockId, BlockUnavailableReason, CollectionId } from '@optimystic/db-core';
|
|
3
|
+
import { peerIdFromString } from '@libp2p/peer-id';
|
|
3
4
|
import { pipe } from 'it-pipe';
|
|
4
5
|
import * as lp from 'it-length-prefixed';
|
|
5
6
|
import { fromString as u8FromString } from 'uint8arrays/from-string';
|
|
@@ -327,7 +328,7 @@ export class BlockTransferService implements Startable {
|
|
|
327
328
|
for (const blockId of request.blockIds) {
|
|
328
329
|
const blockResult = result[blockId];
|
|
329
330
|
if (blockResult?.block) {
|
|
330
|
-
blocks[blockId] =
|
|
331
|
+
blocks[blockId] = u8ToString(u8FromString(JSON.stringify(blockResult.block), 'utf8'), 'base64pad');
|
|
331
332
|
} else {
|
|
332
333
|
missing.push(blockId);
|
|
333
334
|
}
|
|
@@ -370,7 +371,7 @@ export class BlockTransferService implements Startable {
|
|
|
370
371
|
// Decode + parse the wire payload into an IBlock.
|
|
371
372
|
let block: IBlock;
|
|
372
373
|
try {
|
|
373
|
-
block = JSON.parse(
|
|
374
|
+
block = JSON.parse(u8ToString(u8FromString(data, 'base64pad'), 'utf8')) as IBlock;
|
|
374
375
|
} catch {
|
|
375
376
|
missing.push(blockId);
|
|
376
377
|
continue;
|
|
@@ -512,7 +513,7 @@ export class BlockTransferClient extends ProtocolClient {
|
|
|
512
513
|
): Promise<BlockTransferResponse> {
|
|
513
514
|
const blockData: Record<string, string> = {};
|
|
514
515
|
for (let i = 0; i < blockIds.length; i++) {
|
|
515
|
-
blockData[blockIds[i]!] =
|
|
516
|
+
blockData[blockIds[i]!] = u8ToString(blockDataBuffers[i]!, 'base64pad');
|
|
516
517
|
}
|
|
517
518
|
const request: BlockTransferRequest = {
|
|
518
519
|
type: 'push', blockIds, reason, blockData,
|
|
@@ -522,3 +523,130 @@ export class BlockTransferClient extends ProtocolClient {
|
|
|
522
523
|
return await this.processMessage<BlockTransferResponse>(request, this.protocol, { ...options, maxDataLength: MAX_BLOCK_MESSAGE_BYTES });
|
|
523
524
|
}
|
|
524
525
|
}
|
|
526
|
+
|
|
527
|
+
// --- The one push loop ---
|
|
528
|
+
|
|
529
|
+
/** Why one peer did not confirm a pushed block. */
|
|
530
|
+
export type PushRefusal = {
|
|
531
|
+
peerId: string;
|
|
532
|
+
/**
|
|
533
|
+
* `rejected`: the peer ANSWERED and listed the block as missing — it did not persist it
|
|
534
|
+
* (uncertified under its `requirePushCertificate`, unparseable, or a persist fault; the receiver
|
|
535
|
+
* logs which). `unreachable`: the push threw — a dial failure, a deadline, a stream error.
|
|
536
|
+
*/
|
|
537
|
+
reason: 'rejected' | 'unreachable';
|
|
538
|
+
/** The thrown error's message, for `unreachable`. */
|
|
539
|
+
error?: string;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* What {@link pushBlockToPeers} found and did. `no-local-data` and `unavailable` mean nothing was
|
|
544
|
+
* pushed: the first is "this node holds no revision of the block", the second is "this node could
|
|
545
|
+
* not find out" (a failed restore, an unmaterializable history) — a caller that prunes on the first
|
|
546
|
+
* must NOT prune on the second, or it drops a block it may well hold.
|
|
547
|
+
*/
|
|
548
|
+
export type PushBlockOutcome =
|
|
549
|
+
| { status: 'no-local-data' }
|
|
550
|
+
| { status: 'unavailable'; reason: BlockUnavailableReason }
|
|
551
|
+
| {
|
|
552
|
+
status: 'pushed';
|
|
553
|
+
/** The revision pushed — the source's `state.latest`. Absent for a repo that reports none. */
|
|
554
|
+
latest?: ActionRev;
|
|
555
|
+
/** From the pushed block's header, when it names a collection. */
|
|
556
|
+
collectionId?: CollectionId;
|
|
557
|
+
/** Whether a cohort commit proof travelled with the block. Without one a receiver running
|
|
558
|
+
* the default `requirePushCertificate` rejects it, so a `false` here with every peer
|
|
559
|
+
* `rejected` is "cannot place", not "cannot reach". */
|
|
560
|
+
certified: boolean;
|
|
561
|
+
/** Peers that hold the block now: each answered without listing it as missing. */
|
|
562
|
+
confirmed: string[];
|
|
563
|
+
refusals: PushRefusal[];
|
|
564
|
+
/** Peers not pushed to because {@link PushBlockOptions.stopAfterConfirmed} was reached first. */
|
|
565
|
+
skipped: string[];
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
export interface PushBlockOptions {
|
|
569
|
+
/** Default `'replication'`. */
|
|
570
|
+
reason?: BlockTransferRequest['reason'];
|
|
571
|
+
/** The `/optimystic/<networkName>` prefix the receivers register their handler under. */
|
|
572
|
+
protocolPrefix?: string;
|
|
573
|
+
/** Stop once this many distinct peers have confirmed; the rest are reported `skipped`. Default:
|
|
574
|
+
* every peer is pushed to. */
|
|
575
|
+
stopAfterConfirmed?: number;
|
|
576
|
+
/** Per-peer deadlines forwarded to {@link BlockTransferClient.pushBlocks}; omitted = uncapped. */
|
|
577
|
+
dialTimeoutMs?: number;
|
|
578
|
+
responseTimeoutMs?: number;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const textEncoder = new TextEncoder();
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Push ONE locally held block to `peerIds`, in order: read it unpinned from `repo`, build its
|
|
585
|
+
* {@link PushCertification} from that same read, push it to each peer, and read each answer the one
|
|
586
|
+
* way that is true — a peer holds the block only when its response does NOT list the block as
|
|
587
|
+
* missing, because `handlePush` omits a block from `missing` only after persisting it (or already
|
|
588
|
+
* holding that revision). A round trip that returns is not a landed replica; a round trip that
|
|
589
|
+
* throws is not a refusal.
|
|
590
|
+
*
|
|
591
|
+
* This is the whole read-certify-push-interpret sequence every sender runs — the rebalance
|
|
592
|
+
* handoff and confirm (`BlockTransferCoordinator`), spread-on-churn, and the under-replication
|
|
593
|
+
* drain — so a change to how a block is placed is made here once. Retry, stopping condition and
|
|
594
|
+
* deadlines are the caller's: `stopAfterConfirmed` for "until one owner accepts" or "until the
|
|
595
|
+
* floor", the two deadlines for a sequential loop that must not hang on a silent peer, and the
|
|
596
|
+
* caller wraps the call in its own retry. It logs nothing per peer; the outcome carries what each
|
|
597
|
+
* caller's own diagnostics say.
|
|
598
|
+
*
|
|
599
|
+
* `repo` is the node's OWN store, so the read never consults the cluster and the certification
|
|
600
|
+
* comes from the proof this node retained; a repo without a proof accessor pushes meta-only
|
|
601
|
+
* (`certified: false`), which a default-configured receiver refuses.
|
|
602
|
+
*/
|
|
603
|
+
export async function pushBlockToPeers(
|
|
604
|
+
repo: ArchiveServingRepo,
|
|
605
|
+
peerNetwork: IPeerNetwork,
|
|
606
|
+
blockId: BlockId,
|
|
607
|
+
peerIds: readonly string[],
|
|
608
|
+
options: PushBlockOptions = {}
|
|
609
|
+
): Promise<PushBlockOutcome> {
|
|
610
|
+
const result = (await repo.get({ blockIds: [blockId] }))[blockId];
|
|
611
|
+
if (result?.unavailable !== undefined) {
|
|
612
|
+
return { status: 'unavailable', reason: result.unavailable };
|
|
613
|
+
}
|
|
614
|
+
if (!result?.block) {
|
|
615
|
+
return { status: 'no-local-data' };
|
|
616
|
+
}
|
|
617
|
+
const blockData = textEncoder.encode(JSON.stringify(result.block));
|
|
618
|
+
const certification = await sourceBlockCertification(repo, blockId, result);
|
|
619
|
+
const reason = options.reason ?? 'replication';
|
|
620
|
+
const deadlines = { dialTimeoutMs: options.dialTimeoutMs, responseTimeoutMs: options.responseTimeoutMs };
|
|
621
|
+
const stopAfter = options.stopAfterConfirmed ?? Number.POSITIVE_INFINITY;
|
|
622
|
+
|
|
623
|
+
const confirmed: string[] = [];
|
|
624
|
+
const refusals: PushRefusal[] = [];
|
|
625
|
+
const skipped: string[] = [];
|
|
626
|
+
for (const peerIdStr of peerIds) {
|
|
627
|
+
if (confirmed.length >= stopAfter) {
|
|
628
|
+
skipped.push(peerIdStr);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
try {
|
|
632
|
+
const client = new BlockTransferClient(peerIdFromString(peerIdStr), peerNetwork, options.protocolPrefix);
|
|
633
|
+
const response = await client.pushBlocks([blockId], [blockData], reason, certification, deadlines);
|
|
634
|
+
if (response.missing.includes(blockId)) {
|
|
635
|
+
refusals.push({ peerId: peerIdStr, reason: 'rejected' });
|
|
636
|
+
} else {
|
|
637
|
+
confirmed.push(peerIdStr);
|
|
638
|
+
}
|
|
639
|
+
} catch (err) {
|
|
640
|
+
refusals.push({ peerId: peerIdStr, reason: 'unreachable', error: err instanceof Error ? err.message : String(err) });
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return {
|
|
644
|
+
status: 'pushed',
|
|
645
|
+
latest: result.state?.latest,
|
|
646
|
+
collectionId: result.block.header?.collectionId,
|
|
647
|
+
certified: certification.blockProofs !== undefined,
|
|
648
|
+
confirmed,
|
|
649
|
+
refusals,
|
|
650
|
+
skipped
|
|
651
|
+
};
|
|
652
|
+
}
|
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
import type { IPeerNetwork } from '@optimystic/db-core';
|
|
2
|
-
import { peerIdFromString } from '@libp2p/peer-id';
|
|
3
2
|
import type { PartitionDetector } from './partition-detector.js';
|
|
4
3
|
import type { RestorationCoordinator } from '../storage/restoration-coordinator.js';
|
|
5
4
|
import type { ProofRetainingRepo } from '../storage/block-archive.js';
|
|
6
|
-
import {
|
|
5
|
+
import { pushBlockToPeers, type PushBlockOutcome, type PushRefusal } from './block-transfer-service.js';
|
|
7
6
|
import type { GrowthOutcome, RebalanceEvent } from './rebalance-monitor.js';
|
|
8
7
|
import { createLogger } from '../logger.js';
|
|
9
8
|
|
|
10
9
|
const log = createLogger('block-transfer');
|
|
11
10
|
|
|
11
|
+
/** One line per peer that did not confirm, under the calling path's tag (`push` / `confirm`). */
|
|
12
|
+
function logRefusals(path: 'push' | 'confirm', blockId: string, refusals: readonly PushRefusal[]): void {
|
|
13
|
+
for (const refusal of refusals) {
|
|
14
|
+
if (refusal.reason === 'unreachable') {
|
|
15
|
+
log('%s:peer-error block=%s peer=%s err=%s', path, blockId, refusal.peerId, refusal.error);
|
|
16
|
+
} else {
|
|
17
|
+
log('%s:peer-rejected block=%s peer=%s (receiver did not persist)', path, blockId, refusal.peerId);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
export interface BlockTransferConfig {
|
|
13
23
|
/** Max concurrent transfers. Default: 4 */
|
|
14
24
|
maxConcurrency?: number;
|
|
15
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Timeout per block transfer (ms). Default: 30000. A pull is bounded as a whole. A push or
|
|
27
|
+
* confirm is bounded PER PEER, as two deadlines of this length: one on the dial and one on the
|
|
28
|
+
* reply (the `BlockTransferClient.pushBlocks` deadlines, which abort the dial and tear down a
|
|
29
|
+
* silent stream rather than leaving them running).
|
|
30
|
+
*/
|
|
16
31
|
transferTimeoutMs?: number;
|
|
17
32
|
/** Retry attempts for failed transfers. Default: 2 */
|
|
18
33
|
maxRetries?: number;
|
|
@@ -347,52 +362,29 @@ export class BlockTransferCoordinator {
|
|
|
347
362
|
this.inFlight.add(key);
|
|
348
363
|
|
|
349
364
|
try {
|
|
365
|
+
const owners = newOwners.get(blockId);
|
|
366
|
+
if (!owners || owners.length === 0) {
|
|
367
|
+
failed.push(blockId);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
350
371
|
for (let attempt = 0; ; attempt++) {
|
|
351
372
|
await this.acquireSemaphore();
|
|
352
373
|
let pushed = false;
|
|
353
374
|
try {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const result = await this.repo.get({ blockIds: [blockId] });
|
|
362
|
-
const blockResult = result[blockId];
|
|
363
|
-
if (!blockResult?.block) {
|
|
364
|
-
log('push:no-local-data block=%s', blockId);
|
|
375
|
+
// The shared read-certify-push loop, stopping at the FIRST new owner that accepts. A
|
|
376
|
+
// receiver running the default `requirePushCertificate` rejects a push with no proof,
|
|
377
|
+
// so a block whose proof this node never retained (pre-proof history, a diverged
|
|
378
|
+
// commit) simply fails to place here and is retried/kept as today.
|
|
379
|
+
const outcome = await this.pushBlock(blockId, owners, 1);
|
|
380
|
+
if (outcome.status !== 'pushed') {
|
|
381
|
+
log('push:no-local-data block=%s status=%s', blockId, outcome.status);
|
|
365
382
|
failed.push(blockId);
|
|
366
383
|
return;
|
|
367
384
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// read. A receiver running the default `requirePushCertificate` rejects a push with
|
|
372
|
-
// no proof, so a block whose proof this node never retained (pre-proof history, a
|
|
373
|
-
// diverged commit) simply fails to place here and is retried/kept as today.
|
|
374
|
-
const certification = await sourceBlockCertification(this.repo, blockId, blockResult);
|
|
375
|
-
|
|
376
|
-
// Push to at least one new owner
|
|
377
|
-
for (const ownerPeerIdStr of owners) {
|
|
378
|
-
try {
|
|
379
|
-
const peerId = peerIdFromString(ownerPeerIdStr);
|
|
380
|
-
const client = new BlockTransferClient(peerId, this.peerNetwork, this.protocolPrefix);
|
|
381
|
-
const response = await this.withTimeout(
|
|
382
|
-
client.pushBlocks([blockId], [blockData], 'rebalance', certification),
|
|
383
|
-
this.transferTimeoutMs
|
|
384
|
-
);
|
|
385
|
-
|
|
386
|
-
if (response && !response.missing.includes(blockId)) {
|
|
387
|
-
pushed = true;
|
|
388
|
-
log('push:ok block=%s peer=%s', blockId, ownerPeerIdStr);
|
|
389
|
-
break;
|
|
390
|
-
}
|
|
391
|
-
} catch (err) {
|
|
392
|
-
log('push:peer-error block=%s peer=%s err=%s',
|
|
393
|
-
blockId, ownerPeerIdStr, (err as Error).message);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
385
|
+
logRefusals('push', blockId, outcome.refusals);
|
|
386
|
+
pushed = outcome.confirmed.length > 0;
|
|
387
|
+
if (pushed) log('push:ok block=%s peer=%s', blockId, outcome.confirmed[0]);
|
|
396
388
|
} finally {
|
|
397
389
|
this.releaseSemaphore();
|
|
398
390
|
}
|
|
@@ -449,47 +441,28 @@ export class BlockTransferCoordinator {
|
|
|
449
441
|
await this.acquireSemaphore();
|
|
450
442
|
let confirmCount = 0;
|
|
451
443
|
try {
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
444
|
+
// The shared read-certify-push loop, once per attempt, stopping once `floor` DISTINCT
|
|
445
|
+
// owners hold a current replica. A confirming holder either takes a certified replica
|
|
446
|
+
// or reports the block missing (see executePush).
|
|
447
|
+
const outcome = await this.pushBlock(blockId, candidateOwners, floor);
|
|
448
|
+
if (outcome.status !== 'pushed') {
|
|
449
|
+
// No local bytes to prove replication with — cannot confirm; keep serving. An
|
|
450
|
+
// `unavailable` read (this node could not find out what it holds) is folded in
|
|
451
|
+
// here as it always was: neither outcome can confirm anything.
|
|
452
|
+
log('confirm:no-local-data block=%s status=%s', blockId, outcome.status);
|
|
458
453
|
return { confirmed: false, confirmedPeers: allConfirmedPeers, noLocalData: true };
|
|
459
454
|
}
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if (confirmedPeers.size >= floor) break;
|
|
470
|
-
try {
|
|
471
|
-
const peerId = peerIdFromString(ownerPeerIdStr);
|
|
472
|
-
const client = new BlockTransferClient(peerId, this.peerNetwork, this.protocolPrefix);
|
|
473
|
-
const response = await this.withTimeout(
|
|
474
|
-
client.pushBlocks([blockId], [blockData], 'rebalance', certification),
|
|
475
|
-
this.transferTimeoutMs
|
|
476
|
-
);
|
|
477
|
-
if (response && !response.missing.includes(blockId)) {
|
|
478
|
-
confirmedPeers.add(ownerPeerIdStr);
|
|
479
|
-
// NOTE: allConfirmedPeers never un-records a peer. A holder that confirms in one
|
|
480
|
-
// round and reports `missing` in a later one stays recorded, on the reasoning that
|
|
481
|
-
// `handlePush` answers non-missing only after persisting. If the receiver ever
|
|
482
|
-
// gains a path that drops a just-persisted block (an eviction sweep, a rejected
|
|
483
|
-
// revision), the growth arm would record a peer that no longer holds a replica —
|
|
484
|
-
// intersect against the LAST round's confirmedPeers instead of unioning.
|
|
485
|
-
allConfirmedPeers.add(ownerPeerIdStr);
|
|
486
|
-
}
|
|
487
|
-
} catch (err) {
|
|
488
|
-
log('confirm:peer-error block=%s peer=%s err=%s',
|
|
489
|
-
blockId, ownerPeerIdStr, (err as Error).message);
|
|
490
|
-
}
|
|
455
|
+
logRefusals('confirm', blockId, outcome.refusals);
|
|
456
|
+
for (const peerId of outcome.confirmed) {
|
|
457
|
+
// NOTE: allConfirmedPeers never un-records a peer. A holder that confirms in one
|
|
458
|
+
// round and reports `missing` in a later one stays recorded, on the reasoning that
|
|
459
|
+
// `handlePush` answers non-missing only after persisting. If the receiver ever
|
|
460
|
+
// gains a path that drops a just-persisted block (an eviction sweep, a rejected
|
|
461
|
+
// revision), the growth arm would record a peer that no longer holds a replica —
|
|
462
|
+
// intersect against the LAST round's confirmed peers instead of unioning.
|
|
463
|
+
allConfirmedPeers.add(peerId);
|
|
491
464
|
}
|
|
492
|
-
confirmCount =
|
|
465
|
+
confirmCount = outcome.confirmed.length;
|
|
493
466
|
} finally {
|
|
494
467
|
this.releaseSemaphore();
|
|
495
468
|
}
|
|
@@ -511,6 +484,20 @@ export class BlockTransferCoordinator {
|
|
|
511
484
|
}
|
|
512
485
|
}
|
|
513
486
|
|
|
487
|
+
/**
|
|
488
|
+
* This coordinator's binding of the shared push loop: rebalance reason, this node's protocol
|
|
489
|
+
* prefix, and `transferTimeoutMs` as BOTH per-peer deadlines (see {@link BlockTransferConfig}).
|
|
490
|
+
*/
|
|
491
|
+
private pushBlock(blockId: string, peerIds: readonly string[], stopAfterConfirmed: number): Promise<PushBlockOutcome> {
|
|
492
|
+
return pushBlockToPeers(this.repo, this.peerNetwork, blockId, peerIds, {
|
|
493
|
+
reason: 'rebalance',
|
|
494
|
+
protocolPrefix: this.protocolPrefix,
|
|
495
|
+
stopAfterConfirmed,
|
|
496
|
+
dialTimeoutMs: this.transferTimeoutMs,
|
|
497
|
+
responseTimeoutMs: this.transferTimeoutMs
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
514
501
|
// --- Semaphore for concurrency limiting ---
|
|
515
502
|
|
|
516
503
|
private async acquireSemaphore(): Promise<void> {
|
|
@@ -538,10 +525,18 @@ export class BlockTransferCoordinator {
|
|
|
538
525
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
539
526
|
}
|
|
540
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Resolve `undefined` if `promise` has not settled within `ms`. The timer is cleared on either
|
|
530
|
+
* outcome: left running, it outlived every pull, push and confirm by the full transfer timeout and
|
|
531
|
+
* held a stopped node's process open that long (see `withDeadline` in `repo/coordinator-repo.ts`).
|
|
532
|
+
*/
|
|
541
533
|
private withTimeout<T>(promise: Promise<T>, ms: number): Promise<T | undefined> {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
534
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
535
|
+
const timeout = new Promise<undefined>(resolve => {
|
|
536
|
+
timer = setTimeout(() => resolve(undefined), ms);
|
|
537
|
+
});
|
|
538
|
+
return Promise.race([promise, timeout]).finally(() => {
|
|
539
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
540
|
+
});
|
|
546
541
|
}
|
|
547
542
|
}
|