@optimystic/db-p2p 0.24.1 → 0.25.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 → README.md} +688 -668
- package/dist/src/cluster/block-transfer-service.d.ts +133 -10
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +155 -9
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +67 -7
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +127 -24
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/certified-claims.d.ts +170 -0
- package/dist/src/cluster/certified-claims.d.ts.map +1 -0
- package/dist/src/cluster/certified-claims.js +200 -0
- package/dist/src/cluster/certified-claims.js.map +1 -0
- package/dist/src/cluster/client-signature-verifier.d.ts +23 -0
- package/dist/src/cluster/client-signature-verifier.d.ts.map +1 -0
- package/dist/src/cluster/client-signature-verifier.js +33 -0
- package/dist/src/cluster/client-signature-verifier.js.map +1 -0
- package/dist/src/cluster/cluster-policy.d.ts +9 -3
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js +111 -27
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +140 -11
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +485 -29
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-proof.d.ts +134 -0
- package/dist/src/cluster/commit-proof.d.ts.map +1 -0
- package/dist/src/cluster/commit-proof.js +263 -0
- package/dist/src/cluster/commit-proof.js.map +1 -0
- package/dist/src/cluster/quorum-restore.d.ts +95 -8
- package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
- package/dist/src/cluster/quorum-restore.js +113 -14
- package/dist/src/cluster/quorum-restore.js.map +1 -1
- package/dist/src/cluster/rebalance-monitor.d.ts +84 -0
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +207 -5
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/reconcile-block.d.ts +27 -7
- package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
- package/dist/src/cluster/reconcile-block.js +179 -38
- package/dist/src/cluster/reconcile-block.js.map +1 -1
- package/dist/src/cluster/service.d.ts +21 -4
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +28 -17
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts +8 -2
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +7 -7
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +8 -7
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +7 -6
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +14 -41
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/dispute/cascade.d.ts +7 -8
- package/dist/src/dispute/cascade.d.ts.map +1 -1
- package/dist/src/dispute/cascade.js +24 -7
- package/dist/src/dispute/cascade.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts +5 -0
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +13 -3
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/dispute/invalidation.d.ts +33 -15
- package/dist/src/dispute/invalidation.d.ts.map +1 -1
- package/dist/src/dispute/invalidation.js +127 -34
- package/dist/src/dispute/invalidation.js.map +1 -1
- package/dist/src/dispute/service.d.ts.map +1 -1
- package/dist/src/dispute/service.js +2 -1
- package/dist/src/dispute/service.js.map +1 -1
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +78 -6
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +136 -57
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +38 -7
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +150 -62
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +2 -2
- package/dist/src/network/open-protocol-stream.d.ts +56 -0
- package/dist/src/network/open-protocol-stream.d.ts.map +1 -0
- package/dist/src/network/open-protocol-stream.js +60 -0
- package/dist/src/network/open-protocol-stream.js.map +1 -0
- package/dist/src/network/register-protocol-handler.d.ts +66 -0
- package/dist/src/network/register-protocol-handler.d.ts.map +1 -0
- package/dist/src/network/register-protocol-handler.js +51 -0
- package/dist/src/network/register-protocol-handler.js.map +1 -0
- package/dist/src/peer-address-book.d.ts +128 -1
- package/dist/src/peer-address-book.d.ts.map +1 -1
- package/dist/src/peer-address-book.js +198 -15
- package/dist/src/peer-address-book.js.map +1 -1
- package/dist/src/pend-validation.d.ts +65 -0
- package/dist/src/pend-validation.d.ts.map +1 -0
- package/dist/src/pend-validation.js +69 -0
- package/dist/src/pend-validation.js.map +1 -0
- package/dist/src/reactivity/notify-transport.d.ts.map +1 -1
- package/dist/src/reactivity/notify-transport.js +2 -1
- package/dist/src/reactivity/notify-transport.js.map +1 -1
- package/dist/src/reactivity/push-state-gossip.d.ts.map +1 -1
- package/dist/src/reactivity/push-state-gossip.js +2 -1
- package/dist/src/reactivity/push-state-gossip.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +32 -6
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +44 -10
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +181 -25
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +650 -62
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/served-repo-proxy.d.ts +30 -0
- package/dist/src/repo/served-repo-proxy.d.ts.map +1 -0
- package/dist/src/repo/served-repo-proxy.js +50 -0
- package/dist/src/repo/served-repo-proxy.js.map +1 -0
- package/dist/src/repo/service.d.ts +22 -1
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +25 -13
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/reputation/types.d.ts +3 -1
- package/dist/src/reputation/types.d.ts.map +1 -1
- package/dist/src/reputation/types.js +3 -1
- package/dist/src/reputation/types.js.map +1 -1
- package/dist/src/rn.d.ts +24 -2
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +33 -4
- package/dist/src/rn.js.map +1 -1
- package/dist/src/routing/libp2p-known-peers.d.ts.map +1 -1
- package/dist/src/routing/libp2p-known-peers.js +5 -0
- package/dist/src/routing/libp2p-known-peers.js.map +1 -1
- package/dist/src/storage/block-archive.d.ts +153 -0
- package/dist/src/storage/block-archive.d.ts.map +1 -0
- package/dist/src/storage/block-archive.js +194 -0
- package/dist/src/storage/block-archive.js.map +1 -0
- package/dist/src/storage/block-latch.d.ts +92 -0
- package/dist/src/storage/block-latch.d.ts.map +1 -0
- package/dist/src/storage/block-latch.js +132 -0
- package/dist/src/storage/block-latch.js.map +1 -0
- package/dist/src/storage/block-storage.d.ts +169 -12
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +415 -152
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/cached-raw-storage.d.ts +5 -1
- package/dist/src/storage/cached-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/cached-raw-storage.js +16 -1
- package/dist/src/storage/cached-raw-storage.js.map +1 -1
- package/dist/src/storage/cached-store-driver.d.ts +14 -4
- package/dist/src/storage/cached-store-driver.d.ts.map +1 -1
- package/dist/src/storage/cached-store-driver.js +39 -5
- package/dist/src/storage/cached-store-driver.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +70 -19
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-block-storage.js +21 -1
- package/dist/src/storage/i-block-storage.js.map +1 -1
- package/dist/src/storage/i-raw-storage.d.ts +38 -0
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.d.ts +14 -8
- package/dist/src/storage/kv-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.js +29 -9
- package/dist/src/storage/kv-raw-storage.js.map +1 -1
- package/dist/src/storage/memory-store-driver.d.ts +4 -1
- package/dist/src/storage/memory-store-driver.d.ts.map +1 -1
- package/dist/src/storage/memory-store-driver.js +22 -2
- package/dist/src/storage/memory-store-driver.js.map +1 -1
- package/dist/src/storage/raw-store-codec.js +2 -2
- package/dist/src/storage/raw-store-driver.d.ts +44 -3
- package/dist/src/storage/raw-store-driver.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.d.ts +60 -3
- package/dist/src/storage/shared-cache-pool.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.js +68 -4
- package/dist/src/storage/shared-cache-pool.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +204 -26
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +422 -119
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/storage/store-identity.d.ts +21 -0
- package/dist/src/storage/store-identity.d.ts.map +1 -0
- package/dist/src/storage/store-identity.js +33 -0
- package/dist/src/storage/store-identity.js.map +1 -0
- package/dist/src/storage/struct.d.ts +15 -0
- package/dist/src/storage/struct.d.ts.map +1 -1
- package/dist/src/storage/with-read-cache.d.ts +92 -0
- package/dist/src/storage/with-read-cache.d.ts.map +1 -0
- package/dist/src/storage/with-read-cache.js +132 -0
- package/dist/src/storage/with-read-cache.js.map +1 -0
- package/dist/src/sync/service.d.ts +13 -3
- package/dist/src/sync/service.d.ts.map +1 -1
- package/dist/src/sync/service.js +7 -30
- package/dist/src/sync/service.js.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +75 -2
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +193 -81
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/dist/src/testing/raw-storage-conformance.d.ts +8 -0
- package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
- package/dist/src/testing/raw-storage-conformance.js +97 -2
- package/dist/src/testing/raw-storage-conformance.js.map +1 -1
- package/package.json +5 -6
- package/src/cluster/block-transfer-service.ts +528 -312
- package/src/cluster/block-transfer.ts +547 -424
- package/src/cluster/certified-claims.ts +319 -0
- package/src/cluster/client-signature-verifier.ts +34 -0
- package/src/cluster/cluster-policy.ts +294 -203
- package/src/cluster/cluster-repo.ts +2501 -2034
- package/src/cluster/commit-proof.ts +344 -0
- package/src/cluster/quorum-restore.ts +348 -223
- package/src/cluster/rebalance-monitor.ts +539 -254
- package/src/cluster/reconcile-block.ts +404 -203
- package/src/cluster/service.ts +308 -293
- package/src/cluster/spread-on-churn.ts +352 -346
- package/src/cohort-topic/host.ts +8 -7
- package/src/cohort-topic/stream-util.ts +14 -43
- package/src/dispute/cascade.ts +31 -15
- package/src/dispute/dispute-service.ts +706 -696
- package/src/dispute/invalidation.ts +162 -52
- package/src/dispute/service.ts +126 -125
- package/src/index.ts +7 -0
- package/src/libp2p-key-network.ts +142 -58
- package/src/libp2p-node-base.ts +1793 -1675
- package/src/logger.ts +27 -27
- package/src/network/open-protocol-stream.ts +87 -0
- package/src/network/register-protocol-handler.ts +85 -0
- package/src/peer-address-book.ts +360 -149
- package/src/pend-validation.ts +89 -0
- package/src/reactivity/notify-transport.ts +145 -144
- package/src/reactivity/push-state-gossip.ts +292 -291
- package/src/repo/cluster-coordinator.ts +65 -11
- package/src/repo/coordinator-repo.ts +729 -71
- package/src/repo/served-repo-proxy.ts +55 -0
- package/src/repo/service.ts +332 -313
- package/src/reputation/types.ts +3 -1
- package/src/rn.ts +34 -4
- package/src/routing/libp2p-known-peers.ts +31 -26
- package/src/storage/block-archive.ts +242 -0
- package/src/storage/block-latch.ts +144 -0
- package/src/storage/block-storage.ts +787 -498
- package/src/storage/cached-raw-storage.ts +21 -1
- package/src/storage/cached-store-driver.ts +898 -859
- package/src/storage/i-block-storage.ts +172 -116
- package/src/storage/i-raw-storage.ts +40 -0
- package/src/storage/kv-raw-storage.ts +34 -9
- package/src/storage/memory-store-driver.ts +26 -2
- package/src/storage/raw-store-codec.ts +42 -42
- package/src/storage/raw-store-driver.ts +45 -3
- package/src/storage/shared-cache-pool.ts +516 -452
- package/src/storage/storage-repo.ts +1421 -1014
- package/src/storage/store-identity.ts +41 -0
- package/src/storage/struct.ts +52 -34
- package/src/storage/with-read-cache.ts +182 -0
- package/src/sync/service.ts +178 -198
- package/src/testing/cohort-topic-mesh-harness.ts +673 -673
- package/src/testing/mesh-harness.ts +524 -344
- package/src/testing/raw-storage-conformance.ts +114 -2
|
@@ -1,859 +1,898 @@
|
|
|
1
|
-
import type { BlockId, ActionId } from "@optimystic/db-core";
|
|
2
|
-
import type { RawStoreDriver } from "./raw-store-driver.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type
|
|
9
|
-
type
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (a
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
*
|
|
40
|
-
* `
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
let
|
|
46
|
-
let
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
*
|
|
97
|
-
* pool
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* the
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
*
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const revs = entry.value;
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
this.
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
this.
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
this.
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
try {
|
|
742
|
-
|
|
743
|
-
} catch (err) {
|
|
744
|
-
this.
|
|
745
|
-
throw err;
|
|
746
|
-
}
|
|
747
|
-
this.
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
}
|
|
1
|
+
import type { BlockId, ActionId } from "@optimystic/db-core";
|
|
2
|
+
import type { RawStoreDriver } from "./raw-store-driver.js";
|
|
3
|
+
import type { StoreIdentity } from "./store-identity.js";
|
|
4
|
+
import {
|
|
5
|
+
defaultCachePool,
|
|
6
|
+
SharedCachePool,
|
|
7
|
+
CacheStoreHandle,
|
|
8
|
+
type CacheEntryClass,
|
|
9
|
+
type PoolEntry,
|
|
10
|
+
type PoolEntryOwner,
|
|
11
|
+
} from "./shared-cache-pool.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A cached value at the byte layer. `null` records a PROVEN absence — a confirmed miss read
|
|
15
|
+
* through this wrapper, or a delete that went through it. A plain map miss (`undefined`)
|
|
16
|
+
* means "unknown": the wrapper has never observed this key, so the inner driver must be
|
|
17
|
+
* consulted. The distinction is load-bearing for the absence/unavailability logic one layer
|
|
18
|
+
* up: a cached `null` may only ever mean "provably absent", never "could not confirm".
|
|
19
|
+
*/
|
|
20
|
+
type CachedBytes = Uint8Array | null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Inclusive integer interval `[lo, hi]`. Distinct from `RevisionRange` (inclusive-exclusive,
|
|
24
|
+
* open-ended) on purpose: coverage tracking wants closed integer intervals with adjacency
|
|
25
|
+
* merging, and reusing the metadata range type here would invite confusing the two.
|
|
26
|
+
*/
|
|
27
|
+
type CoveredInterval = [lo: number, hi: number];
|
|
28
|
+
|
|
29
|
+
/** True when `[lo, hi]` is entirely inside one covered interval. `covered` is sorted and disjoint. */
|
|
30
|
+
function coversRange(covered: CoveredInterval[], lo: number, hi: number): boolean {
|
|
31
|
+
for (const [a, b] of covered) {
|
|
32
|
+
if (lo >= a && hi <= b) return true;
|
|
33
|
+
if (a > lo) break;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Add `[lo, hi]` to the sorted disjoint interval list, merging overlaps AND integer
|
|
40
|
+
* adjacency (`[1,3]` + `[4,6]` → `[1,6]`), so contiguous point coverage from successive
|
|
41
|
+
* `putRevision` calls collapses into one interval instead of accumulating per-rev entries.
|
|
42
|
+
*/
|
|
43
|
+
function addCovered(covered: CoveredInterval[], lo: number, hi: number): CoveredInterval[] {
|
|
44
|
+
const merged: CoveredInterval[] = [];
|
|
45
|
+
let newLo = lo;
|
|
46
|
+
let newHi = hi;
|
|
47
|
+
let inserted = false;
|
|
48
|
+
for (const [a, b] of covered) {
|
|
49
|
+
if (b < newLo - 1) {
|
|
50
|
+
merged.push([a, b]);
|
|
51
|
+
} else if (a > newHi + 1) {
|
|
52
|
+
if (!inserted) {
|
|
53
|
+
merged.push([newLo, newHi]);
|
|
54
|
+
inserted = true;
|
|
55
|
+
}
|
|
56
|
+
merged.push([a, b]);
|
|
57
|
+
} else {
|
|
58
|
+
newLo = Math.min(newLo, a);
|
|
59
|
+
newHi = Math.max(newHi, b);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!inserted) {
|
|
63
|
+
merged.push([newLo, newHi]);
|
|
64
|
+
}
|
|
65
|
+
return merged;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Per-block revision map: point values plus the intervals over which the cache's knowledge
|
|
70
|
+
* is COMPLETE. A rev inside `covered` with no `byRev` entry is provably absent; a rev
|
|
71
|
+
* outside `covered` is unknown. `gen` increments on every mutation so an in-flight
|
|
72
|
+
* enumeration can detect that a write landed during its inner drain and decline to claim
|
|
73
|
+
* coverage it can no longer prove.
|
|
74
|
+
*/
|
|
75
|
+
interface RevMapState {
|
|
76
|
+
byRev: Map<number, Uint8Array>;
|
|
77
|
+
covered: CoveredInterval[];
|
|
78
|
+
gen: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Per-block pending-id set. `complete` is true only when the set is known to equal the
|
|
83
|
+
* inner driver's set — either seeded by one full enumeration, or maintained from that point
|
|
84
|
+
* by the funnelled writes. The flag lives INSIDE the same entry as the set it describes, so
|
|
85
|
+
* dropping the entry (clear / write-error invalidation / POOL EVICTION) can never strand a
|
|
86
|
+
* half-true completeness claim — an evicted list takes its flag with it, and a fresh entry
|
|
87
|
+
* starts incomplete. `gen` serves the same in-flight-enumeration guard as {@link RevMapState}.
|
|
88
|
+
*/
|
|
89
|
+
interface PendListState {
|
|
90
|
+
ids: Set<ActionId>;
|
|
91
|
+
complete: boolean;
|
|
92
|
+
gen: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* All cached state for one block. Every field holds (or maps to) a {@link PoolEntry} — the
|
|
97
|
+
* value lives ON the pool entry, so residency, byte accounting and eviction are the shared
|
|
98
|
+
* pool's problem while coherence stays here. Entry classes are keyed in the pool as
|
|
99
|
+
* `(storeId, class, key…)`; block ids alone are NOT globally unique (header block ids are
|
|
100
|
+
* name-derived, so two stores running the same schema collide), which is why the store id
|
|
101
|
+
* leads the key.
|
|
102
|
+
*/
|
|
103
|
+
interface BlockCacheState {
|
|
104
|
+
meta?: PoolEntry<CachedBytes>;
|
|
105
|
+
revs?: PoolEntry<RevMapState>;
|
|
106
|
+
pendList?: PoolEntry<PendListState>;
|
|
107
|
+
pending: Map<ActionId, PoolEntry<CachedBytes>>;
|
|
108
|
+
transactions: Map<ActionId, PoolEntry<CachedBytes>>;
|
|
109
|
+
materialized: Map<ActionId, PoolEntry<CachedBytes>>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Approximate bytes for JS-object bookkeeping, charged so tiny/negative entries are not free. */
|
|
113
|
+
const CHAR_BYTES = 2;
|
|
114
|
+
/** Fixed cost of one pool entry: entry object + owner map slot + queue map slot (key string charged separately). */
|
|
115
|
+
const ENTRY_BASE = 256;
|
|
116
|
+
/** Per-revision cost inside a revision map: map slot + boxed rev + Uint8Array header. */
|
|
117
|
+
const REV_SLOT = 56;
|
|
118
|
+
/** Per-interval cost of the coverage list. */
|
|
119
|
+
const INTERVAL_SLOT = 56;
|
|
120
|
+
/** Per-id cost inside a pending-id set (id characters charged separately). */
|
|
121
|
+
const ID_SLOT = 40;
|
|
122
|
+
|
|
123
|
+
function idsContent(ids: Set<ActionId>): number {
|
|
124
|
+
let sum = 0;
|
|
125
|
+
for (const id of ids) sum += ID_SLOT + id.length * CHAR_BYTES;
|
|
126
|
+
return sum;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Write-through coherent cache over any {@link RawStoreDriver}, bounded by a shared
|
|
131
|
+
* {@link SharedCachePool}. Wrap a backend's driver with this before handing it to
|
|
132
|
+
* `KvRawStorage` and every read the kernel issues is served from memory while the entry
|
|
133
|
+
* stays resident in the pool — the pool's 2Q admission keeps hot per-store entries
|
|
134
|
+
* protected while one-off bulk scans die in probation (see the pool's class doc).
|
|
135
|
+
*
|
|
136
|
+
* **Coherence, not eviction policy.** Every save stores its value INTO the cache as it writes
|
|
137
|
+
* the inner driver, rather than invalidating. The cache therefore always holds the last
|
|
138
|
+
* durable value for resident entries and never needs to re-read a key it has seen while that
|
|
139
|
+
* entry survives. This is what a plain invalidate-on-write memo cannot achieve at this seam:
|
|
140
|
+
* the hot reads exist to observe the writes, so invalidation lands between nearly every read
|
|
141
|
+
* pair (measured: only a 23% cut).
|
|
142
|
+
*
|
|
143
|
+
* **Why this is sound** — the invariants in `packages/db-p2p/docs/storage.md` ("Invariants", 1–5):
|
|
144
|
+
* 1. every backend write funnels through `IRawStorage` (and thus this driver) in-process, so
|
|
145
|
+
* the cache sees every mutation;
|
|
146
|
+
* 2. every writer of the metadata blob holds the block write latch, and each cache update is
|
|
147
|
+
* synchronous with its inner write (no `await` between the inner call resolving and the
|
|
148
|
+
* cache mutation), so latch-protected read-after-write sees the new value exactly as a
|
|
149
|
+
* driver read would;
|
|
150
|
+
* 3. committed revisions and materializations are append-only.
|
|
151
|
+
*
|
|
152
|
+
* **Precondition: one process owns the store** (Invariant 5). A second process writing the
|
|
153
|
+
* same backend bypasses this wrapper and makes cached values stale in ways that feed
|
|
154
|
+
* consensus decisions. Not enforced in code — see `packages/db-p2p/docs/storage.md`.
|
|
155
|
+
*
|
|
156
|
+
* **The cache is always clean, never dirty.** No write is deferred, reordered, or coalesced;
|
|
157
|
+
* the inner driver's durable state never depends on cache contents. Eviction of ANY entry at
|
|
158
|
+
* ANY instant is therefore correct — the next read falls through — which is precisely what
|
|
159
|
+
* lets the shared pool evict freely, without pins, dirty tracking, or an exhaustion error.
|
|
160
|
+
* Keep it that way: if a change here ever needs a dirty or pinned entry, the design has gone
|
|
161
|
+
* wrong — stop and reconsider.
|
|
162
|
+
*
|
|
163
|
+
* **Pool discipline** (the one rule new code here must follow): mutate this wrapper's own
|
|
164
|
+
* state FIRST, then make the pool call (`admit`/`updated`/`touch`/`drop`) LAST, and never
|
|
165
|
+
* touch cache state after the pool call — pool mutations can synchronously evict any entry,
|
|
166
|
+
* including the one just written. The eviction callback ({@link onPoolEvict}) removes only
|
|
167
|
+
* the wrapper's reference and never calls back into the pool.
|
|
168
|
+
*
|
|
169
|
+
* **Values are cached as bytes**, exactly the encoded value the kernel wrote or the driver
|
|
170
|
+
* returned. The kernel decodes fresh objects from those bytes on every read, so the
|
|
171
|
+
* clone-on-read/clone-on-write discipline stays structural (same argument as the codec
|
|
172
|
+
* boundary itself), and an entry's byte size feeds the pool's budget directly.
|
|
173
|
+
* NOTE: every cache hit still pays one JSON.parse in the kernel; if decode ever shows up hot
|
|
174
|
+
* on large materialized blocks, cache decoded objects plus an explicit clone instead.
|
|
175
|
+
*
|
|
176
|
+
* **Proofs are a passthrough, not a cached store.** The proofs store is read only on repair
|
|
177
|
+
* and archive-serving paths; see the NOTE at {@link getProof}.
|
|
178
|
+
*
|
|
179
|
+
* **Do not wrap `MemoryStoreDriver`.** The memory driver already holds the same byte
|
|
180
|
+
* references in maps; wrapping it duplicates every map entry (the bytes themselves are
|
|
181
|
+
* shared, so no byte copy — but the bookkeeping is pure overhead with nothing to save).
|
|
182
|
+
*/
|
|
183
|
+
export class CachedStoreDriver implements RawStoreDriver, PoolEntryOwner {
|
|
184
|
+
private readonly blocks = new Map<BlockId, BlockCacheState>();
|
|
185
|
+
private readonly store: CacheStoreHandle;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Optional passthroughs are wired only when the inner driver provides them, mirroring
|
|
189
|
+
* `KvRawStorage`'s constructor: feature-detection above this wrapper must observe the
|
|
190
|
+
* inner driver's true capability. `listBlockIds` deliberately bypasses the cache — it
|
|
191
|
+
* enumerates the inner metadata keyspace, which funnelled writes keep authoritative.
|
|
192
|
+
* `close` is the deliberate exception (a real method below): releasing the store's pool
|
|
193
|
+
* registration is THIS wrapper's own capability, needed whether or not the inner driver
|
|
194
|
+
* has anything to close. `storeIdentity` passes through unchanged — a cache names the SAME
|
|
195
|
+
* store as the driver it fronts; it is not a store of its own — and it is also what the
|
|
196
|
+
* constructor hands the pool, so a second cache over an already-cached store is refused
|
|
197
|
+
* there (see {@link SharedCachePool.registerStore}).
|
|
198
|
+
*/
|
|
199
|
+
listBlockIds?: () => AsyncIterable<BlockId>;
|
|
200
|
+
approximateBytesUsed?: () => Promise<number>;
|
|
201
|
+
storeIdentity?: () => StoreIdentity;
|
|
202
|
+
|
|
203
|
+
constructor(
|
|
204
|
+
private readonly inner: RawStoreDriver,
|
|
205
|
+
private readonly pool: SharedCachePool = defaultCachePool(),
|
|
206
|
+
label?: string,
|
|
207
|
+
) {
|
|
208
|
+
// Registration is the choke point every construction path shares (this constructor, the
|
|
209
|
+
// `CachedRawStorage` wrapper, `withReadCache`), so naming the backing store here is what
|
|
210
|
+
// lets the pool refuse a second, permanently divergent view of it. Throws before anything else is
|
|
211
|
+
// wired; the half-built driver is never returned and holds nothing to clean up.
|
|
212
|
+
this.store = pool.registerStore(label, inner.storeIdentity?.());
|
|
213
|
+
if (inner.storeIdentity) {
|
|
214
|
+
this.storeIdentity = () => inner.storeIdentity!();
|
|
215
|
+
}
|
|
216
|
+
if (inner.listBlockIds) {
|
|
217
|
+
this.listBlockIds = () => inner.listBlockIds!();
|
|
218
|
+
}
|
|
219
|
+
if (inner.approximateBytesUsed) {
|
|
220
|
+
this.approximateBytesUsed = () => inner.approximateBytesUsed!();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Drop every cached entry (releasing their pool residency) and this store's ghost keys —
|
|
226
|
+
* pre-clear recency must not fast-track post-clear refills into the protected queue.
|
|
227
|
+
* Always safe (the cache is clean — see class doc); the next read of each key re-consults
|
|
228
|
+
* the inner driver. In-flight enumerations notice the identity change of their entry
|
|
229
|
+
* objects and decline to claim completeness.
|
|
230
|
+
*/
|
|
231
|
+
clear(): void {
|
|
232
|
+
for (const s of this.blocks.values()) {
|
|
233
|
+
if (s.meta) this.pool.drop(s.meta);
|
|
234
|
+
if (s.revs) this.pool.drop(s.revs);
|
|
235
|
+
if (s.pendList) this.pool.drop(s.pendList);
|
|
236
|
+
for (const e of s.pending.values()) this.pool.drop(e);
|
|
237
|
+
for (const e of s.transactions.values()) this.pool.drop(e);
|
|
238
|
+
for (const e of s.materialized.values()) this.pool.drop(e);
|
|
239
|
+
}
|
|
240
|
+
this.blocks.clear();
|
|
241
|
+
this.pool.purgeGhosts(this.store);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Release this store entirely: drop all entries, unregister from the pool (the store id
|
|
246
|
+
* is never reused), and close the inner driver if it can be closed. A departed workspace
|
|
247
|
+
* that skips this leaks only cold entries the pool will evict under pressure — but call
|
|
248
|
+
* it; on a long-lived provider node the polite release is what keeps occupancy honest.
|
|
249
|
+
*/
|
|
250
|
+
async close(): Promise<void> {
|
|
251
|
+
// NOTE: `unregisterStore` must stay in this method's SYNCHRONOUS prefix — before any
|
|
252
|
+
// `await`. `withReadCache`'s last release retires its registry entry and calls
|
|
253
|
+
// `dispose()` in one synchronous block, so today the pool's identity claim is freed in
|
|
254
|
+
// that same block. Put an await ahead of this line and a re-wrap of the same store
|
|
255
|
+
// between the retire and the unregister would find the registry empty, construct a new
|
|
256
|
+
// cache, and hit `registerStore`'s guard on a claim that is merely late to be released.
|
|
257
|
+
this.clear();
|
|
258
|
+
this.pool.unregisterStore(this.store);
|
|
259
|
+
await this.inner.close?.();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** This store's live pool occupancy; pool-wide numbers come from `SharedCachePool.stats()`. */
|
|
263
|
+
storeStats(): { id: string; label: string | undefined; bytes: number; entries: number } {
|
|
264
|
+
return { id: this.store.id, label: this.store.label, bytes: this.store.bytes, entries: this.store.entries };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Pool eviction callback: remove OUR reference to the entry (identity-checked, so a stale
|
|
269
|
+
* callback can never delete a successor) and reap the block state if that emptied it.
|
|
270
|
+
* Runs synchronously inside a pool mutation — must not call back into the pool.
|
|
271
|
+
*/
|
|
272
|
+
onPoolEvict(entry: PoolEntry): void {
|
|
273
|
+
const s = this.blocks.get(entry.blockId);
|
|
274
|
+
if (s === undefined) return;
|
|
275
|
+
switch (entry.cls) {
|
|
276
|
+
case 'meta': if (s.meta === entry) s.meta = undefined; break;
|
|
277
|
+
case 'revs': if (s.revs === entry) s.revs = undefined; break;
|
|
278
|
+
case 'pendList': if (s.pendList === entry) s.pendList = undefined; break;
|
|
279
|
+
case 'pending': evictFromMap(s.pending, entry); break;
|
|
280
|
+
case 'tx': evictFromMap(s.transactions, entry); break;
|
|
281
|
+
case 'mat': evictFromMap(s.materialized, entry); break;
|
|
282
|
+
}
|
|
283
|
+
this.dropIfEmpty(entry.blockId, s);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* NOTE: this allocates an entry for EVERY block id touched, including ids that turn out
|
|
288
|
+
* not to exist (the proven-absent negative is the point). The shared pool charges those
|
|
289
|
+
* negatives — and the bare bookkeeping of empty container entries — against its budget
|
|
290
|
+
* (`ENTRY_BASE` + key), and {@link dropIfEmpty} reaps block states whose last entry is
|
|
291
|
+
* gone, so a remote peer streaming probes for nonexistent blocks (`StorageRepo.get` is
|
|
292
|
+
* reachable with an arbitrary block-id list) churns the probation queue instead of
|
|
293
|
+
* growing this map without bound.
|
|
294
|
+
*/
|
|
295
|
+
private state(blockId: BlockId): BlockCacheState {
|
|
296
|
+
let s = this.blocks.get(blockId);
|
|
297
|
+
if (!s) {
|
|
298
|
+
s = { pending: new Map(), transactions: new Map(), materialized: new Map() };
|
|
299
|
+
this.blocks.set(blockId, s);
|
|
300
|
+
}
|
|
301
|
+
return s;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Reap a block state holding no entries, so evictions and declined fills leave nothing behind. */
|
|
305
|
+
private dropIfEmpty(blockId: BlockId, s: BlockCacheState): void {
|
|
306
|
+
if (s.meta === undefined && s.revs === undefined && s.pendList === undefined
|
|
307
|
+
&& s.pending.size === 0 && s.transactions.size === 0 && s.materialized.size === 0
|
|
308
|
+
&& this.blocks.get(blockId) === s) {
|
|
309
|
+
this.blocks.delete(blockId);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* An inner read threw after {@link state} allocated: reap the (uncharged) empty state so
|
|
315
|
+
* a stream of probes against a throwing backend cannot grow `blocks` for free.
|
|
316
|
+
*/
|
|
317
|
+
private reapOnThrow(blockId: BlockId, started: BlockCacheState): void {
|
|
318
|
+
if (this.blocks.get(blockId) === started) this.dropIfEmpty(blockId, started);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Install or update a point entry (meta / pending / tx / mat) as the LAST step of a write
|
|
323
|
+
* or read-miss fill. Handles the pool's large-value bypass: an over-limit value is not
|
|
324
|
+
* cached, and an existing entry growing over the limit is dropped to unknown (holding the
|
|
325
|
+
* stale prior value would be wrong; holding the oversized one would defeat the bypass).
|
|
326
|
+
*/
|
|
327
|
+
private cachePoint(
|
|
328
|
+
blockId: BlockId,
|
|
329
|
+
s: BlockCacheState,
|
|
330
|
+
cls: CacheEntryClass,
|
|
331
|
+
actionId: ActionId | undefined,
|
|
332
|
+
bytes: CachedBytes,
|
|
333
|
+
existing: PoolEntry<CachedBytes> | undefined,
|
|
334
|
+
attach: (e: PoolEntry<CachedBytes>) => void,
|
|
335
|
+
detach: () => void,
|
|
336
|
+
): void {
|
|
337
|
+
const len = bytes === null ? 0 : bytes.length;
|
|
338
|
+
if (existing !== undefined) {
|
|
339
|
+
const charge = existing.base + len;
|
|
340
|
+
if (!this.pool.admits(charge)) {
|
|
341
|
+
detach();
|
|
342
|
+
this.pool.drop(existing);
|
|
343
|
+
this.dropIfEmpty(blockId, s);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
existing.value = bytes;
|
|
347
|
+
this.pool.updated(existing, charge);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const key = this.pool.keyFor(this.store, cls, blockId, actionId);
|
|
351
|
+
const base = ENTRY_BASE + key.length * CHAR_BYTES;
|
|
352
|
+
if (!this.pool.admits(base + len)) {
|
|
353
|
+
this.dropIfEmpty(blockId, s);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const entry: PoolEntry<CachedBytes> = {
|
|
357
|
+
key, store: this.store, owner: this, cls, blockId, actionId,
|
|
358
|
+
value: bytes, base, charge: base + len, where: 'none',
|
|
359
|
+
};
|
|
360
|
+
attach(entry);
|
|
361
|
+
this.pool.admit(entry);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Cache a read-miss result into one of the map-backed entry classes, under BOTH fill
|
|
366
|
+
* guards. `started` is the state object the read began on:
|
|
367
|
+
*
|
|
368
|
+
* - **value guard** — fill only while the entry is still unknown, so a newer funnelled
|
|
369
|
+
* write that landed during the inner read is not clobbered by this older value;
|
|
370
|
+
* - **identity guard** — decline entirely if `clear()` (or the empty-state reaper)
|
|
371
|
+
* swapped the state object during the inner read. Without it a resumed read reinstalls
|
|
372
|
+
* its pre-clear value into the fresh state, and the newer write that superseded it went
|
|
373
|
+
* out with the old state — so the stale value would then be served forever. The
|
|
374
|
+
* revision and pending-list paths make the same identity check against their own pool
|
|
375
|
+
* entries.
|
|
376
|
+
*/
|
|
377
|
+
private fillMiss(
|
|
378
|
+
blockId: BlockId,
|
|
379
|
+
started: BlockCacheState,
|
|
380
|
+
map: Map<ActionId, PoolEntry<CachedBytes>>,
|
|
381
|
+
cls: CacheEntryClass,
|
|
382
|
+
actionId: ActionId,
|
|
383
|
+
bytes: Uint8Array | undefined
|
|
384
|
+
): void {
|
|
385
|
+
// `blocks.get`, not `state()` — a declined fill must not resurrect the block entry.
|
|
386
|
+
if (this.blocks.get(blockId) !== started) return;
|
|
387
|
+
if (map.get(actionId) !== undefined) return;
|
|
388
|
+
this.cachePoint(blockId, started, cls, actionId, bytes ?? null, undefined,
|
|
389
|
+
e => map.set(actionId, e), () => map.delete(actionId));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// --- metadata ---
|
|
393
|
+
|
|
394
|
+
async getMetadata(blockId: BlockId): Promise<Uint8Array | undefined> {
|
|
395
|
+
const s = this.state(blockId);
|
|
396
|
+
const hit = s.meta;
|
|
397
|
+
if (hit !== undefined) {
|
|
398
|
+
this.pool.touch(hit);
|
|
399
|
+
return hit.value ?? undefined;
|
|
400
|
+
}
|
|
401
|
+
let bytes: Uint8Array | undefined;
|
|
402
|
+
try {
|
|
403
|
+
bytes = await this.inner.getMetadata(blockId);
|
|
404
|
+
} catch (err) {
|
|
405
|
+
this.reapOnThrow(blockId, s);
|
|
406
|
+
throw err;
|
|
407
|
+
}
|
|
408
|
+
// Same two fill guards as {@link fillMiss} (value + state identity), spelled out here
|
|
409
|
+
// because metadata is a property rather than a map entry. Caching the miss (`null`) is
|
|
410
|
+
// deliberate: everything that creates metadata funnels through this wrapper, so a
|
|
411
|
+
// confirmed miss stays provably absent until a funnelled write overwrites it —
|
|
412
|
+
// repeated probes of not-yet-created blocks are a real cold-start pattern.
|
|
413
|
+
if (this.blocks.get(blockId) === s && s.meta === undefined) {
|
|
414
|
+
this.cachePoint(blockId, s, 'meta', undefined, bytes ?? null, undefined,
|
|
415
|
+
e => { s.meta = e; }, () => { s.meta = undefined; });
|
|
416
|
+
}
|
|
417
|
+
return bytes;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async putMetadata(blockId: BlockId, value: Uint8Array): Promise<void> {
|
|
421
|
+
try {
|
|
422
|
+
await this.inner.putMetadata(blockId, value);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
// The inner write failed with the backend in an unknown state — drop to unknown so
|
|
425
|
+
// the next read consults the driver. Same recovery shape on every write path below.
|
|
426
|
+
const s = this.blocks.get(blockId);
|
|
427
|
+
const m = s?.meta;
|
|
428
|
+
if (s !== undefined && m !== undefined) {
|
|
429
|
+
s.meta = undefined;
|
|
430
|
+
this.pool.drop(m);
|
|
431
|
+
this.dropIfEmpty(blockId, s);
|
|
432
|
+
}
|
|
433
|
+
throw err;
|
|
434
|
+
}
|
|
435
|
+
const s = this.state(blockId);
|
|
436
|
+
this.cachePoint(blockId, s, 'meta', undefined, value, s.meta,
|
|
437
|
+
e => { s.meta = e; }, () => { s.meta = undefined; });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// --- revisions ---
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Get or eagerly admit the block's revision-map entry. Eager (before any inner await) so
|
|
444
|
+
* the fill guards have a stable identity to check across the await; the empty container
|
|
445
|
+
* is charged its base cost, so probe streams pay for their bookkeeping. Returns
|
|
446
|
+
* `undefined` when the pool bypasses even the base charge (absurdly small budget) — the
|
|
447
|
+
* callers then pass straight through to the inner driver, uncached.
|
|
448
|
+
*/
|
|
449
|
+
private ensureRevs(blockId: BlockId): PoolEntry<RevMapState> | undefined {
|
|
450
|
+
const s = this.state(blockId);
|
|
451
|
+
if (s.revs !== undefined) return s.revs;
|
|
452
|
+
const key = this.pool.keyFor(this.store, 'revs', blockId);
|
|
453
|
+
const base = ENTRY_BASE + key.length * CHAR_BYTES;
|
|
454
|
+
if (!this.pool.admits(base)) {
|
|
455
|
+
this.dropIfEmpty(blockId, s);
|
|
456
|
+
return undefined;
|
|
457
|
+
}
|
|
458
|
+
const entry: PoolEntry<RevMapState> = {
|
|
459
|
+
key, store: this.store, owner: this, cls: 'revs', blockId, actionId: undefined,
|
|
460
|
+
value: { byRev: new Map(), covered: [], gen: 0 }, base, charge: base, where: 'none',
|
|
461
|
+
};
|
|
462
|
+
s.revs = entry;
|
|
463
|
+
this.pool.admit(entry);
|
|
464
|
+
return entry;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async getRevision(blockId: BlockId, rev: number): Promise<Uint8Array | undefined> {
|
|
468
|
+
const entry = this.ensureRevs(blockId);
|
|
469
|
+
if (entry === undefined) return this.inner.getRevision(blockId, rev);
|
|
470
|
+
const revs = entry.value;
|
|
471
|
+
if (coversRange(revs.covered, rev, rev)) {
|
|
472
|
+
this.pool.touch(entry);
|
|
473
|
+
return revs.byRev.get(rev);
|
|
474
|
+
}
|
|
475
|
+
const bytes = await this.inner.getRevision(blockId, rev);
|
|
476
|
+
const current = this.blocks.get(blockId)?.revs;
|
|
477
|
+
if (current === entry && !coversRange(revs.covered, rev, rev)) {
|
|
478
|
+
let charge = entry.charge;
|
|
479
|
+
if (bytes !== undefined) {
|
|
480
|
+
revs.byRev.set(rev, bytes);
|
|
481
|
+
charge += REV_SLOT + bytes.length;
|
|
482
|
+
}
|
|
483
|
+
const before = revs.covered.length;
|
|
484
|
+
revs.covered = addCovered(revs.covered, rev, rev);
|
|
485
|
+
charge += (revs.covered.length - before) * INTERVAL_SLOT;
|
|
486
|
+
this.pool.updated(entry, charge);
|
|
487
|
+
}
|
|
488
|
+
return bytes;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async putRevision(blockId: BlockId, rev: number, value: Uint8Array): Promise<void> {
|
|
492
|
+
try {
|
|
493
|
+
await this.inner.putRevision(blockId, rev, value);
|
|
494
|
+
} catch (err) {
|
|
495
|
+
const s = this.blocks.get(blockId);
|
|
496
|
+
const entry = s?.revs;
|
|
497
|
+
if (s !== undefined && entry !== undefined) {
|
|
498
|
+
s.revs = undefined;
|
|
499
|
+
this.pool.drop(entry);
|
|
500
|
+
this.dropIfEmpty(blockId, s);
|
|
501
|
+
}
|
|
502
|
+
throw err;
|
|
503
|
+
}
|
|
504
|
+
const entry = this.ensureRevs(blockId);
|
|
505
|
+
if (entry === undefined) return;
|
|
506
|
+
const revs = entry.value;
|
|
507
|
+
const had = revs.byRev.get(rev);
|
|
508
|
+
revs.byRev.set(rev, value);
|
|
509
|
+
let charge = entry.charge + (had !== undefined ? value.length - had.length : REV_SLOT + value.length);
|
|
510
|
+
const before = revs.covered.length;
|
|
511
|
+
revs.covered = addCovered(revs.covered, rev, rev);
|
|
512
|
+
charge += (revs.covered.length - before) * INTERVAL_SLOT;
|
|
513
|
+
revs.gen++;
|
|
514
|
+
this.pool.updated(entry, charge);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async *rangeRevisions(blockId: BlockId, lo: number, hi: number, reverse: boolean): AsyncIterable<[number, Uint8Array]> {
|
|
518
|
+
const entry = this.ensureRevs(blockId);
|
|
519
|
+
if (entry === undefined) {
|
|
520
|
+
const drained: [number, Uint8Array][] = [];
|
|
521
|
+
for await (const item of this.inner.rangeRevisions(blockId, lo, hi, reverse)) {
|
|
522
|
+
drained.push(item);
|
|
523
|
+
}
|
|
524
|
+
yield* drained;
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const revs = entry.value;
|
|
528
|
+
if (coversRange(revs.covered, lo, hi)) {
|
|
529
|
+
this.pool.touch(entry);
|
|
530
|
+
// Snapshot before yielding (drain-before-yield): concurrent writes during the
|
|
531
|
+
// consumer's awaits must not mutate what this iteration yields.
|
|
532
|
+
// NOTE: this walks every integer in [lo, hi], not just the present revs — fine
|
|
533
|
+
// while revisions are dense (one per commit) and callers bound `hi` by a real
|
|
534
|
+
// `latest.rev`. If a sparse or very wide range ever appears here, iterate
|
|
535
|
+
// `byRev`'s keys sorted instead.
|
|
536
|
+
const out: [number, Uint8Array][] = [];
|
|
537
|
+
if (reverse) {
|
|
538
|
+
for (let rev = hi; rev >= lo; rev--) {
|
|
539
|
+
const bytes = revs.byRev.get(rev);
|
|
540
|
+
if (bytes !== undefined) out.push([rev, bytes]);
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
for (let rev = lo; rev <= hi; rev++) {
|
|
544
|
+
const bytes = revs.byRev.get(rev);
|
|
545
|
+
if (bytes !== undefined) out.push([rev, bytes]);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
yield* out;
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const gen = revs.gen;
|
|
552
|
+
const drained: [number, Uint8Array][] = [];
|
|
553
|
+
for await (const item of this.inner.rangeRevisions(blockId, lo, hi, reverse)) {
|
|
554
|
+
drained.push(item);
|
|
555
|
+
}
|
|
556
|
+
// Claim coverage of [lo, hi] only if nothing mutated the map during the drain (gen
|
|
557
|
+
// unchanged, same entry — clear() and pool eviction both swap the entry). A write that
|
|
558
|
+
// landed mid-drain may be missing from `drained`, and recording coverage anyway would
|
|
559
|
+
// turn that miss into a provably-wrong "rev absent" answer forever after.
|
|
560
|
+
const current = this.blocks.get(blockId)?.revs;
|
|
561
|
+
if (current === entry && revs.gen === gen) {
|
|
562
|
+
let charge = entry.charge;
|
|
563
|
+
for (const [rev, bytes] of drained) {
|
|
564
|
+
const had = revs.byRev.get(rev);
|
|
565
|
+
revs.byRev.set(rev, bytes);
|
|
566
|
+
charge += had !== undefined ? bytes.length - had.length : REV_SLOT + bytes.length;
|
|
567
|
+
}
|
|
568
|
+
const before = revs.covered.length;
|
|
569
|
+
revs.covered = addCovered(revs.covered, lo, hi);
|
|
570
|
+
charge += (revs.covered.length - before) * INTERVAL_SLOT;
|
|
571
|
+
this.pool.updated(entry, charge);
|
|
572
|
+
}
|
|
573
|
+
yield* drained;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// --- pending ---
|
|
577
|
+
|
|
578
|
+
/** Same eager-admission shape (and bypass fallback) as {@link ensureRevs}, for the pending-id set. */
|
|
579
|
+
private ensurePendList(blockId: BlockId): PoolEntry<PendListState> | undefined {
|
|
580
|
+
const s = this.state(blockId);
|
|
581
|
+
if (s.pendList !== undefined) return s.pendList;
|
|
582
|
+
const key = this.pool.keyFor(this.store, 'pendList', blockId);
|
|
583
|
+
const base = ENTRY_BASE + key.length * CHAR_BYTES;
|
|
584
|
+
if (!this.pool.admits(base)) {
|
|
585
|
+
this.dropIfEmpty(blockId, s);
|
|
586
|
+
return undefined;
|
|
587
|
+
}
|
|
588
|
+
const entry: PoolEntry<PendListState> = {
|
|
589
|
+
key, store: this.store, owner: this, cls: 'pendList', blockId, actionId: undefined,
|
|
590
|
+
value: { ids: new Set(), complete: false, gen: 0 }, base, charge: base, where: 'none',
|
|
591
|
+
};
|
|
592
|
+
s.pendList = entry;
|
|
593
|
+
this.pool.admit(entry);
|
|
594
|
+
return entry;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Mirror a funnelled pend into the id set, if one is resident. Re-reads the entry — the write that preceded this call may have evicted it. */
|
|
598
|
+
private notePendListAdd(blockId: BlockId, actionId: ActionId): void {
|
|
599
|
+
const entry = this.blocks.get(blockId)?.pendList;
|
|
600
|
+
if (entry === undefined) return;
|
|
601
|
+
const list = entry.value;
|
|
602
|
+
let charge = entry.charge;
|
|
603
|
+
if (!list.ids.has(actionId)) {
|
|
604
|
+
list.ids.add(actionId);
|
|
605
|
+
charge += ID_SLOT + actionId.length * CHAR_BYTES;
|
|
606
|
+
}
|
|
607
|
+
list.gen++;
|
|
608
|
+
this.pool.updated(entry, charge);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private notePendListRemove(blockId: BlockId, actionId: ActionId): void {
|
|
612
|
+
const entry = this.blocks.get(blockId)?.pendList;
|
|
613
|
+
if (entry === undefined) return;
|
|
614
|
+
const list = entry.value;
|
|
615
|
+
let charge = entry.charge;
|
|
616
|
+
if (list.ids.delete(actionId)) {
|
|
617
|
+
charge -= ID_SLOT + actionId.length * CHAR_BYTES;
|
|
618
|
+
}
|
|
619
|
+
list.gen++;
|
|
620
|
+
this.pool.updated(entry, charge);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Failed pending write: the affected value entry AND the id set drop to unknown. */
|
|
624
|
+
private dropPendingState(blockId: BlockId, actionId: ActionId): void {
|
|
625
|
+
const s = this.blocks.get(blockId);
|
|
626
|
+
if (s === undefined) return;
|
|
627
|
+
const p = s.pending.get(actionId);
|
|
628
|
+
if (p !== undefined) {
|
|
629
|
+
s.pending.delete(actionId);
|
|
630
|
+
this.pool.drop(p);
|
|
631
|
+
}
|
|
632
|
+
const list = s.pendList;
|
|
633
|
+
if (list !== undefined) {
|
|
634
|
+
s.pendList = undefined;
|
|
635
|
+
this.pool.drop(list);
|
|
636
|
+
}
|
|
637
|
+
this.dropIfEmpty(blockId, s);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async getPending(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
|
|
641
|
+
const s = this.state(blockId);
|
|
642
|
+
const cached = s.pending.get(actionId);
|
|
643
|
+
if (cached !== undefined) {
|
|
644
|
+
this.pool.touch(cached);
|
|
645
|
+
return cached.value ?? undefined;
|
|
646
|
+
}
|
|
647
|
+
let bytes: Uint8Array | undefined;
|
|
648
|
+
try {
|
|
649
|
+
bytes = await this.inner.getPending(blockId, actionId);
|
|
650
|
+
} catch (err) {
|
|
651
|
+
this.reapOnThrow(blockId, s);
|
|
652
|
+
throw err;
|
|
653
|
+
}
|
|
654
|
+
this.fillMiss(blockId, s, s.pending, 'pending', actionId, bytes);
|
|
655
|
+
return bytes;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async putPending(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
|
|
659
|
+
try {
|
|
660
|
+
await this.inner.putPending(blockId, actionId, value);
|
|
661
|
+
} catch (err) {
|
|
662
|
+
this.dropPendingState(blockId, actionId);
|
|
663
|
+
throw err;
|
|
664
|
+
}
|
|
665
|
+
const s = this.state(blockId);
|
|
666
|
+
this.cachePoint(blockId, s, 'pending', actionId, value, s.pending.get(actionId),
|
|
667
|
+
e => s.pending.set(actionId, e), () => s.pending.delete(actionId));
|
|
668
|
+
this.notePendListAdd(blockId, actionId);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async deletePending(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
672
|
+
try {
|
|
673
|
+
await this.inner.deletePending(blockId, actionId);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
this.dropPendingState(blockId, actionId);
|
|
676
|
+
throw err;
|
|
677
|
+
}
|
|
678
|
+
const s = this.state(blockId);
|
|
679
|
+
this.cachePoint(blockId, s, 'pending', actionId, null, s.pending.get(actionId),
|
|
680
|
+
e => s.pending.set(actionId, e), () => s.pending.delete(actionId));
|
|
681
|
+
this.notePendListRemove(blockId, actionId);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async *listPendingActionIds(blockId: BlockId): AsyncIterable<ActionId> {
|
|
685
|
+
const entry = this.ensurePendList(blockId);
|
|
686
|
+
if (entry === undefined) {
|
|
687
|
+
const drained: ActionId[] = [];
|
|
688
|
+
for await (const id of this.inner.listPendingActionIds(blockId)) {
|
|
689
|
+
drained.push(id);
|
|
690
|
+
}
|
|
691
|
+
yield* drained;
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const list = entry.value;
|
|
695
|
+
if (list.complete) {
|
|
696
|
+
this.pool.touch(entry);
|
|
697
|
+
yield* Array.from(list.ids);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
// One full enumeration seeds completeness; funnelled writes maintain it from then on.
|
|
701
|
+
// Completeness is NEVER inferred from metadata birth: at the raw layer a pending
|
|
702
|
+
// record CAN exist for a block with no metadata (the conformance suite writes exactly
|
|
703
|
+
// that), so only an actual drain of the inner driver proves the set.
|
|
704
|
+
const gen = list.gen;
|
|
705
|
+
const drained: ActionId[] = [];
|
|
706
|
+
for await (const id of this.inner.listPendingActionIds(blockId)) {
|
|
707
|
+
drained.push(id);
|
|
708
|
+
}
|
|
709
|
+
const current = this.blocks.get(blockId)?.pendList;
|
|
710
|
+
if (current === entry && list.gen === gen) {
|
|
711
|
+
let charge = entry.charge - idsContent(list.ids);
|
|
712
|
+
list.ids = new Set(drained);
|
|
713
|
+
list.complete = true;
|
|
714
|
+
charge += idsContent(list.ids);
|
|
715
|
+
this.pool.updated(entry, charge);
|
|
716
|
+
}
|
|
717
|
+
yield* drained;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// --- transactions ---
|
|
721
|
+
|
|
722
|
+
async getTransaction(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
|
|
723
|
+
const s = this.state(blockId);
|
|
724
|
+
const cached = s.transactions.get(actionId);
|
|
725
|
+
if (cached !== undefined) {
|
|
726
|
+
this.pool.touch(cached);
|
|
727
|
+
return cached.value ?? undefined;
|
|
728
|
+
}
|
|
729
|
+
let bytes: Uint8Array | undefined;
|
|
730
|
+
try {
|
|
731
|
+
bytes = await this.inner.getTransaction(blockId, actionId);
|
|
732
|
+
} catch (err) {
|
|
733
|
+
this.reapOnThrow(blockId, s);
|
|
734
|
+
throw err;
|
|
735
|
+
}
|
|
736
|
+
this.fillMiss(blockId, s, s.transactions, 'tx', actionId, bytes);
|
|
737
|
+
return bytes;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async putTransaction(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
|
|
741
|
+
try {
|
|
742
|
+
await this.inner.putTransaction(blockId, actionId, value);
|
|
743
|
+
} catch (err) {
|
|
744
|
+
this.dropMapEntry(blockId, 'transactions', actionId);
|
|
745
|
+
throw err;
|
|
746
|
+
}
|
|
747
|
+
const s = this.state(blockId);
|
|
748
|
+
this.cachePoint(blockId, s, 'tx', actionId, value, s.transactions.get(actionId),
|
|
749
|
+
e => s.transactions.set(actionId, e), () => s.transactions.delete(actionId));
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// --- proofs (PASSTHROUGH — deliberately not cached) ---
|
|
753
|
+
|
|
754
|
+
// Proof reads happen on repair and archive-serving paths, never on the hot read path,
|
|
755
|
+
// so proofs get no cache namespace of their own: two delegating methods instead of new
|
|
756
|
+
// cache state, eviction accounting and coherence rules. A namespace that is never
|
|
757
|
+
// populated also cannot go stale. This IS a small behaviour change from when proofs
|
|
758
|
+
// rode the transactions store and were cached incidentally.
|
|
759
|
+
// NOTE: if proof reads ever show up as hot, give proofs their own cache namespace
|
|
760
|
+
// alongside `transactions` (point entries keyed by rev, same shape as `tx`).
|
|
761
|
+
|
|
762
|
+
async getProof(blockId: BlockId, rev: number): Promise<Uint8Array | undefined> {
|
|
763
|
+
return this.inner.getProof(blockId, rev);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async putProof(blockId: BlockId, rev: number, value: Uint8Array): Promise<void> {
|
|
767
|
+
await this.inner.putProof(blockId, rev, value);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// --- materialized ---
|
|
771
|
+
|
|
772
|
+
async getMaterialized(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
|
|
773
|
+
const s = this.state(blockId);
|
|
774
|
+
const cached = s.materialized.get(actionId);
|
|
775
|
+
if (cached !== undefined) {
|
|
776
|
+
this.pool.touch(cached);
|
|
777
|
+
return cached.value ?? undefined;
|
|
778
|
+
}
|
|
779
|
+
let bytes: Uint8Array | undefined;
|
|
780
|
+
try {
|
|
781
|
+
bytes = await this.inner.getMaterialized(blockId, actionId);
|
|
782
|
+
} catch (err) {
|
|
783
|
+
this.reapOnThrow(blockId, s);
|
|
784
|
+
throw err;
|
|
785
|
+
}
|
|
786
|
+
this.fillMiss(blockId, s, s.materialized, 'mat', actionId, bytes);
|
|
787
|
+
return bytes;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
async putMaterialized(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
|
|
791
|
+
try {
|
|
792
|
+
await this.inner.putMaterialized(blockId, actionId, value);
|
|
793
|
+
} catch (err) {
|
|
794
|
+
this.dropMapEntry(blockId, 'materialized', actionId);
|
|
795
|
+
throw err;
|
|
796
|
+
}
|
|
797
|
+
const s = this.state(blockId);
|
|
798
|
+
this.cachePoint(blockId, s, 'mat', actionId, value, s.materialized.get(actionId),
|
|
799
|
+
e => s.materialized.set(actionId, e), () => s.materialized.delete(actionId));
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async deleteMaterialized(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
803
|
+
try {
|
|
804
|
+
await this.inner.deleteMaterialized(blockId, actionId);
|
|
805
|
+
} catch (err) {
|
|
806
|
+
this.dropMapEntry(blockId, 'materialized', actionId);
|
|
807
|
+
throw err;
|
|
808
|
+
}
|
|
809
|
+
// A delete through the funnel is a PROVEN absence, not an unknown.
|
|
810
|
+
const s = this.state(blockId);
|
|
811
|
+
this.cachePoint(blockId, s, 'mat', actionId, null, s.materialized.get(actionId),
|
|
812
|
+
e => s.materialized.set(actionId, e), () => s.materialized.delete(actionId));
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** Error-path invalidation of one map-backed entry (write failed → unknown). */
|
|
816
|
+
private dropMapEntry(blockId: BlockId, which: 'transactions' | 'materialized', actionId: ActionId): void {
|
|
817
|
+
const s = this.blocks.get(blockId);
|
|
818
|
+
if (s === undefined) return;
|
|
819
|
+
const map = s[which];
|
|
820
|
+
const e = map.get(actionId);
|
|
821
|
+
if (e !== undefined) {
|
|
822
|
+
map.delete(actionId);
|
|
823
|
+
this.pool.drop(e);
|
|
824
|
+
this.dropIfEmpty(blockId, s);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// --- promote (the single hardest coherence point — see "Invariant P" on
|
|
829
|
+
// `IBlockStorage.promotePendingTransaction`, src/storage/i-block-storage.ts) ---
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* The inner driver performs the atomic pending → committed move; this wrapper then mirrors
|
|
833
|
+
* it as one synchronous cache mutation sequence. When the pending transform was never
|
|
834
|
+
* cached — pended before this wrapper attached, or its entry EVICTED by the pool — the
|
|
835
|
+
* committed entry is INVALIDATED rather than synthesized: fabricating it would recreate
|
|
836
|
+
* exactly the phantom-record class Invariant P exists to prevent. Invalidation also
|
|
837
|
+
* removes any cached negative for the committed entry, which the promote has just made
|
|
838
|
+
* stale.
|
|
839
|
+
*
|
|
840
|
+
* Eviction landing between the three coupled mutations is safe by construction: the inner
|
|
841
|
+
* atomic move has already resolved before the first cache mutation, so each of the three
|
|
842
|
+
* entries (pending value, pending-id set, committed value) steps from one individually
|
|
843
|
+
* coherent state to another — an eviction merely turns one of those steps into "unknown,
|
|
844
|
+
* fall through", never into a fabricated value.
|
|
845
|
+
*/
|
|
846
|
+
async promote(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
847
|
+
try {
|
|
848
|
+
await this.inner.promote(blockId, actionId);
|
|
849
|
+
} catch (err) {
|
|
850
|
+
// On the contract's missing-pend throw the inner state is unchanged, but the throw
|
|
851
|
+
// may also be a mid-operation fault whose outcome is unknown ("exactly one of the
|
|
852
|
+
// two states") — drop all three affected entries to unknown rather than guess.
|
|
853
|
+
const s = this.blocks.get(blockId);
|
|
854
|
+
if (s !== undefined) {
|
|
855
|
+
const p = s.pending.get(actionId);
|
|
856
|
+
if (p !== undefined) {
|
|
857
|
+
s.pending.delete(actionId);
|
|
858
|
+
this.pool.drop(p);
|
|
859
|
+
}
|
|
860
|
+
const t = s.transactions.get(actionId);
|
|
861
|
+
if (t !== undefined) {
|
|
862
|
+
s.transactions.delete(actionId);
|
|
863
|
+
this.pool.drop(t);
|
|
864
|
+
}
|
|
865
|
+
const list = s.pendList;
|
|
866
|
+
if (list !== undefined) {
|
|
867
|
+
s.pendList = undefined;
|
|
868
|
+
this.pool.drop(list);
|
|
869
|
+
}
|
|
870
|
+
this.dropIfEmpty(blockId, s);
|
|
871
|
+
}
|
|
872
|
+
throw err;
|
|
873
|
+
}
|
|
874
|
+
const s = this.state(blockId);
|
|
875
|
+
const pendingBytes = s.pending.get(actionId)?.value;
|
|
876
|
+
this.cachePoint(blockId, s, 'pending', actionId, null, s.pending.get(actionId),
|
|
877
|
+
e => s.pending.set(actionId, e), () => s.pending.delete(actionId));
|
|
878
|
+
this.notePendListRemove(blockId, actionId);
|
|
879
|
+
if (pendingBytes instanceof Uint8Array) {
|
|
880
|
+
// The block state may have been reaped by an eviction the mutations above caused;
|
|
881
|
+
// re-fetch (or re-create) it before installing the committed value.
|
|
882
|
+
const s2 = this.blocks.get(blockId) ?? this.state(blockId);
|
|
883
|
+
this.cachePoint(blockId, s2, 'tx', actionId, pendingBytes, s2.transactions.get(actionId),
|
|
884
|
+
e => s2.transactions.set(actionId, e), () => s2.transactions.delete(actionId));
|
|
885
|
+
} else {
|
|
886
|
+
// Unknown (never observed, or evicted) — or a cached negative that promote just
|
|
887
|
+
// contradicted, which the funnel makes unreachable but is handled identically for
|
|
888
|
+
// safety. Invalidate; never synthesize.
|
|
889
|
+
this.dropMapEntry(blockId, 'transactions', actionId);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function evictFromMap(map: Map<ActionId, PoolEntry<CachedBytes>>, entry: PoolEntry): void {
|
|
895
|
+
if (entry.actionId !== undefined && map.get(entry.actionId) === entry) {
|
|
896
|
+
map.delete(entry.actionId);
|
|
897
|
+
}
|
|
898
|
+
}
|