@optimystic/db-p2p 0.13.4 → 0.14.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/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 +8 -1
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +67 -8
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +57 -3
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +523 -6
- 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 +25 -0
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +126 -4
- 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 -19
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +9 -0
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +65 -22
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +20 -0
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +101 -16
- package/dist/src/repo/coordinator-repo.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 +67 -9
- package/src/libp2p-node-base.ts +633 -14
- 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 +119 -5
- 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 -19
- package/src/repo/cluster-coordinator.ts +66 -26
- package/src/repo/coordinator-repo.ts +99 -15
- 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
|
@@ -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
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
IRepo, MessageOptions, BlockId, CommitRequest, CommitResult, GetBlockResults, PendRequest, PendResult, ActionBlocks,
|
|
3
3
|
ActionId, BlockGets, ActionPending, PendSuccess, ActionTransform, ActionTransforms,
|
|
4
|
-
GetBlockResult,
|
|
5
|
-
PendValidationHook
|
|
4
|
+
GetBlockResult, IBlock, ActionRev,
|
|
5
|
+
PendValidationHook,
|
|
6
|
+
CollectionId, IBlockChangeNotifier, CollectionChangeListener, CollectionChangeEvent
|
|
6
7
|
} from "@optimystic/db-core";
|
|
7
8
|
import {
|
|
8
9
|
Latches, transformForBlockId, applyTransform, groupBy, concatTransform, emptyTransforms,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
} from "@optimystic/db-core";
|
|
11
12
|
import { asyncIteratorToArray } from "../it-utility.js";
|
|
12
13
|
import type { IBlockStorage } from "./i-block-storage.js";
|
|
14
|
+
import type { IBlockReplicaStore } from "../cluster/block-transfer-service.js";
|
|
13
15
|
import { createLogger } from "../logger.js";
|
|
14
16
|
|
|
15
17
|
const log = createLogger('storage-repo');
|
|
@@ -19,8 +21,12 @@ export type StorageRepoOptions = {
|
|
|
19
21
|
validatePend?: PendValidationHook;
|
|
20
22
|
};
|
|
21
23
|
|
|
22
|
-
export class StorageRepo implements IRepo {
|
|
24
|
+
export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaStore {
|
|
23
25
|
private readonly validatePend?: PendValidationHook;
|
|
26
|
+
/** Per-collection change listeners; empty sets are pruned on unsubscribe. */
|
|
27
|
+
private readonly changeListeners = new Map<CollectionId, Set<CollectionChangeListener>>();
|
|
28
|
+
/** Catch-all change listeners — fire for EVERY collection's commit on this node. */
|
|
29
|
+
private readonly anyChangeListeners = new Set<CollectionChangeListener>();
|
|
24
30
|
|
|
25
31
|
constructor(
|
|
26
32
|
private readonly createBlockStorage: (blockId: BlockId) => IBlockStorage,
|
|
@@ -29,9 +35,96 @@ export class StorageRepo implements IRepo {
|
|
|
29
35
|
this.validatePend = options?.validatePend;
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Subscribe to commits that mutate `collectionId`'s blocks on this node.
|
|
40
|
+
* Returns an idempotent unsubscribe. See {@link IBlockChangeNotifier}.
|
|
41
|
+
*/
|
|
42
|
+
onCollectionChange(collectionId: CollectionId, listener: CollectionChangeListener): () => void {
|
|
43
|
+
let set = this.changeListeners.get(collectionId);
|
|
44
|
+
if (!set) {
|
|
45
|
+
set = new Set();
|
|
46
|
+
this.changeListeners.set(collectionId, set);
|
|
47
|
+
}
|
|
48
|
+
set.add(listener);
|
|
49
|
+
let unsubscribed = false;
|
|
50
|
+
return () => {
|
|
51
|
+
if (unsubscribed) return;
|
|
52
|
+
unsubscribed = true;
|
|
53
|
+
const current = this.changeListeners.get(collectionId);
|
|
54
|
+
if (current) {
|
|
55
|
+
current.delete(listener);
|
|
56
|
+
if (current.size === 0) {
|
|
57
|
+
this.changeListeners.delete(collectionId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Subscribe to commits mutating ANY collection on this node — the catch-all feed the
|
|
65
|
+
* cohort-topic origination bridge consumes (it cannot enumerate collection ids ahead of time,
|
|
66
|
+
* so a per-collection {@link onCollectionChange} subscription cannot see every commit). Fires for
|
|
67
|
+
* the same `(pending → committed)` transitions as {@link onCollectionChange}, but across every
|
|
68
|
+
* collection. Returns an idempotent unsubscribe; a throwing listener is isolated + logged.
|
|
69
|
+
*/
|
|
70
|
+
onAnyCollectionChange(listener: CollectionChangeListener): () => void {
|
|
71
|
+
this.anyChangeListeners.add(listener);
|
|
72
|
+
let unsubscribed = false;
|
|
73
|
+
return () => {
|
|
74
|
+
if (unsubscribed) return;
|
|
75
|
+
unsubscribed = true;
|
|
76
|
+
this.anyChangeListeners.delete(listener);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Fire one {@link CollectionChangeEvent} per distinct collection that was
|
|
82
|
+
* newly committed. Called AFTER the commit critical section (locks released),
|
|
83
|
+
* fire-and-forget synchronous; a throwing listener is isolated and logged. Each event reaches
|
|
84
|
+
* both that collection's {@link onCollectionChange} subscribers and every
|
|
85
|
+
* {@link onAnyCollectionChange} catch-all subscriber.
|
|
86
|
+
*
|
|
87
|
+
* `tailId` is the `CommitRequest.tailId` on the commit path; `undefined` on read-driven
|
|
88
|
+
* promotions (the get/emitPromotions path has no commit request). A single commit is for one
|
|
89
|
+
* collection's chain in practice, so all events from one commit share the same `tailId`.
|
|
90
|
+
*/
|
|
91
|
+
private emitCollectionChanges(collectionBlocks: Map<CollectionId, BlockId[]>, actionId: ActionId, rev: number, tailId?: BlockId): void {
|
|
92
|
+
const hasCatchAll = this.anyChangeListeners.size > 0;
|
|
93
|
+
for (const [collectionId, blockIds] of collectionBlocks) {
|
|
94
|
+
const listeners = this.changeListeners.get(collectionId);
|
|
95
|
+
if ((!listeners || listeners.size === 0) && !hasCatchAll) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const event: CollectionChangeEvent = { collectionId, blockIds, actionId, rev, tailId };
|
|
99
|
+
if (listeners && listeners.size > 0) {
|
|
100
|
+
this.fireChangeListeners(listeners, event);
|
|
101
|
+
}
|
|
102
|
+
if (hasCatchAll) {
|
|
103
|
+
this.fireChangeListeners(this.anyChangeListeners, event);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Dispatch `event` to a snapshot of `listeners` (safe under mid-emit (un)subscribe), isolating + logging any throw. */
|
|
109
|
+
private fireChangeListeners(listeners: Set<CollectionChangeListener>, event: CollectionChangeEvent): void {
|
|
110
|
+
for (const listener of Array.from(listeners)) {
|
|
111
|
+
try {
|
|
112
|
+
listener(event);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
log('onCollectionChange listener threw for collection=%s: %o', event.collectionId, err);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
32
119
|
async get({ blockIds, context }: BlockGets, _options?: MessageOptions): Promise<GetBlockResults> {
|
|
33
120
|
const distinctBlockIds = Array.from(new Set(blockIds));
|
|
34
121
|
log('get blockIds=%d', distinctBlockIds.length);
|
|
122
|
+
// Read-driven promotions that land durably here, captured so we can emit a
|
|
123
|
+
// change event per durable landing after the parallel reads complete (mirrors
|
|
124
|
+
// commit's "emit after the work" ordering). The array is shared across the
|
|
125
|
+
// parallel map closures below — safe because each push happens synchronously
|
|
126
|
+
// between awaits (single-threaded), never concurrently.
|
|
127
|
+
const promotions: { collectionId: CollectionId, blockId: BlockId, actionId: ActionId, rev: number }[] = [];
|
|
35
128
|
const results = await Promise.all(distinctBlockIds.map(async (blockId) => {
|
|
36
129
|
const blockStorage = this.createBlockStorage(blockId);
|
|
37
130
|
|
|
@@ -44,7 +137,10 @@ export class StorageRepo implements IRepo {
|
|
|
44
137
|
for (const { actionId, rev } of missing.sort((a, b) => a.rev - b.rev)) {
|
|
45
138
|
const pending = await blockStorage.getPendingTransaction(actionId);
|
|
46
139
|
if (pending) {
|
|
47
|
-
await this.internalCommit(blockId, actionId, rev, blockStorage);
|
|
140
|
+
const collectionId = await this.internalCommit(blockId, actionId, rev, blockStorage);
|
|
141
|
+
if (collectionId !== undefined) {
|
|
142
|
+
promotions.push({ collectionId, blockId, actionId, rev });
|
|
143
|
+
}
|
|
48
144
|
}
|
|
49
145
|
}
|
|
50
146
|
}
|
|
@@ -82,9 +178,42 @@ export class StorageRepo implements IRepo {
|
|
|
82
178
|
}
|
|
83
179
|
}];
|
|
84
180
|
}));
|
|
181
|
+
|
|
182
|
+
// Emit per durable read-driven landing (Option A — emit eagerly). Done after the
|
|
183
|
+
// parallel reads complete so emission stays outside the per-block work, matching
|
|
184
|
+
// commit's ordering. No-op when nothing was promoted.
|
|
185
|
+
this.emitPromotions(promotions);
|
|
186
|
+
|
|
85
187
|
return Object.fromEntries(results);
|
|
86
188
|
}
|
|
87
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Emit a {@link CollectionChangeEvent} for each read-driven promotion that landed
|
|
192
|
+
* during a {@link get}. A single get() can promote multiple distinct actions, each
|
|
193
|
+
* at its own `(actionId, rev)`, so group by `(actionId, rev)` and route each group
|
|
194
|
+
* through {@link emitCollectionChanges} once.
|
|
195
|
+
*/
|
|
196
|
+
private emitPromotions(promotions: { collectionId: CollectionId, blockId: BlockId, actionId: ActionId, rev: number }[]): void {
|
|
197
|
+
if (promotions.length === 0) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const groups = new Map<string, { actionId: ActionId, rev: number, collectionBlocks: Map<CollectionId, BlockId[]> }>();
|
|
201
|
+
for (const { collectionId, blockId, actionId, rev } of promotions) {
|
|
202
|
+
const key = `${actionId} ${rev}`;
|
|
203
|
+
let group = groups.get(key);
|
|
204
|
+
if (!group) {
|
|
205
|
+
group = { actionId, rev, collectionBlocks: new Map() };
|
|
206
|
+
groups.set(key, group);
|
|
207
|
+
}
|
|
208
|
+
const list = group.collectionBlocks.get(collectionId) ?? [];
|
|
209
|
+
list.push(blockId);
|
|
210
|
+
group.collectionBlocks.set(collectionId, list);
|
|
211
|
+
}
|
|
212
|
+
for (const { actionId, rev, collectionBlocks } of groups.values()) {
|
|
213
|
+
this.emitCollectionChanges(collectionBlocks, actionId, rev);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
88
217
|
async pend(request: PendRequest, _options?: MessageOptions): Promise<PendResult> {
|
|
89
218
|
// Validate transaction if present and validation hook is configured
|
|
90
219
|
if (this.validatePend && request.transaction && request.operationsHash) {
|
|
@@ -194,6 +323,14 @@ export class StorageRepo implements IRepo {
|
|
|
194
323
|
log('commit actionId=%s rev=%d blockIds=%d', request.actionId, request.rev, request.blockIds.length);
|
|
195
324
|
const uniqueBlockIds = Array.from(new Set(request.blockIds)).sort();
|
|
196
325
|
const releases: (() => void)[] = [];
|
|
326
|
+
// Collects the blocks newly committed in this call, grouped by collection,
|
|
327
|
+
// so we can emit change events once locks are released. Blocks that land before
|
|
328
|
+
// a mid-loop failure stay here and are still emitted (Option A — emit eagerly):
|
|
329
|
+
// they are durably committed and a retry rolls the remainder forward.
|
|
330
|
+
const collectionBlocks = new Map<CollectionId, BlockId[]>();
|
|
331
|
+
// Captured when internalCommit throws mid-loop; we break (rather than return)
|
|
332
|
+
// so locks release and accumulated landings still emit before we report failure.
|
|
333
|
+
let failure: { reason: string } | undefined;
|
|
197
334
|
|
|
198
335
|
try {
|
|
199
336
|
// Acquire locks sequentially based on sorted IDs to prevent deadlocks
|
|
@@ -273,14 +410,19 @@ export class StorageRepo implements IRepo {
|
|
|
273
410
|
for (const { blockId, storage } of toCommit) {
|
|
274
411
|
try {
|
|
275
412
|
// internalCommit will throw if it encounters an issue
|
|
276
|
-
await this.internalCommit(blockId, request.actionId, request.rev, storage);
|
|
413
|
+
const collectionId = await this.internalCommit(blockId, request.actionId, request.rev, storage);
|
|
414
|
+
if (collectionId !== undefined) {
|
|
415
|
+
const list = collectionBlocks.get(collectionId) ?? [];
|
|
416
|
+
list.push(blockId);
|
|
417
|
+
collectionBlocks.set(collectionId, list);
|
|
418
|
+
}
|
|
277
419
|
} catch (err) {
|
|
278
|
-
// Partial-commit recovery:
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
420
|
+
// Partial-commit recovery: blocks already in collectionBlocks DID land
|
|
421
|
+
// durably and must still emit; a retry with the same (actionId, rev)
|
|
422
|
+
// treats them as idempotent no-ops and advances the remainder. Break
|
|
423
|
+
// instead of returning so locks release and those landings emit below.
|
|
424
|
+
failure = { reason: err instanceof Error ? err.message : 'Unknown error during commit' };
|
|
425
|
+
break;
|
|
284
426
|
}
|
|
285
427
|
}
|
|
286
428
|
}
|
|
@@ -289,7 +431,12 @@ export class StorageRepo implements IRepo {
|
|
|
289
431
|
releases.reverse().forEach(release => release());
|
|
290
432
|
}
|
|
291
433
|
|
|
292
|
-
|
|
434
|
+
// Notify after the critical section, for every block newly committed here —
|
|
435
|
+
// including those that landed before a mid-loop failure (alreadyDone / stale
|
|
436
|
+
// partitions never reach `collectionBlocks`).
|
|
437
|
+
this.emitCollectionChanges(collectionBlocks, request.actionId, request.rev, request.tailId);
|
|
438
|
+
|
|
439
|
+
return failure ? { success: false, reason: failure.reason } : { success: true };
|
|
293
440
|
}
|
|
294
441
|
|
|
295
442
|
/**
|
|
@@ -305,7 +452,50 @@ export class StorageRepo implements IRepo {
|
|
|
305
452
|
await storage.recover();
|
|
306
453
|
}
|
|
307
454
|
|
|
308
|
-
|
|
455
|
+
/**
|
|
456
|
+
* Persist a replica of a block received out-of-band (churn re-replication) into
|
|
457
|
+
* local storage. Distinct from the {@link IRepo} commit funnel: the block arrives
|
|
458
|
+
* already materialized from a departing owner, not as a pend/commit. See
|
|
459
|
+
* {@link IBlockStorage.saveReplica} for the durability/monotonicity contract.
|
|
460
|
+
*
|
|
461
|
+
* Held under the same `StorageRepo.commit:<id>` latch as {@link commit} so the
|
|
462
|
+
* replica's read-modify-write of `latest` is mutually exclusive with a concurrent
|
|
463
|
+
* local commit on the same block — otherwise `saveReplica`'s monotonic guard could
|
|
464
|
+
* read a stale `latest` and clobber a commit that advanced it in between.
|
|
465
|
+
*/
|
|
466
|
+
async saveReplicatedBlock(blockId: BlockId, block: IBlock, source?: ActionRev): Promise<void> {
|
|
467
|
+
log('saveReplicatedBlock blockId=%s rev=%s', blockId, source?.rev);
|
|
468
|
+
const storage = this.createBlockStorage(blockId);
|
|
469
|
+
const release = await Latches.acquire(`StorageRepo.commit:${blockId}`);
|
|
470
|
+
// Captured under the latch; emitted after release to match commit's ordering.
|
|
471
|
+
let landed: { collectionId: CollectionId, actionId: ActionId, rev: number } | undefined;
|
|
472
|
+
try {
|
|
473
|
+
const priorLatest = await storage.getLatest();
|
|
474
|
+
const effective = await storage.saveReplica(block, source);
|
|
475
|
+
// Advanced iff there was no prior revision or the effective rev moved past it. On the
|
|
476
|
+
// monotonic no-op, saveReplica returns the held latest unchanged → effective.rev === priorLatest.rev.
|
|
477
|
+
const advanced = priorLatest === undefined || effective.rev > priorLatest.rev;
|
|
478
|
+
const collectionId = block.header?.collectionId;
|
|
479
|
+
if (advanced && collectionId !== undefined) {
|
|
480
|
+
landed = { collectionId, actionId: effective.actionId, rev: effective.rev };
|
|
481
|
+
}
|
|
482
|
+
} finally {
|
|
483
|
+
release();
|
|
484
|
+
}
|
|
485
|
+
// Replica-persist has no CommitRequest, hence no tailId — like a read-driven promotion,
|
|
486
|
+
// this wakes local onCollectionChange watchers but is cert-gated out of cohort-topic
|
|
487
|
+
// re-origination downstream (change-bridge selfIsCohortMember treats a tail-less event as
|
|
488
|
+
// never a member).
|
|
489
|
+
if (landed) {
|
|
490
|
+
this.emitCollectionChanges(
|
|
491
|
+
new Map([[landed.collectionId, [blockId]]]),
|
|
492
|
+
landed.actionId,
|
|
493
|
+
landed.rev,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private async internalCommit(blockId: BlockId, actionId: ActionId, rev: number, storage: IBlockStorage): Promise<CollectionId | undefined> {
|
|
309
499
|
// Note: This method is called within the locked critical section of commit()
|
|
310
500
|
// So, operations like getPendingTransaction, getLatest, getBlock, saveMaterializedBlock,
|
|
311
501
|
// saveRevision, promotePendingTransaction, setLatest are protected against
|
|
@@ -340,6 +530,12 @@ export class StorageRepo implements IRepo {
|
|
|
340
530
|
|
|
341
531
|
// Update latest revision *last*
|
|
342
532
|
await storage.setLatest({ actionId, rev });
|
|
533
|
+
|
|
534
|
+
// Report the affected collection for change-event routing. For a delete the
|
|
535
|
+
// materialized block is undefined, so fall back to the prior block's header.
|
|
536
|
+
// Either may be absent only for a malformed/headerless block — return
|
|
537
|
+
// undefined so the caller skips it rather than emitting a bogus event.
|
|
538
|
+
return newBlock?.header.collectionId ?? priorBlock?.header.collectionId;
|
|
343
539
|
}
|
|
344
540
|
}
|
|
345
541
|
|
package/src/sync/client.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { PeerId } from '@libp2p/interface';
|
|
|
2
2
|
import type { IPeerNetwork } from '@optimystic/db-core';
|
|
3
3
|
import { ProtocolClient } from '../protocol-client.js';
|
|
4
4
|
import { buildSyncProtocol, type SyncRequest, type SyncResponse } from './protocol.js';
|
|
5
|
+
import { withRpcDeadlineDefaults, type RpcDeadlineOptions } from '../rpc-deadline.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Client for sending sync requests to remote peers.
|
|
@@ -25,11 +26,13 @@ export class SyncClient extends ProtocolClient {
|
|
|
25
26
|
* Request a block from the remote peer.
|
|
26
27
|
*
|
|
27
28
|
* @param request - Sync request specifying block and options
|
|
29
|
+
* @param options - Optional per-call deadlines/cancellation. Absent keys fall
|
|
30
|
+
* back to the client-level defaults so a silent peer can't hang the caller.
|
|
28
31
|
* @returns Response with archive if successful
|
|
29
32
|
* @throws Error if request fails or times out
|
|
30
33
|
*/
|
|
31
|
-
async requestBlock(request: SyncRequest): Promise<SyncResponse> {
|
|
32
|
-
return await this.processMessage<SyncResponse>(request, this.protocol);
|
|
34
|
+
async requestBlock(request: SyncRequest, options?: RpcDeadlineOptions): Promise<SyncResponse> {
|
|
35
|
+
return await this.processMessage<SyncResponse>(request, this.protocol, withRpcDeadlineDefaults(options));
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/**
|