@optimystic/db-p2p 0.13.5 → 0.14.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/README.md +6 -6
- package/dist/src/cluster/block-transfer-service.d.ts +67 -8
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +101 -62
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/client.d.ts +18 -1
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +55 -39
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-error.d.ts +38 -0
- package/dist/src/cluster/cluster-error.d.ts.map +1 -0
- package/dist/src/cluster/cluster-error.js +51 -0
- package/dist/src/cluster/cluster-error.js.map +1 -0
- package/dist/src/cluster/cluster-repo.d.ts +135 -3
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +333 -58
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-cert.d.ts +63 -0
- package/dist/src/cluster/commit-cert.d.ts.map +1 -0
- package/dist/src/cluster/commit-cert.js +108 -0
- package/dist/src/cluster/commit-cert.js.map +1 -0
- package/dist/src/cluster/service.d.ts +46 -11
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +97 -33
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts +13 -0
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +34 -4
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cohort-topic/bootstrap-evidence-builder.d.ts +59 -0
- package/dist/src/cohort-topic/bootstrap-evidence-builder.d.ts.map +1 -0
- package/dist/src/cohort-topic/bootstrap-evidence-builder.js +79 -0
- package/dist/src/cohort-topic/bootstrap-evidence-builder.js.map +1 -0
- package/dist/src/cohort-topic/bootstrap-evidence-verifiers.d.ts +81 -0
- package/dist/src/cohort-topic/bootstrap-evidence-verifiers.d.ts.map +1 -0
- package/dist/src/cohort-topic/bootstrap-evidence-verifiers.js +93 -0
- package/dist/src/cohort-topic/bootstrap-evidence-verifiers.js.map +1 -0
- package/dist/src/cohort-topic/bootstrap-parent-reference.d.ts +108 -0
- package/dist/src/cohort-topic/bootstrap-parent-reference.d.ts.map +1 -0
- package/dist/src/cohort-topic/bootstrap-parent-reference.js +108 -0
- package/dist/src/cohort-topic/bootstrap-parent-reference.js.map +1 -0
- package/dist/src/cohort-topic/change-bridge.d.ts +65 -0
- package/dist/src/cohort-topic/change-bridge.d.ts.map +1 -0
- package/dist/src/cohort-topic/change-bridge.js +76 -0
- package/dist/src/cohort-topic/change-bridge.js.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.d.ts +74 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.d.ts.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.js +87 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.js.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.d.ts +42 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.d.ts.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.js +57 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.js.map +1 -0
- package/dist/src/cohort-topic/fret-trust-anchor.d.ts +100 -0
- package/dist/src/cohort-topic/fret-trust-anchor.d.ts.map +1 -0
- package/dist/src/cohort-topic/fret-trust-anchor.js +115 -0
- package/dist/src/cohort-topic/fret-trust-anchor.js.map +1 -0
- package/dist/src/cohort-topic/host.d.ts +573 -0
- package/dist/src/cohort-topic/host.d.ts.map +1 -0
- package/dist/src/cohort-topic/host.js +1460 -0
- package/dist/src/cohort-topic/host.js.map +1 -0
- package/dist/src/cohort-topic/index.d.ts +14 -0
- package/dist/src/cohort-topic/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/index.js +14 -0
- package/dist/src/cohort-topic/index.js.map +1 -0
- package/dist/src/cohort-topic/membership-publish-sink.d.ts +14 -0
- package/dist/src/cohort-topic/membership-publish-sink.d.ts.map +1 -0
- package/dist/src/cohort-topic/membership-publish-sink.js +17 -0
- package/dist/src/cohort-topic/membership-publish-sink.js.map +1 -0
- package/dist/src/cohort-topic/membership-source.d.ts +36 -0
- package/dist/src/cohort-topic/membership-source.d.ts.map +1 -0
- package/dist/src/cohort-topic/membership-source.js +58 -0
- package/dist/src/cohort-topic/membership-source.js.map +1 -0
- package/dist/src/cohort-topic/peer-codec.d.ts +18 -0
- package/dist/src/cohort-topic/peer-codec.d.ts.map +1 -0
- package/dist/src/cohort-topic/peer-codec.js +26 -0
- package/dist/src/cohort-topic/peer-codec.js.map +1 -0
- package/dist/src/cohort-topic/peer-sig.d.ts +52 -0
- package/dist/src/cohort-topic/peer-sig.d.ts.map +1 -0
- package/dist/src/cohort-topic/peer-sig.js +83 -0
- package/dist/src/cohort-topic/peer-sig.js.map +1 -0
- package/dist/src/cohort-topic/protocols.d.ts +49 -0
- package/dist/src/cohort-topic/protocols.d.ts.map +1 -0
- package/dist/src/cohort-topic/protocols.js +58 -0
- package/dist/src/cohort-topic/protocols.js.map +1 -0
- package/dist/src/cohort-topic/reactivity-membership-gate.d.ts +49 -0
- package/dist/src/cohort-topic/reactivity-membership-gate.d.ts.map +1 -0
- package/dist/src/cohort-topic/reactivity-membership-gate.js +42 -0
- package/dist/src/cohort-topic/reactivity-membership-gate.js.map +1 -0
- package/dist/src/cohort-topic/size-estimator.d.ts +16 -0
- package/dist/src/cohort-topic/size-estimator.d.ts.map +1 -0
- package/dist/src/cohort-topic/size-estimator.js +16 -0
- package/dist/src/cohort-topic/size-estimator.js.map +1 -0
- package/dist/src/cohort-topic/stream-util.d.ts +19 -0
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -0
- package/dist/src/cohort-topic/stream-util.js +76 -0
- package/dist/src/cohort-topic/stream-util.js.map +1 -0
- package/dist/src/cohort-topic/threshold-crypto.d.ts +94 -0
- package/dist/src/cohort-topic/threshold-crypto.d.ts.map +1 -0
- package/dist/src/cohort-topic/threshold-crypto.js +178 -0
- package/dist/src/cohort-topic/threshold-crypto.js.map +1 -0
- package/dist/src/cohort-topic/topic-router.d.ts +41 -0
- package/dist/src/cohort-topic/topic-router.d.ts.map +1 -0
- package/dist/src/cohort-topic/topic-router.js +63 -0
- package/dist/src/cohort-topic/topic-router.js.map +1 -0
- package/dist/src/dispute/cascade.d.ts +179 -0
- package/dist/src/dispute/cascade.d.ts.map +1 -0
- package/dist/src/dispute/cascade.js +315 -0
- package/dist/src/dispute/cascade.js.map +1 -0
- package/dist/src/dispute/client.d.ts +2 -1
- package/dist/src/dispute/client.d.ts.map +1 -1
- package/dist/src/dispute/client.js +12 -3
- package/dist/src/dispute/client.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts +41 -1
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +147 -21
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/dispute/index.d.ts +2 -0
- package/dist/src/dispute/index.d.ts.map +1 -1
- package/dist/src/dispute/index.js +2 -0
- package/dist/src/dispute/index.js.map +1 -1
- package/dist/src/dispute/invalidation.d.ts +266 -0
- package/dist/src/dispute/invalidation.d.ts.map +1 -0
- package/dist/src/dispute/invalidation.js +405 -0
- package/dist/src/dispute/invalidation.js.map +1 -0
- package/dist/src/dispute/types.d.ts +14 -0
- package/dist/src/dispute/types.d.ts.map +1 -1
- package/dist/src/dispute/types.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +7 -0
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +49 -5
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +45 -3
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +522 -5
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/matchmaking/aggregate-counts.d.ts +67 -0
- package/dist/src/matchmaking/aggregate-counts.d.ts.map +1 -0
- package/dist/src/matchmaking/aggregate-counts.js +60 -0
- package/dist/src/matchmaking/aggregate-counts.js.map +1 -0
- package/dist/src/matchmaking/index.d.ts +20 -0
- package/dist/src/matchmaking/index.d.ts.map +1 -0
- package/dist/src/matchmaking/index.js +20 -0
- package/dist/src/matchmaking/index.js.map +1 -0
- package/dist/src/matchmaking/module.d.ts +181 -0
- package/dist/src/matchmaking/module.d.ts.map +1 -0
- package/dist/src/matchmaking/module.js +221 -0
- package/dist/src/matchmaking/module.js.map +1 -0
- package/dist/src/matchmaking/protocols.d.ts +37 -0
- package/dist/src/matchmaking/protocols.d.ts.map +1 -0
- package/dist/src/matchmaking/protocols.js +42 -0
- package/dist/src/matchmaking/protocols.js.map +1 -0
- package/dist/src/matchmaking/provider-manager.d.ts +52 -0
- package/dist/src/matchmaking/provider-manager.d.ts.map +1 -0
- package/dist/src/matchmaking/provider-manager.js +74 -0
- package/dist/src/matchmaking/provider-manager.js.map +1 -0
- package/dist/src/matchmaking/query-handler.d.ts +34 -0
- package/dist/src/matchmaking/query-handler.d.ts.map +1 -0
- package/dist/src/matchmaking/query-handler.js +62 -0
- package/dist/src/matchmaking/query-handler.js.map +1 -0
- package/dist/src/matchmaking/query-transport.d.ts +180 -0
- package/dist/src/matchmaking/query-transport.d.ts.map +1 -0
- package/dist/src/matchmaking/query-transport.js +315 -0
- package/dist/src/matchmaking/query-transport.js.map +1 -0
- package/dist/src/matchmaking/seeker-manager.d.ts +39 -0
- package/dist/src/matchmaking/seeker-manager.d.ts.map +1 -0
- package/dist/src/matchmaking/seeker-manager.js +49 -0
- package/dist/src/matchmaking/seeker-manager.js.map +1 -0
- package/dist/src/matchmaking/seeker-walk-client.d.ts +127 -0
- package/dist/src/matchmaking/seeker-walk-client.d.ts.map +1 -0
- package/dist/src/matchmaking/seeker-walk-client.js +189 -0
- package/dist/src/matchmaking/seeker-walk-client.js.map +1 -0
- package/dist/src/matchmaking/traffic-validation.d.ts +128 -0
- package/dist/src/matchmaking/traffic-validation.d.ts.map +1 -0
- package/dist/src/matchmaking/traffic-validation.js +105 -0
- package/dist/src/matchmaking/traffic-validation.js.map +1 -0
- package/dist/src/network/network-manager-service.d.ts +4 -2
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +9 -3
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/protocol-client.d.ts +13 -0
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +74 -2
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/reactivity/forwarder-host.d.ts +198 -0
- package/dist/src/reactivity/forwarder-host.d.ts.map +1 -0
- package/dist/src/reactivity/forwarder-host.js +355 -0
- package/dist/src/reactivity/forwarder-host.js.map +1 -0
- package/dist/src/reactivity/index.d.ts +19 -0
- package/dist/src/reactivity/index.d.ts.map +1 -0
- package/dist/src/reactivity/index.js +19 -0
- package/dist/src/reactivity/index.js.map +1 -0
- package/dist/src/reactivity/notify-transport.d.ts +70 -0
- package/dist/src/reactivity/notify-transport.d.ts.map +1 -0
- package/dist/src/reactivity/notify-transport.js +111 -0
- package/dist/src/reactivity/notify-transport.js.map +1 -0
- package/dist/src/reactivity/origination-manager.d.ts +110 -0
- package/dist/src/reactivity/origination-manager.d.ts.map +1 -0
- package/dist/src/reactivity/origination-manager.js +129 -0
- package/dist/src/reactivity/origination-manager.js.map +1 -0
- package/dist/src/reactivity/protocols.d.ts +43 -0
- package/dist/src/reactivity/protocols.d.ts.map +1 -0
- package/dist/src/reactivity/protocols.js +50 -0
- package/dist/src/reactivity/protocols.js.map +1 -0
- package/dist/src/reactivity/push-state-gossip.d.ts +132 -0
- package/dist/src/reactivity/push-state-gossip.d.ts.map +1 -0
- package/dist/src/reactivity/push-state-gossip.js +231 -0
- package/dist/src/reactivity/push-state-gossip.js.map +1 -0
- package/dist/src/reactivity/recover-transport.d.ts +167 -0
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -0
- package/dist/src/reactivity/recover-transport.js +308 -0
- package/dist/src/reactivity/recover-transport.js.map +1 -0
- package/dist/src/reactivity/rotation-rereg-scheduler.d.ts +142 -0
- package/dist/src/reactivity/rotation-rereg-scheduler.d.ts.map +1 -0
- package/dist/src/reactivity/rotation-rereg-scheduler.js +223 -0
- package/dist/src/reactivity/rotation-rereg-scheduler.js.map +1 -0
- package/dist/src/reactivity/subscriber-registry.d.ts +47 -0
- package/dist/src/reactivity/subscriber-registry.d.ts.map +1 -0
- package/dist/src/reactivity/subscriber-registry.js +87 -0
- package/dist/src/reactivity/subscriber-registry.js.map +1 -0
- package/dist/src/reactivity/subscription-manager.d.ts +221 -0
- package/dist/src/reactivity/subscription-manager.d.ts.map +1 -0
- package/dist/src/reactivity/subscription-manager.js +302 -0
- package/dist/src/reactivity/subscription-manager.js.map +1 -0
- package/dist/src/reactivity/topic-bytes.d.ts +32 -0
- package/dist/src/reactivity/topic-bytes.d.ts.map +1 -0
- package/dist/src/reactivity/topic-bytes.js +34 -0
- package/dist/src/reactivity/topic-bytes.js.map +1 -0
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +51 -23
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/service.d.ts +46 -1
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +79 -45
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/rpc-deadline.d.ts +37 -0
- package/dist/src/rpc-deadline.d.ts.map +1 -0
- package/dist/src/rpc-deadline.js +31 -0
- package/dist/src/rpc-deadline.js.map +1 -0
- package/dist/src/storage/block-storage.d.ts +2 -0
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +95 -2
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +25 -0
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +53 -2
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +193 -9
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/sync/client.d.ts +4 -1
- package/dist/src/sync/client.d.ts.map +1 -1
- package/dist/src/sync/client.js +5 -2
- package/dist/src/sync/client.js.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts +219 -0
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -0
- package/dist/src/testing/cohort-topic-mesh-harness.js +467 -0
- package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -0
- package/dist/src/testing/matchmaking-mesh-harness.d.ts +187 -0
- package/dist/src/testing/matchmaking-mesh-harness.d.ts.map +1 -0
- package/dist/src/testing/matchmaking-mesh-harness.js +358 -0
- package/dist/src/testing/matchmaking-mesh-harness.js.map +1 -0
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +23 -1
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/dist/src/testing/reactivity-mesh-harness.d.ts +298 -0
- package/dist/src/testing/reactivity-mesh-harness.d.ts.map +1 -0
- package/dist/src/testing/reactivity-mesh-harness.js +663 -0
- package/dist/src/testing/reactivity-mesh-harness.js.map +1 -0
- package/package.json +7 -4
- package/src/cluster/block-transfer-service.ts +133 -68
- package/src/cluster/client.ts +64 -45
- package/src/cluster/cluster-error.ts +64 -0
- package/src/cluster/cluster-repo.ts +405 -55
- package/src/cluster/commit-cert.ts +139 -0
- package/src/cluster/service.ts +116 -44
- package/src/cluster/spread-on-churn.ts +328 -285
- package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -0
- package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -0
- package/src/cohort-topic/bootstrap-parent-reference.ts +159 -0
- package/src/cohort-topic/change-bridge.ts +109 -0
- package/src/cohort-topic/cohort-gossip-driver.ts +144 -0
- package/src/cohort-topic/cohort-gossip-transport.ts +74 -0
- package/src/cohort-topic/fret-trust-anchor.ts +153 -0
- package/src/cohort-topic/host.ts +2175 -0
- package/src/cohort-topic/index.ts +13 -0
- package/src/cohort-topic/membership-publish-sink.ts +20 -0
- package/src/cohort-topic/membership-source.ts +68 -0
- package/src/cohort-topic/peer-codec.ts +31 -0
- package/src/cohort-topic/peer-sig.ts +86 -0
- package/src/cohort-topic/protocols.ts +71 -0
- package/src/cohort-topic/reactivity-membership-gate.ts +77 -0
- package/src/cohort-topic/size-estimator.ts +16 -0
- package/src/cohort-topic/stream-util.ts +87 -0
- package/src/cohort-topic/threshold-crypto.ts +239 -0
- package/src/cohort-topic/topic-router.ts +77 -0
- package/src/dispute/cascade.ts +517 -0
- package/src/dispute/client.ts +12 -2
- package/src/dispute/dispute-service.ts +171 -24
- package/src/dispute/index.ts +42 -0
- package/src/dispute/invalidation.ts +587 -0
- package/src/dispute/types.ts +14 -0
- package/src/index.ts +4 -0
- package/src/libp2p-key-network.ts +49 -6
- package/src/libp2p-node-base.ts +620 -12
- package/src/matchmaking/aggregate-counts.ts +104 -0
- package/src/matchmaking/index.ts +20 -0
- package/src/matchmaking/module.ts +363 -0
- package/src/matchmaking/protocols.ts +51 -0
- package/src/matchmaking/provider-manager.ts +95 -0
- package/src/matchmaking/query-handler.ts +88 -0
- package/src/matchmaking/query-transport.ts +483 -0
- package/src/matchmaking/seeker-manager.ts +64 -0
- package/src/matchmaking/seeker-walk-client.ts +293 -0
- package/src/matchmaking/traffic-validation.ts +195 -0
- package/src/network/network-manager-service.ts +9 -2
- package/src/protocol-client.ts +64 -3
- package/src/reactivity/forwarder-host.ts +438 -0
- package/src/reactivity/index.ts +19 -0
- package/src/reactivity/notify-transport.ts +144 -0
- package/src/reactivity/origination-manager.ts +192 -0
- package/src/reactivity/protocols.ts +61 -0
- package/src/reactivity/push-state-gossip.ts +291 -0
- package/src/reactivity/recover-transport.ts +408 -0
- package/src/reactivity/rotation-rereg-scheduler.ts +256 -0
- package/src/reactivity/subscriber-registry.ts +96 -0
- package/src/reactivity/subscription-manager.ts +450 -0
- package/src/reactivity/topic-bytes.ts +37 -0
- package/src/repo/client.ts +54 -23
- package/src/repo/service.ts +90 -42
- package/src/rpc-deadline.ts +45 -0
- package/src/storage/block-storage.ts +106 -3
- package/src/storage/i-block-storage.ts +27 -0
- package/src/storage/storage-repo.ts +209 -13
- package/src/sync/client.ts +5 -2
- package/src/testing/cohort-topic-mesh-harness.ts +613 -0
- package/src/testing/matchmaking-mesh-harness.ts +475 -0
- package/src/testing/mesh-harness.ts +22 -2
- package/src/testing/reactivity-mesh-harness.ts +922 -0
package/src/repo/client.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
PendRequest, CommitRequest, BlockGets, IPeerNetwork, PeerId
|
|
4
4
|
} from "@optimystic/db-core";
|
|
5
5
|
import type { RepoMessage } from "@optimystic/db-core";
|
|
6
|
+
import { blockIdsForTransforms, blockIdToBytes } from "@optimystic/db-core";
|
|
6
7
|
import { ProtocolClient } from "../protocol-client.js";
|
|
7
8
|
import { peerIdFromString } from "@libp2p/peer-id";
|
|
8
9
|
|
|
@@ -64,20 +65,39 @@ export class RepoClient extends ProtocolClient implements IRepo {
|
|
|
64
65
|
};
|
|
65
66
|
const correlationId = this.extractCorrelationId(operations);
|
|
66
67
|
const deadline = options.expiration ?? (Date.now() + 30_000)
|
|
67
|
-
const
|
|
68
|
-
const withTimeout = async <U>(fn: () => Promise<U>): Promise<U> => {
|
|
69
|
-
return await Promise.race<U>([
|
|
70
|
-
fn(),
|
|
71
|
-
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('RepoClient timeout')), msLeft))
|
|
72
|
-
])
|
|
73
|
-
}
|
|
74
|
-
let response: any
|
|
68
|
+
const deadlineMs = Math.max(1, deadline - Date.now())
|
|
75
69
|
const preferred = (this.protocolPrefix ?? '/db-p2p') + '/repo/1.0.0'
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
70
|
+
|
|
71
|
+
// Drive the remaining `expiration` budget through an AbortController whose
|
|
72
|
+
// abort *reason* is the caller-facing `'RepoClient timeout'` Error, combined
|
|
73
|
+
// with any caller signal. processMessage forwards `signal` to both the dial
|
|
74
|
+
// and the response-read and calls `stream.abort(signal.reason)` on abort, so —
|
|
75
|
+
// unlike the old `Promise.race`, whose losing branch left the inner read
|
|
76
|
+
// running and leaked a pending read + stream on every timed-out RPC to a silent
|
|
77
|
+
// peer — the deadline now genuinely cancels the inner read while the caller
|
|
78
|
+
// still observes an error whose `.message === 'RepoClient timeout'`.
|
|
79
|
+
//
|
|
80
|
+
// Intentionally no `responseTimeoutMs`: the combined signal already bounds the
|
|
81
|
+
// read at `deadlineMs`. A second, shorter cap would surface as
|
|
82
|
+
// ResponseTimeoutError and mask the caller-facing 'RepoClient timeout' message.
|
|
83
|
+
const deadlineController = new AbortController()
|
|
84
|
+
const timer = setTimeout(
|
|
85
|
+
() => deadlineController.abort(new Error('RepoClient timeout')),
|
|
86
|
+
deadlineMs
|
|
87
|
+
)
|
|
88
|
+
const combinedSignal = options?.signal
|
|
89
|
+
? AbortSignal.any([options.signal, deadlineController.signal])
|
|
90
|
+
: deadlineController.signal
|
|
91
|
+
let response: any
|
|
92
|
+
try {
|
|
93
|
+
response = await super.processMessage<any>(message, preferred, {
|
|
94
|
+
signal: combinedSignal,
|
|
95
|
+
correlationId,
|
|
96
|
+
dialTimeoutMs: options?.dialTimeoutMs,
|
|
97
|
+
})
|
|
98
|
+
} finally {
|
|
99
|
+
clearTimeout(timer)
|
|
100
|
+
}
|
|
81
101
|
|
|
82
102
|
if (response?.redirect?.peers?.length) {
|
|
83
103
|
if (hop >= 2) {
|
|
@@ -89,8 +109,10 @@ export class RepoClient extends ProtocolClient implements IRepo {
|
|
|
89
109
|
if (next.id === currentIdStr) {
|
|
90
110
|
throw new Error('Redirect loop detected in RepoClient (same peer)')
|
|
91
111
|
}
|
|
92
|
-
// cache hint
|
|
93
|
-
|
|
112
|
+
// cache hint — await so the recorded key bytes match what findCoordinator
|
|
113
|
+
// later looks up (blockIdToBytes is async); deterministic ordering also
|
|
114
|
+
// keeps the hint testable. The redirect retry below is async anyway.
|
|
115
|
+
await this.recordCoordinatorForOpsIfSupported(operations, nextId)
|
|
94
116
|
// single-hop retry against target peer using repo protocol
|
|
95
117
|
const nextClient = RepoClient.create(nextId, this.peerNetwork, this.protocolPrefix)
|
|
96
118
|
return await nextClient.processRepoMessage<T>(operations, options, hop + 1)
|
|
@@ -98,28 +120,37 @@ export class RepoClient extends ProtocolClient implements IRepo {
|
|
|
98
120
|
return response as T;
|
|
99
121
|
}
|
|
100
122
|
|
|
101
|
-
private extractKeyFromOperations(ops: RepoMessage['operations']): Uint8Array | undefined {
|
|
123
|
+
private async extractKeyFromOperations(ops: RepoMessage['operations']): Promise<Uint8Array | undefined> {
|
|
102
124
|
const op = ops[0];
|
|
125
|
+
// The recorded key MUST be the sha256 digest produced by blockIdToBytes — the
|
|
126
|
+
// exact bytes findCoordinator/recordCoordinator key the coordinator cache on
|
|
127
|
+
// (NetworkTransactor passes blockIdToBytes(blockId)). Raw utf8 of the id would
|
|
128
|
+
// never match, so the hint would silently never be retrieved.
|
|
103
129
|
if ('get' in op) {
|
|
104
130
|
const id = op.get.blockIds[0];
|
|
105
|
-
return id ?
|
|
131
|
+
return id ? await blockIdToBytes(id) : undefined;
|
|
106
132
|
}
|
|
107
133
|
if ('pend' in op) {
|
|
108
|
-
|
|
109
|
-
|
|
134
|
+
// Key on a real block id the pend touches, NOT a structural transforms field
|
|
135
|
+
// name ('inserts'/'updates'/'deletes'); see RepoService.deriveBlockKey.
|
|
136
|
+
const id = blockIdsForTransforms(op.pend.transforms)[0];
|
|
137
|
+
return id ? await blockIdToBytes(id) : undefined;
|
|
110
138
|
}
|
|
111
139
|
if ('commit' in op) {
|
|
112
|
-
|
|
140
|
+
// Anchor on blockIds[0] (where CoordinatorRepo.commit runs consensus +
|
|
141
|
+
// verifyResponsibility), NOT tailId — they differ for a non-tail batch.
|
|
142
|
+
const id = op.commit.blockIds[0];
|
|
143
|
+
return id ? await blockIdToBytes(id) : undefined;
|
|
113
144
|
}
|
|
114
145
|
if ('cancel' in op) {
|
|
115
146
|
const id = op.cancel.actionRef.blockIds[0];
|
|
116
|
-
return id ?
|
|
147
|
+
return id ? await blockIdToBytes(id) : undefined;
|
|
117
148
|
}
|
|
118
149
|
return undefined;
|
|
119
150
|
}
|
|
120
151
|
|
|
121
|
-
private recordCoordinatorForOpsIfSupported(ops: RepoMessage['operations'], peerId: PeerId): void {
|
|
122
|
-
const keyBytes = this.extractKeyFromOperations(ops)
|
|
152
|
+
private async recordCoordinatorForOpsIfSupported(ops: RepoMessage['operations'], peerId: PeerId): Promise<void> {
|
|
153
|
+
const keyBytes = await this.extractKeyFromOperations(ops)
|
|
123
154
|
const pn: any = this.peerNetwork as any
|
|
124
155
|
if (keyBytes != null && typeof pn?.recordCoordinator === 'function') {
|
|
125
156
|
pn.recordCoordinator(keyBytes, peerId)
|
package/src/repo/service.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { pipe } from 'it-pipe'
|
|
2
2
|
import { decode as lpDecode, encode as lpEncode } from 'it-length-prefixed'
|
|
3
|
-
import type { Startable, Logger, Stream, Connection, StreamHandler, PeerId } from '@libp2p/interface'
|
|
3
|
+
import type { Startable, Logger, Stream, Connection, StreamHandler, PeerId, Libp2p } from '@libp2p/interface'
|
|
4
4
|
import type { IRepo, RepoMessage } from '@optimystic/db-core'
|
|
5
|
+
import { blockIdsForTransforms } from '@optimystic/db-core'
|
|
5
6
|
import { peersEqual } from '../peer-utils.js'
|
|
6
|
-
import { sha256 } from 'multiformats/hashes/sha2'
|
|
7
7
|
import { encodePeers, type RedirectPayload } from './redirect.js'
|
|
8
8
|
import type { Uint8ArrayList } from 'uint8arraylist'
|
|
9
9
|
import { createLogger } from '../logger.js'
|
|
@@ -28,6 +28,13 @@ export type RepoServiceComponents = BaseComponents & {
|
|
|
28
28
|
networkManager?: NetworkManagerLike
|
|
29
29
|
peerId?: PeerId
|
|
30
30
|
getConnectionAddrs?: (peerId: PeerId) => string[]
|
|
31
|
+
/**
|
|
32
|
+
* Optional libp2p node. The production wiring injects the node post-construction
|
|
33
|
+
* via {@link RepoService.setLibp2p} (the `components.libp2p` proxy does not
|
|
34
|
+
* reliably resolve from inside a service at request time); this field is a
|
|
35
|
+
* best-effort fallback resolver used only when no node has been injected.
|
|
36
|
+
*/
|
|
37
|
+
libp2p?: Libp2p
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
export type RepoServiceInit = {
|
|
@@ -65,6 +72,14 @@ export class RepoService implements Startable {
|
|
|
65
72
|
private running: boolean
|
|
66
73
|
/** Responsibility K - how many peers are responsible for a key (for redirect decisions) */
|
|
67
74
|
private readonly responsibilityK: number
|
|
75
|
+
/**
|
|
76
|
+
* The libp2p node, injected post-construction by the node wiring (see
|
|
77
|
+
* libp2p-node-base.ts, mirroring how `networkManager`/`fret` receive theirs).
|
|
78
|
+
* The libp2p `components.libp2p` proxy does NOT reliably resolve from inside a
|
|
79
|
+
* service at request time, so the redirect path resolves the network manager,
|
|
80
|
+
* self identity, and connection addrs through this explicitly-set reference.
|
|
81
|
+
*/
|
|
82
|
+
private libp2pRef: Libp2p | undefined
|
|
68
83
|
|
|
69
84
|
constructor(components: RepoServiceComponents, init: RepoServiceInit = {}) {
|
|
70
85
|
this.components = components
|
|
@@ -80,6 +95,19 @@ export class RepoService implements Startable {
|
|
|
80
95
|
|
|
81
96
|
readonly [Symbol.toStringTag] = '@libp2p/repo-service'
|
|
82
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Inject the running libp2p node. Called once post-construction by the node
|
|
100
|
+
* wiring so the redirect path can resolve the network manager / self id / addrs.
|
|
101
|
+
*/
|
|
102
|
+
setLibp2p(libp2p: Libp2p): void {
|
|
103
|
+
this.libp2pRef = libp2p
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Resolve the libp2p node: the injected ref first, then the (best-effort) components proxy. */
|
|
107
|
+
private getLibp2p(): Libp2p | undefined {
|
|
108
|
+
return this.libp2pRef ?? (this.components as any).libp2p
|
|
109
|
+
}
|
|
110
|
+
|
|
83
111
|
/**
|
|
84
112
|
* Start the service
|
|
85
113
|
*/
|
|
@@ -110,17 +138,17 @@ export class RepoService implements Startable {
|
|
|
110
138
|
|
|
111
139
|
private getNetworkManager(): NetworkManagerLike | undefined {
|
|
112
140
|
if (this.components.networkManager) return this.components.networkManager
|
|
113
|
-
return (this.
|
|
141
|
+
return (this.getLibp2p() as any)?.services?.networkManager as NetworkManagerLike | undefined
|
|
114
142
|
}
|
|
115
143
|
|
|
116
144
|
private getSelfId(): PeerId | undefined {
|
|
117
145
|
if (this.components.peerId) return this.components.peerId
|
|
118
|
-
return
|
|
146
|
+
return this.getLibp2p()?.peerId as PeerId | undefined
|
|
119
147
|
}
|
|
120
148
|
|
|
121
149
|
private getPeerAddrs(peerId: PeerId): string[] {
|
|
122
150
|
if (this.components.getConnectionAddrs) return this.components.getConnectionAddrs(peerId)
|
|
123
|
-
const libp2p =
|
|
151
|
+
const libp2p = this.getLibp2p() as any
|
|
124
152
|
if (!libp2p?.getConnections) return []
|
|
125
153
|
const conns: any[] = libp2p.getConnections(peerId) ?? []
|
|
126
154
|
const addrs: string[] = []
|
|
@@ -131,6 +159,41 @@ export class RepoService implements Startable {
|
|
|
131
159
|
return addrs
|
|
132
160
|
}
|
|
133
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Derive the redirect routing key and op name for a single operation.
|
|
164
|
+
*
|
|
165
|
+
* The key MUST be the block the corresponding handler actually coordinates and
|
|
166
|
+
* verifies responsibility on, so redirect routing stays consistent with where the
|
|
167
|
+
* op is executed:
|
|
168
|
+
* - get → blockIds[0]
|
|
169
|
+
* - pend → blockIdsForTransforms(transforms)[0]
|
|
170
|
+
* - cancel → actionRef.blockIds[0]
|
|
171
|
+
* - commit → blockIds[0] (CoordinatorRepo.commit anchors consensus on
|
|
172
|
+
* getClusterSize(blockIds[0]) / executeClusterTransaction(blockIds[0]) and guards
|
|
173
|
+
* with verifyResponsibility(blockIds) — NOT tailId; for a per-block commit batch
|
|
174
|
+
* whose blockIds[0] !== tailId, keying on tailId redirected the commit to the
|
|
175
|
+
* collection tail's cluster, which then fails verifyResponsibility for the non-tail block.)
|
|
176
|
+
*
|
|
177
|
+
* Returns blockKey === undefined when the op carries no routable key (e.g. a cancel
|
|
178
|
+
* with an empty blockIds list), in which case the caller handles it locally without a
|
|
179
|
+
* redirect check.
|
|
180
|
+
*/
|
|
181
|
+
deriveBlockKey(operation: RepoMessage['operations'][number]): { blockKey: string | undefined, opName: string } {
|
|
182
|
+
if ('get' in operation) {
|
|
183
|
+
return { blockKey: operation.get.blockIds[0], opName: 'get' }
|
|
184
|
+
}
|
|
185
|
+
if ('pend' in operation) {
|
|
186
|
+
return { blockKey: blockIdsForTransforms(operation.pend.transforms)[0], opName: 'pend' }
|
|
187
|
+
}
|
|
188
|
+
if ('cancel' in operation) {
|
|
189
|
+
return { blockKey: operation.cancel.actionRef.blockIds[0], opName: 'cancel' }
|
|
190
|
+
}
|
|
191
|
+
if ('commit' in operation) {
|
|
192
|
+
return { blockKey: operation.commit.blockIds[0], opName: 'commit' }
|
|
193
|
+
}
|
|
194
|
+
return { blockKey: undefined, opName: 'unknown' }
|
|
195
|
+
}
|
|
196
|
+
|
|
134
197
|
/**
|
|
135
198
|
* Check if this node should redirect the request for a given key.
|
|
136
199
|
* Returns a RedirectPayload if not responsible, null if should handle locally.
|
|
@@ -140,8 +203,14 @@ export class RepoService implements Startable {
|
|
|
140
203
|
const nm = this.getNetworkManager()
|
|
141
204
|
if (!nm) return null
|
|
142
205
|
|
|
143
|
-
|
|
144
|
-
|
|
206
|
+
// Pass the RAW encoded block-key bytes to getCluster. getCluster hashes
|
|
207
|
+
// internally (hashKey == sha256), so the responsible-set coordinate becomes
|
|
208
|
+
// hashKey(encode(blockKey)) — identical to how the cluster coordinator
|
|
209
|
+
// derives it (ClusterCoordinator.getClusterForBlock → findCluster(encode(blockId))).
|
|
210
|
+
// Pre-hashing here would double-hash (hashKey(sha256(encode(blockKey)))), placing
|
|
211
|
+
// the cohort at an unrelated ring coordinate and redirecting requests the
|
|
212
|
+
// coordinator legitimately routed to this peer.
|
|
213
|
+
const key = new TextEncoder().encode(blockKey)
|
|
145
214
|
const cluster = await nm.getCluster(key)
|
|
146
215
|
;(message as any).cluster = cluster.map((p: PeerId) => p.toString?.() ?? String(p))
|
|
147
216
|
|
|
@@ -175,46 +244,25 @@ export class RepoService implements Startable {
|
|
|
175
244
|
const decoded = new TextDecoder().decode(msg.subarray())
|
|
176
245
|
const message = JSON.parse(decoded) as RepoMessage
|
|
177
246
|
|
|
178
|
-
// Process each operation
|
|
247
|
+
// Process each operation. Derive the redirect routing key once (keyed on the
|
|
248
|
+
// block the handler actually coordinates), redirect-check it, then dispatch.
|
|
179
249
|
const operation = message.operations[0]
|
|
180
|
-
|
|
250
|
+
const { blockKey, opName } = this.deriveBlockKey(operation)
|
|
251
|
+
const redirect = blockKey !== undefined
|
|
252
|
+
? await this.checkRedirect(blockKey, opName, message)
|
|
253
|
+
: null
|
|
181
254
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
} else {
|
|
188
|
-
response = await this.repo.get(operation.get, { expiration: message.expiration, skipClusterFetch: true } as any)
|
|
189
|
-
}
|
|
255
|
+
let response: any
|
|
256
|
+
if (redirect) {
|
|
257
|
+
response = redirect
|
|
258
|
+
} else if ('get' in operation) {
|
|
259
|
+
response = await this.repo.get(operation.get, { expiration: message.expiration, skipClusterFetch: true } as any)
|
|
190
260
|
} else if ('pend' in operation) {
|
|
191
|
-
|
|
192
|
-
const redirect = await this.checkRedirect(blockKey, 'pend', message)
|
|
193
|
-
if (redirect) {
|
|
194
|
-
response = redirect
|
|
195
|
-
} else {
|
|
196
|
-
response = await this.repo.pend(operation.pend, { expiration: message.expiration })
|
|
197
|
-
}
|
|
261
|
+
response = await this.repo.pend(operation.pend, { expiration: message.expiration })
|
|
198
262
|
} else if ('cancel' in operation) {
|
|
199
|
-
|
|
200
|
-
if (blockKey) {
|
|
201
|
-
const redirect = await this.checkRedirect(blockKey, 'cancel', message)
|
|
202
|
-
if (redirect) {
|
|
203
|
-
response = redirect
|
|
204
|
-
} else {
|
|
205
|
-
response = await this.repo.cancel(operation.cancel.actionRef, { expiration: message.expiration })
|
|
206
|
-
}
|
|
207
|
-
} else {
|
|
208
|
-
response = await this.repo.cancel(operation.cancel.actionRef, { expiration: message.expiration })
|
|
209
|
-
}
|
|
263
|
+
response = await this.repo.cancel(operation.cancel.actionRef, { expiration: message.expiration })
|
|
210
264
|
} else if ('commit' in operation) {
|
|
211
|
-
|
|
212
|
-
const redirect = await this.checkRedirect(blockKey, 'commit', message)
|
|
213
|
-
if (redirect) {
|
|
214
|
-
response = redirect
|
|
215
|
-
} else {
|
|
216
|
-
response = await this.repo.commit(operation.commit, { expiration: message.expiration })
|
|
217
|
-
}
|
|
265
|
+
response = await this.repo.commit(operation.commit, { expiration: message.expiration })
|
|
218
266
|
}
|
|
219
267
|
|
|
220
268
|
// Encode and yield the response
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-RPC deadline knobs shared by the simple {@link ProtocolClient} subclasses
|
|
3
|
+
* (cluster / sync / dispute). `dialTimeoutMs` bounds connecting; `responseTimeoutMs`
|
|
4
|
+
* bounds waiting for the reply once connected (so a peer that connects then goes
|
|
5
|
+
* silent throws {@link ResponseTimeoutError} instead of hanging the caller forever);
|
|
6
|
+
* `signal` cancels the whole request. All optional.
|
|
7
|
+
*/
|
|
8
|
+
export type RpcDeadlineOptions = {
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
dialTimeoutMs?: number;
|
|
11
|
+
responseTimeoutMs?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default per-peer dial deadline. Matches `spread-on-churn.ts` `pushDialTimeoutMs`
|
|
16
|
+
* (3000ms) — an unreachable peer fails the dial fast so the caller can re-pick a
|
|
17
|
+
* different coordinator rather than blocking on a dead route.
|
|
18
|
+
*/
|
|
19
|
+
export const DEFAULT_DIAL_TIMEOUT_MS = 3000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Default per-peer response deadline. Matches `spread-on-churn.ts`
|
|
23
|
+
* `pushResponseTimeoutMs` (10000ms) — a peer that connects then never writes a
|
|
24
|
+
* reply is abandoned instead of hanging the caller forever.
|
|
25
|
+
*/
|
|
26
|
+
export const DEFAULT_RESPONSE_TIMEOUT_MS = 10000;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Merge caller-supplied deadline options with the client-level defaults. An
|
|
30
|
+
* explicitly-supplied value wins (including a deliberate `0`, which
|
|
31
|
+
* {@link ProtocolClient.processMessage} reads as "no cap"); an absent key falls
|
|
32
|
+
* back to the default so a caller that passes nothing still gets a deadline.
|
|
33
|
+
* `signal` has no default — cancellation is always caller-driven.
|
|
34
|
+
*
|
|
35
|
+
* Unlike `BlockTransferClient` (which leaves its defaults to the owning
|
|
36
|
+
* `SpreadOnChurnMonitor` config), these clients have many callers and no single
|
|
37
|
+
* owning monitor, so the deadline default belongs here on the client.
|
|
38
|
+
*/
|
|
39
|
+
export function withRpcDeadlineDefaults(options?: RpcDeadlineOptions): RpcDeadlineOptions {
|
|
40
|
+
return {
|
|
41
|
+
dialTimeoutMs: options?.dialTimeoutMs ?? DEFAULT_DIAL_TIMEOUT_MS,
|
|
42
|
+
responseTimeoutMs: options?.responseTimeoutMs ?? DEFAULT_RESPONSE_TIMEOUT_MS,
|
|
43
|
+
signal: options?.signal,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BlockId, IBlock, Transform, ActionId, ActionRev } from "@optimystic/db-core";
|
|
2
|
-
import { Latches, applyTransform } from "@optimystic/db-core";
|
|
2
|
+
import { Latches, applyTransform, hashString } from "@optimystic/db-core";
|
|
3
3
|
import type { BlockArchive, BlockMetadata, RestoreCallback, RevisionRange } from "./struct.js";
|
|
4
4
|
import type { IRawStorage } from "./i-raw-storage.js";
|
|
5
5
|
import { mergeRanges } from "./helpers.js";
|
|
@@ -123,6 +123,105 @@ export class BlockStorage implements IBlockStorage {
|
|
|
123
123
|
return { reconciled: false, latest: meta.latest };
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
async saveReplica(block: IBlock, source?: ActionRev): Promise<ActionRev> {
|
|
127
|
+
const rev = source?.rev ?? 1;
|
|
128
|
+
// Deterministic fallback id when the sender carried no revision metadata, so a
|
|
129
|
+
// re-push of the same block resolves to the same (rev, actionId) and stays
|
|
130
|
+
// idempotent. Never random/time-based — that would mint a new revision per retry.
|
|
131
|
+
const actionId = source?.actionId ?? await hashString(`${this.blockId}:${JSON.stringify(block)}`);
|
|
132
|
+
|
|
133
|
+
// Serialize the read-modify-write on this block's metadata (mirrors ensureRevision).
|
|
134
|
+
const lockId = `BlockStorage.saveReplica:${this.blockId}`;
|
|
135
|
+
const release = await Latches.acquire(lockId);
|
|
136
|
+
try {
|
|
137
|
+
let meta = await this.storage.getMetadata(this.blockId);
|
|
138
|
+
|
|
139
|
+
// Monotonic guard: an equal-or-newer revision is already held. The block is
|
|
140
|
+
// durably present; do not downgrade `latest` or rewrite the metadata.
|
|
141
|
+
if (meta?.latest && meta.latest.rev >= rev) {
|
|
142
|
+
log('replica:skip blockId=%s rev=%d held=%d', this.blockId, rev, meta.latest.rev);
|
|
143
|
+
return meta.latest;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Write rev → actionId, the action transform, and the materialized block.
|
|
147
|
+
// `{ insert: block }` satisfies saveRestored's write invariants; on the serving
|
|
148
|
+
// path materializeBlock returns the materialized block directly (single rev), so
|
|
149
|
+
// this transform is never applied — see ticket notes.
|
|
150
|
+
const archive: BlockArchive = {
|
|
151
|
+
blockId: this.blockId,
|
|
152
|
+
revisions: {
|
|
153
|
+
[rev]: {
|
|
154
|
+
action: { actionId, rev, transform: { insert: block } },
|
|
155
|
+
block
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
range: [rev, rev + 1]
|
|
159
|
+
};
|
|
160
|
+
await this.saveRestored(archive);
|
|
161
|
+
|
|
162
|
+
// Seed metadata when absent, advance latest, and merge the covered range.
|
|
163
|
+
if (!meta) {
|
|
164
|
+
meta = { latest: undefined, ranges: [] };
|
|
165
|
+
}
|
|
166
|
+
meta.latest = { rev, actionId };
|
|
167
|
+
meta.ranges.unshift([rev, rev + 1]);
|
|
168
|
+
meta.ranges = mergeRanges(meta.ranges);
|
|
169
|
+
await this.storage.saveMetadata(this.blockId, meta);
|
|
170
|
+
|
|
171
|
+
log('replica:save blockId=%s rev=%d actionId=%s', this.blockId, rev, actionId);
|
|
172
|
+
return meta.latest;
|
|
173
|
+
} finally {
|
|
174
|
+
release();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async saveDeletion(source: ActionRev): Promise<ActionRev> {
|
|
179
|
+
const { rev, actionId } = source;
|
|
180
|
+
|
|
181
|
+
// Share the saveReplica latch: both do a read-modify-write of `meta.latest`, so they must be
|
|
182
|
+
// mutually exclusive on this block to keep the monotonic guard sound.
|
|
183
|
+
const lockId = `BlockStorage.saveReplica:${this.blockId}`;
|
|
184
|
+
const release = await Latches.acquire(lockId);
|
|
185
|
+
try {
|
|
186
|
+
let meta = await this.storage.getMetadata(this.blockId);
|
|
187
|
+
|
|
188
|
+
// Monotonic guard: an equal-or-newer revision is already held. Do not downgrade `latest`
|
|
189
|
+
// or rewrite metadata — the tombstone (or a later revision) is already durable.
|
|
190
|
+
if (meta?.latest && meta.latest.rev >= rev) {
|
|
191
|
+
log('deletion:skip blockId=%s rev=%d held=%d', this.blockId, rev, meta.latest.rev);
|
|
192
|
+
return meta.latest;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Forward tombstone: a `{ delete: true }` transform and NO materialized block. saveRestored
|
|
196
|
+
// skips materialization when `block` is absent, so the reverse-apply in materializeBlock
|
|
197
|
+
// resolves this revision to an absent block (read-back as undefined).
|
|
198
|
+
const archive: BlockArchive = {
|
|
199
|
+
blockId: this.blockId,
|
|
200
|
+
revisions: {
|
|
201
|
+
[rev]: {
|
|
202
|
+
action: { actionId, rev, transform: { delete: true } }
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
range: [rev, rev + 1]
|
|
206
|
+
};
|
|
207
|
+
await this.saveRestored(archive);
|
|
208
|
+
|
|
209
|
+
// Seed metadata when absent, advance latest, and merge the covered range.
|
|
210
|
+
if (!meta) {
|
|
211
|
+
meta = { latest: undefined, ranges: [] };
|
|
212
|
+
}
|
|
213
|
+
meta.latest = { rev, actionId };
|
|
214
|
+
meta.ranges.unshift([rev, rev + 1]);
|
|
215
|
+
meta.ranges = mergeRanges(meta.ranges);
|
|
216
|
+
await this.storage.saveMetadata(this.blockId, meta);
|
|
217
|
+
|
|
218
|
+
log('deletion:save blockId=%s rev=%d actionId=%s', this.blockId, rev, actionId);
|
|
219
|
+
return meta.latest;
|
|
220
|
+
} finally {
|
|
221
|
+
release();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
126
225
|
private async ensureRevision(meta: BlockMetadata, rev: number): Promise<void> {
|
|
127
226
|
if (this.inRanges(rev, meta.ranges)) {
|
|
128
227
|
return;
|
|
@@ -154,7 +253,7 @@ export class BlockStorage implements IBlockStorage {
|
|
|
154
253
|
}
|
|
155
254
|
}
|
|
156
255
|
|
|
157
|
-
private async materializeBlock(_meta: BlockMetadata, targetRev: number): Promise<{ block: IBlock, actionRev: ActionRev }> {
|
|
256
|
+
private async materializeBlock(_meta: BlockMetadata, targetRev: number): Promise<{ block: IBlock, actionRev: ActionRev } | undefined> {
|
|
158
257
|
let block: IBlock | undefined;
|
|
159
258
|
let materializedActionRev: ActionRev | undefined;
|
|
160
259
|
const actions: ActionRev[] = [];
|
|
@@ -187,7 +286,11 @@ export class BlockStorage implements IBlockStorage {
|
|
|
187
286
|
}
|
|
188
287
|
|
|
189
288
|
if (!block) {
|
|
190
|
-
|
|
289
|
+
// The reverse-apply collapsed to a tombstone (a `{ delete: true }` revision) — the block
|
|
290
|
+
// is absent at this revision, not corrupt. Read it back as undefined (matching getBlock's
|
|
291
|
+
// "no materialized content" contract) rather than throwing. The genuine-truncation throw
|
|
292
|
+
// ("Failed to find materialized block") above still fires when no materialization exists.
|
|
293
|
+
return undefined;
|
|
191
294
|
}
|
|
192
295
|
if (actions.length) {
|
|
193
296
|
await this.storage.saveMaterializedBlock(this.blockId, actions[0]!.actionId, block);
|
|
@@ -44,6 +44,33 @@ export interface IBlockStorage {
|
|
|
44
44
|
/** Sets the latest revision information */
|
|
45
45
|
setLatest(latest: ActionRev): Promise<void>;
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Persist a replica of a block received out-of-band (churn re-replication).
|
|
49
|
+
*
|
|
50
|
+
* Seeds metadata if absent, writes `rev → actionId`, the action transform, and the
|
|
51
|
+
* materialized block, merges `[rev, rev+1]` into `ranges`, and advances `latest`
|
|
52
|
+
* monotonically. When `source` is provided its `rev`/`actionId` are used; otherwise
|
|
53
|
+
* it falls back to `rev = 1` and a deterministic `actionId` derived from the block
|
|
54
|
+
* (so retries stay idempotent — never random).
|
|
55
|
+
*
|
|
56
|
+
* No-op (still durable) when an equal-or-newer revision is already present: `latest`
|
|
57
|
+
* is never downgraded. Idempotent for a fixed `(rev, actionId)`. Returns the
|
|
58
|
+
* effective latest `ActionRev`.
|
|
59
|
+
*/
|
|
60
|
+
saveReplica(block: IBlock, source?: ActionRev): Promise<ActionRev>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Writes a forward TOMBSTONE revision that reverses a block creation: persists `rev → actionId`,
|
|
64
|
+
* a `{ delete: true }` transform, and NO materialized block, then merges `[rev, rev+1]` into
|
|
65
|
+
* `ranges` and advances `latest` monotonically. The reverse-apply path treats the absent
|
|
66
|
+
* materialization as a deletion, so a `getBlock()` after a tombstone reads back as *absent*
|
|
67
|
+
* (`undefined`) while a historical `getBlock(creationRev)` still materializes the created content.
|
|
68
|
+
*
|
|
69
|
+
* Idempotent for a fixed `(rev, actionId)`; never downgrades `latest` (a no-op — still durable —
|
|
70
|
+
* when an equal-or-newer revision is already present). Returns the effective latest `ActionRev`.
|
|
71
|
+
*/
|
|
72
|
+
saveDeletion(source: ActionRev): Promise<ActionRev>;
|
|
73
|
+
|
|
47
74
|
/**
|
|
48
75
|
* Reconciles `metadata.latest` with the highest contiguous fully-promoted revision in
|
|
49
76
|
* the revisions table. Intended for post-crash recovery of the Crash-D3 gap, where
|