@optimystic/db-p2p 0.14.1 → 0.16.3

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.
Files changed (275) hide show
  1. package/README.md +616 -609
  2. package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
  3. package/dist/src/cluster/block-transfer-service.js +4 -3
  4. package/dist/src/cluster/block-transfer-service.js.map +1 -1
  5. package/dist/src/cluster/block-transfer.d.ts +52 -3
  6. package/dist/src/cluster/block-transfer.d.ts.map +1 -1
  7. package/dist/src/cluster/block-transfer.js +129 -6
  8. package/dist/src/cluster/block-transfer.js.map +1 -1
  9. package/dist/src/cluster/client.d.ts.map +1 -1
  10. package/dist/src/cluster/client.js +3 -1
  11. package/dist/src/cluster/client.js.map +1 -1
  12. package/dist/src/cluster/cluster-repo.d.ts +182 -11
  13. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  14. package/dist/src/cluster/cluster-repo.js +429 -69
  15. package/dist/src/cluster/cluster-repo.js.map +1 -1
  16. package/dist/src/cluster/peer-key-binding.d.ts +25 -0
  17. package/dist/src/cluster/peer-key-binding.d.ts.map +1 -0
  18. package/dist/src/cluster/peer-key-binding.js +39 -0
  19. package/dist/src/cluster/peer-key-binding.js.map +1 -0
  20. package/dist/src/cluster/quorum-restore.d.ts +72 -0
  21. package/dist/src/cluster/quorum-restore.d.ts.map +1 -0
  22. package/dist/src/cluster/quorum-restore.js +0 -0
  23. package/dist/src/cluster/quorum-restore.js.map +1 -0
  24. package/dist/src/cluster/rebalance-monitor.d.ts +23 -1
  25. package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
  26. package/dist/src/cluster/rebalance-monitor.js +17 -2
  27. package/dist/src/cluster/rebalance-monitor.js.map +1 -1
  28. package/dist/src/cluster/service.d.ts.map +1 -1
  29. package/dist/src/cluster/service.js +7 -1
  30. package/dist/src/cluster/service.js.map +1 -1
  31. package/dist/src/cluster/spread-on-churn.d.ts +7 -0
  32. package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
  33. package/dist/src/cluster/spread-on-churn.js +4 -1
  34. package/dist/src/cluster/spread-on-churn.js.map +1 -1
  35. package/dist/src/cluster/supermajority-coupling.d.ts +22 -0
  36. package/dist/src/cluster/supermajority-coupling.d.ts.map +1 -0
  37. package/dist/src/cluster/supermajority-coupling.js +27 -0
  38. package/dist/src/cluster/supermajority-coupling.js.map +1 -0
  39. package/dist/src/cohort-topic/cohort-gossip-driver.d.ts +46 -4
  40. package/dist/src/cohort-topic/cohort-gossip-driver.d.ts.map +1 -1
  41. package/dist/src/cohort-topic/cohort-gossip-driver.js +62 -7
  42. package/dist/src/cohort-topic/cohort-gossip-driver.js.map +1 -1
  43. package/dist/src/cohort-topic/cohort-gossip-transport.d.ts +7 -0
  44. package/dist/src/cohort-topic/cohort-gossip-transport.d.ts.map +1 -1
  45. package/dist/src/cohort-topic/cohort-gossip-transport.js +9 -0
  46. package/dist/src/cohort-topic/cohort-gossip-transport.js.map +1 -1
  47. package/dist/src/cohort-topic/host.d.ts +197 -27
  48. package/dist/src/cohort-topic/host.d.ts.map +1 -1
  49. package/dist/src/cohort-topic/host.js +643 -87
  50. package/dist/src/cohort-topic/host.js.map +1 -1
  51. package/dist/src/dispute/arbitrator-selection.d.ts +68 -7
  52. package/dist/src/dispute/arbitrator-selection.d.ts.map +1 -1
  53. package/dist/src/dispute/arbitrator-selection.js +105 -16
  54. package/dist/src/dispute/arbitrator-selection.js.map +1 -1
  55. package/dist/src/dispute/cascade.d.ts +7 -0
  56. package/dist/src/dispute/cascade.d.ts.map +1 -1
  57. package/dist/src/dispute/cascade.js +1 -1
  58. package/dist/src/dispute/cascade.js.map +1 -1
  59. package/dist/src/dispute/client.d.ts.map +1 -1
  60. package/dist/src/dispute/client.js +3 -2
  61. package/dist/src/dispute/client.js.map +1 -1
  62. package/dist/src/dispute/dispute-service.d.ts +32 -3
  63. package/dist/src/dispute/dispute-service.d.ts.map +1 -1
  64. package/dist/src/dispute/dispute-service.js +82 -9
  65. package/dist/src/dispute/dispute-service.js.map +1 -1
  66. package/dist/src/dispute/index.d.ts +1 -1
  67. package/dist/src/dispute/index.d.ts.map +1 -1
  68. package/dist/src/dispute/index.js +1 -1
  69. package/dist/src/dispute/index.js.map +1 -1
  70. package/dist/src/dispute/invalidation.d.ts +25 -0
  71. package/dist/src/dispute/invalidation.d.ts.map +1 -1
  72. package/dist/src/dispute/invalidation.js +14 -2
  73. package/dist/src/dispute/invalidation.js.map +1 -1
  74. package/dist/src/dispute/service.d.ts.map +1 -1
  75. package/dist/src/dispute/service.js +7 -1
  76. package/dist/src/dispute/service.js.map +1 -1
  77. package/dist/src/index.d.ts +4 -1
  78. package/dist/src/index.d.ts.map +1 -1
  79. package/dist/src/index.js +4 -1
  80. package/dist/src/index.js.map +1 -1
  81. package/dist/src/it-utility.d.ts +1 -1
  82. package/dist/src/it-utility.d.ts.map +1 -1
  83. package/dist/src/it-utility.js +4 -13
  84. package/dist/src/it-utility.js.map +1 -1
  85. package/dist/src/libp2p-key-network.d.ts +98 -1
  86. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  87. package/dist/src/libp2p-key-network.js +272 -17
  88. package/dist/src/libp2p-key-network.js.map +1 -1
  89. package/dist/src/libp2p-node-base.d.ts +12 -0
  90. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  91. package/dist/src/libp2p-node-base.js +441 -89
  92. package/dist/src/libp2p-node-base.js.map +1 -1
  93. package/dist/src/matchmaking/query-transport.d.ts +2 -0
  94. package/dist/src/matchmaking/query-transport.d.ts.map +1 -1
  95. package/dist/src/matchmaking/query-transport.js +6 -0
  96. package/dist/src/matchmaking/query-transport.js.map +1 -1
  97. package/dist/src/matchmaking/traffic-validation.js +1 -0
  98. package/dist/src/matchmaking/traffic-validation.js.map +1 -1
  99. package/dist/src/network/network-manager-service.d.ts +2 -2
  100. package/dist/src/network/network-manager-service.d.ts.map +1 -1
  101. package/dist/src/network/network-manager-service.js +8 -4
  102. package/dist/src/network/network-manager-service.js.map +1 -1
  103. package/dist/src/owned-block-seed.d.ts +27 -0
  104. package/dist/src/owned-block-seed.d.ts.map +1 -0
  105. package/dist/src/owned-block-seed.js +37 -0
  106. package/dist/src/owned-block-seed.js.map +1 -0
  107. package/dist/src/protocol-client.d.ts +1 -0
  108. package/dist/src/protocol-client.d.ts.map +1 -1
  109. package/dist/src/protocol-client.js +9 -2
  110. package/dist/src/protocol-client.js.map +1 -1
  111. package/dist/src/protocol-limits.d.ts +32 -0
  112. package/dist/src/protocol-limits.d.ts.map +1 -0
  113. package/dist/src/protocol-limits.js +32 -0
  114. package/dist/src/protocol-limits.js.map +1 -0
  115. package/dist/src/repo/client.d.ts.map +1 -1
  116. package/dist/src/repo/client.js +3 -0
  117. package/dist/src/repo/client.js.map +1 -1
  118. package/dist/src/repo/cluster-coordinator.d.ts +41 -4
  119. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  120. package/dist/src/repo/cluster-coordinator.js +128 -80
  121. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  122. package/dist/src/repo/coordinator-repo.d.ts +38 -1
  123. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  124. package/dist/src/repo/coordinator-repo.js +92 -13
  125. package/dist/src/repo/coordinator-repo.js.map +1 -1
  126. package/dist/src/repo/service.d.ts.map +1 -1
  127. package/dist/src/repo/service.js +7 -1
  128. package/dist/src/repo/service.js.map +1 -1
  129. package/dist/src/reputation/peer-reputation.js +1 -1
  130. package/dist/src/reputation/peer-reputation.js.map +1 -1
  131. package/dist/src/reputation/types.d.ts +3 -1
  132. package/dist/src/reputation/types.d.ts.map +1 -1
  133. package/dist/src/reputation/types.js +3 -0
  134. package/dist/src/reputation/types.js.map +1 -1
  135. package/dist/src/rn.d.ts +5 -1
  136. package/dist/src/rn.d.ts.map +1 -1
  137. package/dist/src/rn.js +7 -1
  138. package/dist/src/rn.js.map +1 -1
  139. package/dist/src/storage/arachnode-fret-adapter.d.ts +30 -1
  140. package/dist/src/storage/arachnode-fret-adapter.d.ts.map +1 -1
  141. package/dist/src/storage/arachnode-fret-adapter.js +15 -2
  142. package/dist/src/storage/arachnode-fret-adapter.js.map +1 -1
  143. package/dist/src/storage/arachnode-partition.d.ts +49 -0
  144. package/dist/src/storage/arachnode-partition.d.ts.map +1 -0
  145. package/dist/src/storage/arachnode-partition.js +61 -0
  146. package/dist/src/storage/arachnode-partition.js.map +1 -0
  147. package/dist/src/storage/block-storage.d.ts +40 -1
  148. package/dist/src/storage/block-storage.d.ts.map +1 -1
  149. package/dist/src/storage/block-storage.js +150 -57
  150. package/dist/src/storage/block-storage.js.map +1 -1
  151. package/dist/src/storage/i-block-storage.d.ts +8 -0
  152. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  153. package/dist/src/storage/i-raw-storage.d.ts +13 -0
  154. package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
  155. package/dist/src/storage/kv-raw-storage.d.ts +45 -0
  156. package/dist/src/storage/kv-raw-storage.d.ts.map +1 -0
  157. package/dist/src/storage/kv-raw-storage.js +113 -0
  158. package/dist/src/storage/kv-raw-storage.js.map +1 -0
  159. package/dist/src/storage/memory-storage.d.ts +20 -58
  160. package/dist/src/storage/memory-storage.d.ts.map +1 -1
  161. package/dist/src/storage/memory-storage.js +22 -148
  162. package/dist/src/storage/memory-storage.js.map +1 -1
  163. package/dist/src/storage/memory-store-driver.d.ts +39 -0
  164. package/dist/src/storage/memory-store-driver.d.ts.map +1 -0
  165. package/dist/src/storage/memory-store-driver.js +129 -0
  166. package/dist/src/storage/memory-store-driver.js.map +1 -0
  167. package/dist/src/storage/raw-store-codec.d.ts +10 -0
  168. package/dist/src/storage/raw-store-codec.d.ts.map +1 -0
  169. package/dist/src/storage/raw-store-codec.js +36 -0
  170. package/dist/src/storage/raw-store-codec.js.map +1 -0
  171. package/dist/src/storage/raw-store-driver.d.ts +69 -0
  172. package/dist/src/storage/raw-store-driver.d.ts.map +1 -0
  173. package/dist/src/storage/raw-store-driver.js +2 -0
  174. package/dist/src/storage/raw-store-driver.js.map +1 -0
  175. package/dist/src/storage/restoration-coordinator-v2.d.ts.map +1 -1
  176. package/dist/src/storage/restoration-coordinator-v2.js +1 -1
  177. package/dist/src/storage/restoration-coordinator-v2.js.map +1 -1
  178. package/dist/src/storage/restoration-coordinator.d.ts +73 -0
  179. package/dist/src/storage/restoration-coordinator.d.ts.map +1 -0
  180. package/dist/src/storage/restoration-coordinator.js +181 -0
  181. package/dist/src/storage/restoration-coordinator.js.map +1 -0
  182. package/dist/src/storage/ring-selector.d.ts +92 -5
  183. package/dist/src/storage/ring-selector.d.ts.map +1 -1
  184. package/dist/src/storage/ring-selector.js +153 -51
  185. package/dist/src/storage/ring-selector.js.map +1 -1
  186. package/dist/src/storage/ring-shift-coordinator.d.ts +128 -0
  187. package/dist/src/storage/ring-shift-coordinator.d.ts.map +1 -0
  188. package/dist/src/storage/ring-shift-coordinator.js +196 -0
  189. package/dist/src/storage/ring-shift-coordinator.js.map +1 -0
  190. package/dist/src/storage/storage-monitor.d.ts +28 -0
  191. package/dist/src/storage/storage-monitor.d.ts.map +1 -1
  192. package/dist/src/storage/storage-monitor.js +42 -2
  193. package/dist/src/storage/storage-monitor.js.map +1 -1
  194. package/dist/src/storage/storage-repo.d.ts +19 -0
  195. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  196. package/dist/src/storage/storage-repo.js +172 -22
  197. package/dist/src/storage/storage-repo.js.map +1 -1
  198. package/dist/src/sync/client.d.ts.map +1 -1
  199. package/dist/src/sync/client.js +5 -1
  200. package/dist/src/sync/client.js.map +1 -1
  201. package/dist/src/sync/service.d.ts.map +1 -1
  202. package/dist/src/sync/service.js +2 -1
  203. package/dist/src/sync/service.js.map +1 -1
  204. package/dist/src/testing/cohort-topic-mesh-harness.d.ts +28 -4
  205. package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
  206. package/dist/src/testing/cohort-topic-mesh-harness.js +51 -10
  207. package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
  208. package/dist/src/testing/index.d.ts +1 -0
  209. package/dist/src/testing/index.d.ts.map +1 -1
  210. package/dist/src/testing/index.js +1 -0
  211. package/dist/src/testing/index.js.map +1 -1
  212. package/dist/src/testing/mesh-harness.d.ts.map +1 -1
  213. package/dist/src/testing/mesh-harness.js +15 -7
  214. package/dist/src/testing/mesh-harness.js.map +1 -1
  215. package/dist/src/testing/raw-storage-conformance.d.ts +24 -0
  216. package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -0
  217. package/dist/src/testing/raw-storage-conformance.js +313 -0
  218. package/dist/src/testing/raw-storage-conformance.js.map +1 -0
  219. package/package.json +4 -4
  220. package/src/cluster/block-transfer-service.ts +4 -3
  221. package/src/cluster/block-transfer.ts +424 -265
  222. package/src/cluster/client.ts +3 -1
  223. package/src/cluster/cluster-repo.ts +481 -71
  224. package/src/cluster/peer-key-binding.ts +37 -0
  225. package/src/cluster/quorum-restore.ts +0 -0
  226. package/src/cluster/rebalance-monitor.ts +34 -3
  227. package/src/cluster/service.ts +7 -1
  228. package/src/cluster/spread-on-churn.ts +11 -1
  229. package/src/cluster/supermajority-coupling.ts +37 -0
  230. package/src/cohort-topic/cohort-gossip-driver.ts +96 -9
  231. package/src/cohort-topic/cohort-gossip-transport.ts +10 -0
  232. package/src/cohort-topic/host.ts +831 -105
  233. package/src/dispute/arbitrator-selection.ts +138 -28
  234. package/src/dispute/cascade.ts +8 -1
  235. package/src/dispute/client.ts +3 -2
  236. package/src/dispute/dispute-service.ts +94 -9
  237. package/src/dispute/index.ts +7 -1
  238. package/src/dispute/invalidation.ts +40 -2
  239. package/src/dispute/service.ts +7 -1
  240. package/src/index.ts +46 -43
  241. package/src/it-utility.ts +5 -13
  242. package/src/libp2p-key-network.ts +281 -18
  243. package/src/libp2p-node-base.ts +511 -81
  244. package/src/matchmaking/query-transport.ts +9 -0
  245. package/src/network/network-manager-service.ts +8 -2
  246. package/src/owned-block-seed.ts +40 -0
  247. package/src/protocol-client.ts +10 -3
  248. package/src/protocol-limits.ts +33 -0
  249. package/src/repo/client.ts +3 -0
  250. package/src/repo/cluster-coordinator.ts +145 -82
  251. package/src/repo/coordinator-repo.ts +96 -16
  252. package/src/repo/service.ts +7 -1
  253. package/src/reputation/types.ts +3 -0
  254. package/src/rn.ts +7 -1
  255. package/src/storage/arachnode-fret-adapter.ts +33 -2
  256. package/src/storage/arachnode-partition.ts +74 -0
  257. package/src/storage/block-storage.ts +169 -61
  258. package/src/storage/i-block-storage.ts +9 -0
  259. package/src/storage/i-raw-storage.ts +14 -0
  260. package/src/storage/kv-raw-storage.ts +135 -0
  261. package/src/storage/memory-storage.ts +25 -173
  262. package/src/storage/memory-store-driver.ts +157 -0
  263. package/src/storage/raw-store-codec.ts +42 -0
  264. package/src/storage/raw-store-driver.ts +80 -0
  265. package/src/storage/{restoration-coordinator-v2.ts → restoration-coordinator.ts} +43 -37
  266. package/src/storage/ring-selector.ts +317 -176
  267. package/src/storage/ring-shift-coordinator.ts +271 -0
  268. package/src/storage/storage-monitor.ts +70 -3
  269. package/src/storage/storage-repo.ts +710 -558
  270. package/src/sync/client.ts +5 -1
  271. package/src/sync/service.ts +2 -1
  272. package/src/testing/cohort-topic-mesh-harness.ts +61 -11
  273. package/src/testing/index.ts +1 -0
  274. package/src/testing/mesh-harness.ts +14 -6
  275. package/src/testing/raw-storage-conformance.ts +397 -0
@@ -1,173 +1,25 @@
1
- import type { BlockId, IBlock, Transform, ActionId, ActionRev } from "@optimystic/db-core";
2
- import type { BlockMetadata } from "./struct.js";
3
- import type { IRawStorage } from "./i-raw-storage.js";
4
-
5
- export class MemoryRawStorage implements IRawStorage {
6
- private metadata = new Map<BlockId, BlockMetadata>();
7
- private revisions = new Map<string, ActionId>(); // blockId:rev -> actionId
8
- private pendingActions = new Map<string, Transform>(); // blockId:actionId -> transform
9
- private actions = new Map<string, Transform>(); // blockId:actionId -> transform
10
- private materializedBlocks = new Map<string, IBlock>(); // blockId:actionId -> block
11
-
12
- private getRevisionKey(blockId: BlockId, rev: number): string {
13
- return `${blockId}:${rev}`;
14
- }
15
-
16
- private getActionKey(blockId: BlockId, actionId: ActionId): string {
17
- return `${blockId}:${actionId}`;
18
- }
19
-
20
- /**
21
- * Retrieves metadata for a block.
22
- *
23
- * @pitfall **MUST return a clone** - `BlockStorage.setLatest` mutates the returned
24
- * metadata in place (`meta.latest = latest`) before calling `saveMetadata`. Returning
25
- * the stored reference leaks that mutation into RAM even when a subsequent
26
- * `saveMetadata` call fails, masking mid-commit crashes that a persistent store
27
- * (file/sqlite/leveldb) would surface correctly.
28
- * @see docs/internals.md "Storage Returns References" pitfall
29
- */
30
- async getMetadata(blockId: BlockId): Promise<BlockMetadata | undefined> {
31
- const meta = this.metadata.get(blockId);
32
- return meta ? structuredClone(meta) : undefined;
33
- }
34
-
35
- /**
36
- * Stores metadata for a block.
37
- *
38
- * @pitfall **MUST store a clone** - callers may continue mutating the metadata object
39
- * after saving; storing the reference lets those mutations corrupt persisted state.
40
- * @see docs/internals.md "Storage Returns References" pitfall
41
- */
42
- async saveMetadata(blockId: BlockId, metadata: BlockMetadata): Promise<void> {
43
- this.metadata.set(blockId, structuredClone(metadata));
44
- }
45
-
46
- async getRevision(blockId: BlockId, rev: number): Promise<ActionId | undefined> {
47
- return this.revisions.get(this.getRevisionKey(blockId, rev));
48
- }
49
-
50
- async saveRevision(blockId: BlockId, rev: number, actionId: ActionId): Promise<void> {
51
- this.revisions.set(this.getRevisionKey(blockId, rev), actionId);
52
- }
53
-
54
- async *listRevisions(blockId: BlockId, startRev: number, endRev: number): AsyncIterable<ActionRev> {
55
- const ascending = startRev <= endRev;
56
- const actualStart = ascending ? startRev : endRev;
57
- const actualEnd = ascending ? endRev : startRev;
58
-
59
- const results: ActionRev[] = [];
60
- for (let rev = actualStart; rev <= actualEnd; rev++) {
61
- const actionId = this.revisions.get(this.getRevisionKey(blockId, rev));
62
- if (actionId) {
63
- results.push({ rev, actionId });
64
- }
65
- }
66
-
67
- if (!ascending) {
68
- results.reverse();
69
- }
70
-
71
- for (const result of results) {
72
- yield result;
73
- }
74
- }
75
-
76
- async getPendingTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined> {
77
- return this.pendingActions.get(this.getActionKey(blockId, actionId));
78
- }
79
-
80
- async savePendingTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
81
- // Clone transform to prevent external modifications from affecting stored data
82
- this.pendingActions.set(this.getActionKey(blockId, actionId), structuredClone(transform));
83
- }
84
-
85
- async deletePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
86
- this.pendingActions.delete(this.getActionKey(blockId, actionId));
87
- }
88
-
89
- async *listPendingTransactions(blockId: BlockId): AsyncIterable<ActionId> {
90
- const prefix = `${blockId}:`;
91
- for (const [key] of Array.from(this.pendingActions.entries())) {
92
- if (key.startsWith(prefix)) {
93
- yield key.substring(prefix.length);
94
- }
95
- }
96
- }
97
-
98
- async getTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined> {
99
- return this.actions.get(this.getActionKey(blockId, actionId));
100
- }
101
-
102
- async saveTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
103
- this.actions.set(this.getActionKey(blockId, actionId), transform);
104
- }
105
-
106
- /**
107
- * Retrieves a materialized block at a specific revision.
108
- *
109
- * @pitfall **MUST return a clone** - `applyTransform()` mutates blocks in place.
110
- * If we return the stored reference, mutations corrupt ALL revisions that share
111
- * the same underlying object.
112
- * @see docs/internals.md "Storage Returns References" pitfall
113
- */
114
- async getMaterializedBlock(blockId: BlockId, actionId: ActionId): Promise<IBlock | undefined> {
115
- const block = this.materializedBlocks.get(this.getActionKey(blockId, actionId));
116
- // Clone to prevent external mutations from affecting stored data
117
- return block ? structuredClone(block) : undefined;
118
- }
119
-
120
- /**
121
- * Stores a materialized block at a specific revision.
122
- *
123
- * @pitfall **MUST store a clone** - callers may continue mutating the block after saving.
124
- * If we store the reference, those mutations corrupt the stored data.
125
- * @see docs/internals.md "Storage Returns References" pitfall
126
- */
127
- async saveMaterializedBlock(blockId: BlockId, actionId: ActionId, block?: IBlock): Promise<void> {
128
- const key = this.getActionKey(blockId, actionId);
129
- if (block) {
130
- // Clone to prevent external mutations from affecting stored data
131
- this.materializedBlocks.set(key, structuredClone(block));
132
- } else {
133
- this.materializedBlocks.delete(key);
134
- }
135
- }
136
-
137
- async promotePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
138
- const key = this.getActionKey(blockId, actionId);
139
- const transform = this.pendingActions.get(key);
140
- if (transform) {
141
- this.actions.set(key, transform);
142
- this.pendingActions.delete(key);
143
- }
144
- }
145
-
146
- async getApproximateBytesUsed(): Promise<number> {
147
- let total = 0;
148
- for (const [blockId, meta] of this.metadata) {
149
- total += blockId.length + jsonByteSize(meta);
150
- }
151
- for (const [key, actionId] of this.revisions) {
152
- total += key.length + actionId.length;
153
- }
154
- for (const [key, transform] of this.pendingActions) {
155
- total += key.length + jsonByteSize(transform);
156
- }
157
- for (const [key, transform] of this.actions) {
158
- total += key.length + jsonByteSize(transform);
159
- }
160
- for (const [key, block] of this.materializedBlocks) {
161
- total += key.length + jsonByteSize(block);
162
- }
163
- return total;
164
- }
165
- }
166
-
167
- function jsonByteSize(value: unknown): number {
168
- try {
169
- return JSON.stringify(value)?.length ?? 0;
170
- } catch {
171
- return 0;
172
- }
173
- }
1
+ import { KvRawStorage } from "./kv-raw-storage.js";
2
+ import { MemoryStoreDriver } from "./memory-store-driver.js";
3
+
4
+ /**
5
+ * In-memory {@link IRawStorage}, now a thin shell over the shared
6
+ * {@link KvRawStorage} kernel backed by a {@link MemoryStoreDriver}.
7
+ *
8
+ * @pitfall The old hand-rolled version had to `structuredClone` on every get and
9
+ * put otherwise callers mutating a returned/stored object corrupted the store
10
+ * (see docs/internals.md "Storage Returns References"). That discipline is GONE
11
+ * and must not be reintroduced: values cross the driver boundary as
12
+ * `Uint8Array` produced by `JSON`-encode and consumed by `JSON`-decode, so every
13
+ * read decodes a fresh object and every write stored an independent byte
14
+ * snapshot BY CONSTRUCTION. The clone-on-store / clone-on-read invariant is now
15
+ * structural, not a rule. The conformance suite asserts it so a future driver
16
+ * that shortcuts the byte boundary is caught.
17
+ *
18
+ * The name is kept stable so existing imports (`from '@optimystic/db-p2p'`)
19
+ * continue to resolve.
20
+ */
21
+ export class MemoryRawStorage extends KvRawStorage {
22
+ constructor() {
23
+ super(new MemoryStoreDriver());
24
+ }
25
+ }
@@ -0,0 +1,157 @@
1
+ import type { BlockId, ActionId } from "@optimystic/db-core";
2
+ import type { RawStoreDriver } from "./raw-store-driver.js";
3
+
4
+ /**
5
+ * In-memory {@link RawStoreDriver}: the five logical block-storage stores as
6
+ * `Map`s of `Uint8Array` values. `KvRawStorage` hands this driver bytes produced
7
+ * by `JSON`-encode and reads them back via `JSON`-decode, so every get yields a
8
+ * fresh object and every save stored an independent byte snapshot BY
9
+ * CONSTRUCTION — the old `structuredClone`-on-every-get/put discipline is now
10
+ * structural, not a rule a maintainer has to remember. The driver therefore
11
+ * stores the byte reference directly: those bytes are never mutated by the
12
+ * kernel or exposed to callers as a mutable handle.
13
+ */
14
+ export class MemoryStoreDriver implements RawStoreDriver {
15
+ private readonly metadata = new Map<BlockId, Uint8Array>();
16
+ private readonly revisions = new Map<string, Uint8Array>(); // `${blockId}:${rev}` -> actionId bytes
17
+ private readonly pending = new Map<string, Uint8Array>(); // `${blockId}:${actionId}` -> transform bytes
18
+ private readonly transactions = new Map<string, Uint8Array>();
19
+ private readonly materialized = new Map<string, Uint8Array>();
20
+
21
+ private revisionKey(blockId: BlockId, rev: number): string {
22
+ return `${blockId}:${rev}`;
23
+ }
24
+
25
+ private actionKey(blockId: BlockId, actionId: ActionId): string {
26
+ return `${blockId}:${actionId}`;
27
+ }
28
+
29
+ // --- metadata ---
30
+
31
+ async getMetadata(blockId: BlockId): Promise<Uint8Array | undefined> {
32
+ return this.metadata.get(blockId);
33
+ }
34
+
35
+ async putMetadata(blockId: BlockId, value: Uint8Array): Promise<void> {
36
+ this.metadata.set(blockId, value);
37
+ }
38
+
39
+ // --- revisions ---
40
+
41
+ async getRevision(blockId: BlockId, rev: number): Promise<Uint8Array | undefined> {
42
+ return this.revisions.get(this.revisionKey(blockId, rev));
43
+ }
44
+
45
+ async putRevision(blockId: BlockId, rev: number, value: Uint8Array): Promise<void> {
46
+ this.revisions.set(this.revisionKey(blockId, rev), value);
47
+ }
48
+
49
+ async *rangeRevisions(blockId: BlockId, lo: number, hi: number, reverse: boolean): AsyncIterable<[number, Uint8Array]> {
50
+ // Drain into an array before yielding (drain-before-yield contract): the
51
+ // consumer awaits between yields, and a Map has no live cursor to pin, but
52
+ // this keeps the memory driver's semantics identical to the native backends.
53
+ const results: [number, Uint8Array][] = [];
54
+ for (let rev = lo; rev <= hi; rev++) {
55
+ const value = this.revisions.get(this.revisionKey(blockId, rev));
56
+ if (value !== undefined) {
57
+ results.push([rev, value]);
58
+ }
59
+ }
60
+ if (reverse) {
61
+ results.reverse();
62
+ }
63
+ for (const result of results) {
64
+ yield result;
65
+ }
66
+ }
67
+
68
+ // --- pending ---
69
+
70
+ async getPending(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
71
+ return this.pending.get(this.actionKey(blockId, actionId));
72
+ }
73
+
74
+ async putPending(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
75
+ this.pending.set(this.actionKey(blockId, actionId), value);
76
+ }
77
+
78
+ async deletePending(blockId: BlockId, actionId: ActionId): Promise<void> {
79
+ this.pending.delete(this.actionKey(blockId, actionId));
80
+ }
81
+
82
+ async *listPendingActionIds(blockId: BlockId): AsyncIterable<ActionId> {
83
+ const prefix = `${blockId}:`;
84
+ // Snapshot before yielding (drain-before-yield): a concurrent putPending during
85
+ // the scan must not invalidate a live map iterator.
86
+ const ids: ActionId[] = [];
87
+ for (const key of Array.from(this.pending.keys())) {
88
+ if (key.startsWith(prefix)) {
89
+ ids.push(key.substring(prefix.length) as ActionId);
90
+ }
91
+ }
92
+ for (const id of ids) {
93
+ yield id;
94
+ }
95
+ }
96
+
97
+ // --- transactions ---
98
+
99
+ async getTransaction(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
100
+ return this.transactions.get(this.actionKey(blockId, actionId));
101
+ }
102
+
103
+ async putTransaction(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
104
+ this.transactions.set(this.actionKey(blockId, actionId), value);
105
+ }
106
+
107
+ // --- materialized ---
108
+
109
+ async getMaterialized(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
110
+ return this.materialized.get(this.actionKey(blockId, actionId));
111
+ }
112
+
113
+ async putMaterialized(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void> {
114
+ this.materialized.set(this.actionKey(blockId, actionId), value);
115
+ }
116
+
117
+ async deleteMaterialized(blockId: BlockId, actionId: ActionId): Promise<void> {
118
+ this.materialized.delete(this.actionKey(blockId, actionId));
119
+ }
120
+
121
+ // --- promote (atomic move; synchronous Map ops make it indivisible here) ---
122
+
123
+ async promote(blockId: BlockId, actionId: ActionId): Promise<void> {
124
+ const key = this.actionKey(blockId, actionId);
125
+ const value = this.pending.get(key);
126
+ if (value === undefined) {
127
+ throw new Error(`Pending action ${actionId} not found for block ${blockId}`);
128
+ }
129
+ this.transactions.set(key, value);
130
+ this.pending.delete(key);
131
+ }
132
+
133
+ // --- optional passthroughs ---
134
+
135
+ async *listBlockIds(): AsyncIterable<BlockId> {
136
+ // Snapshot the keys before yielding so a concurrent putMetadata during the scan
137
+ // doesn't invalidate a live map iterator. Fresh in-memory storage is empty, so at
138
+ // real process startup this yields nothing — it exists so the seed path can be
139
+ // unit-tested against a pre-populated store.
140
+ for (const blockId of Array.from(this.metadata.keys())) {
141
+ yield blockId;
142
+ }
143
+ }
144
+
145
+ async approximateBytesUsed(): Promise<number> {
146
+ let total = 0;
147
+ for (const [blockId, value] of this.metadata) {
148
+ total += blockId.length + value.byteLength;
149
+ }
150
+ for (const store of [this.revisions, this.pending, this.transactions, this.materialized]) {
151
+ for (const [key, value] of store) {
152
+ total += key.length + value.byteLength;
153
+ }
154
+ }
155
+ return total;
156
+ }
157
+ }
@@ -0,0 +1,42 @@
1
+ import type { ActionId } from "@optimystic/db-core";
2
+
3
+ /**
4
+ * Value codec shared by every `KvRawStorage`-backed store. Values cross the
5
+ * `RawStoreDriver` boundary as `Uint8Array`, so this module is the single place
6
+ * that turns `BlockMetadata`/`Transform`/`IBlock` (via JSON) and the `ActionId`
7
+ * string (via UTF-8) into bytes and back.
8
+ *
9
+ * Because a get always decodes fresh bytes into a NEW object and a save always
10
+ * encodes an independent byte copy, the clone-on-store / clone-on-read invariant
11
+ * that in-memory storage used to enforce by hand (`structuredClone`) is now
12
+ * structural for kernel-backed stores — see docs/internals.md "Storage Returns
13
+ * References".
14
+ *
15
+ * Round-trip fidelity matters for `BlockMetadata`: an open-ended `RevisionRange`
16
+ * is encoded `[E]` (one element, upper bound `undefined`). `JSON.stringify([5])`
17
+ * → `"[5]"` → `JSON.parse` → `[5]`, so the open-ended encoding survives byte-exact.
18
+ * Do NOT normalize ranges here.
19
+ */
20
+
21
+ const encoder = new TextEncoder();
22
+ const decoder = new TextDecoder();
23
+
24
+ /** JSON-encode a value (`BlockMetadata` / `Transform` / `IBlock`) to UTF-8 bytes. */
25
+ export function encodeJson<T>(value: T): Uint8Array {
26
+ return encoder.encode(JSON.stringify(value));
27
+ }
28
+
29
+ /** Decode UTF-8 JSON bytes back into a value. Callers guard the miss (`undefined`) first. */
30
+ export function decodeJson<T>(bytes: Uint8Array): T {
31
+ return JSON.parse(decoder.decode(bytes)) as T;
32
+ }
33
+
34
+ /** Encode an `ActionId` string to UTF-8 bytes (stored as the revisions-store value). */
35
+ export function encodeActionId(actionId: ActionId): Uint8Array {
36
+ return encoder.encode(actionId);
37
+ }
38
+
39
+ /** Decode UTF-8 bytes back into an `ActionId` string. */
40
+ export function decodeActionId(bytes: Uint8Array): ActionId {
41
+ return decoder.decode(bytes) as ActionId;
42
+ }
@@ -0,0 +1,80 @@
1
+ import type { BlockId, ActionId } from "@optimystic/db-core";
2
+
3
+ /**
4
+ * Bytes-valued, per-logical-store driver surface. Each backend implements the
5
+ * five block-storage stores (metadata, revisions, pending, transactions,
6
+ * materialized) over its native mechanism (LevelDB tag-ranges, five SQLite
7
+ * tables, five IndexedDB object stores, five filesystem subdirectories, five
8
+ * in-memory maps). `KvRawStorage` layers all JSON serialization and call
9
+ * orchestration on top — drivers never (de)serialize values and never see the
10
+ * `BlockMetadata`/`Transform`/`IBlock` types. Drivers speak only
11
+ * `Uint8Array`/`BlockId`/`ActionId`/`number`.
12
+ *
13
+ * ### Iteration semantics (drain-before-yield)
14
+ *
15
+ * `rangeRevisions` and `listPendingActionIds` return an `AsyncIterable`, but a
16
+ * driver MUST drain its native cursor/iterator into memory BEFORE yielding to
17
+ * the consumer. A live LevelDB iterator, IndexedDB transaction, or SQLite
18
+ * cursor must not straddle the consumer's `await`s: IndexedDB auto-commits an
19
+ * idle transaction, SQLite would hold its mutex slot, and LevelDB pins native
20
+ * resources. The kernel encodes this as a contract (not a shared implementation)
21
+ * because the drain is backend-specific. The conformance suite exercises it by
22
+ * interleaving other awaits between yielded items.
23
+ *
24
+ * ### Promote atomicity
25
+ *
26
+ * `promote` is the ONLY cross-key atomic operation the kernel requires. Every
27
+ * other write is a single put/delete. Each backend satisfies `promote` with its
28
+ * native atomic mechanism (LevelDB batch, SQLite transaction, IndexedDB
29
+ * readwrite transaction, filesystem rename); the kernel never assumes an
30
+ * atomicity a backend cannot deliver.
31
+ */
32
+ export interface RawStoreDriver {
33
+ // metadata store — keyed by blockId
34
+ getMetadata(blockId: BlockId): Promise<Uint8Array | undefined>;
35
+ putMetadata(blockId: BlockId, value: Uint8Array): Promise<void>;
36
+
37
+ // revisions store — keyed by (blockId, rev), ORDERED BY rev
38
+ getRevision(blockId: BlockId, rev: number): Promise<Uint8Array | undefined>;
39
+ putRevision(blockId: BlockId, rev: number, value: Uint8Array): Promise<void>;
40
+ /**
41
+ * Yield `[rev, value]` for every present rev in `[lo, hi]` (both inclusive),
42
+ * ascending when `reverse` is false, descending when `reverse` is true. The
43
+ * driver MUST drain any native cursor into memory before yielding — see the
44
+ * "drain-before-yield" contract above.
45
+ */
46
+ rangeRevisions(blockId: BlockId, lo: number, hi: number, reverse: boolean): AsyncIterable<[number, Uint8Array]>;
47
+
48
+ // pending store — keyed by (blockId, actionId)
49
+ getPending(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined>;
50
+ putPending(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void>;
51
+ deletePending(blockId: BlockId, actionId: ActionId): Promise<void>;
52
+ /** Yield each present pending actionId for the block. MUST drain before yielding (see above). */
53
+ listPendingActionIds(blockId: BlockId): AsyncIterable<ActionId>;
54
+
55
+ // transactions store — keyed by (blockId, actionId)
56
+ getTransaction(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined>;
57
+ putTransaction(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void>;
58
+
59
+ // materialized store — keyed by (blockId, actionId)
60
+ getMaterialized(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined>;
61
+ putMaterialized(blockId: BlockId, actionId: ActionId, value: Uint8Array): Promise<void>;
62
+ deleteMaterialized(blockId: BlockId, actionId: ActionId): Promise<void>;
63
+
64
+ /**
65
+ * Atomically move `pending(blockId, actionId)` → `transactions(blockId, actionId)`:
66
+ * write the transactions entry and remove the pending entry as one indivisible
67
+ * step (batch / DB transaction / rename). A crash must leave exactly one of the
68
+ * two states, never both/neither. Throw
69
+ * `Pending action <actionId> not found for block <blockId>` when no pending
70
+ * entry exists. This is the ONLY cross-key atomic operation the kernel requires.
71
+ */
72
+ promote(blockId: BlockId, actionId: ActionId): Promise<void>;
73
+
74
+ /** Optional — enumerate block ids with durable metadata (startup seed). Passed through by the kernel. */
75
+ listBlockIds?(): AsyncIterable<BlockId>;
76
+ /** Optional — best cheap byte estimate. Passed through by the kernel. */
77
+ approximateBytesUsed?(): Promise<number>;
78
+ /** Optional — release the underlying handle. */
79
+ close?(): Promise<void>;
80
+ }
@@ -1,10 +1,11 @@
1
1
  import type { BlockId } from '@optimystic/db-core';
2
- import { hashKey } from 'p2p-fret';
2
+ import { hashKey, type RingCoord } from 'p2p-fret';
3
3
  import { peerIdFromString } from '@libp2p/peer-id';
4
4
  import type { BlockArchive, RestoreCallback } from './struct.js';
5
5
  import { SyncClient } from '../sync/client.js';
6
6
  import type { IPeerNetwork } from '@optimystic/db-core';
7
7
  import type { ArachnodeFretAdapter } from './arachnode-fret-adapter.js';
8
+ import { partitionCovers } from './arachnode-partition.js';
8
9
  import { createLogger } from '../logger.js';
9
10
 
10
11
  /**
@@ -41,8 +42,14 @@ export class RestorationCoordinator {
41
42
  const startTime = Date.now();
42
43
  this.metrics.totalRequests++;
43
44
 
45
+ // Hash the block id once into the shared coordinate space. Peer responsibility
46
+ // (RingSelector.calculatePartition) and cohort assembly both operate on hashed
47
+ // coords, so restoration must filter in the same space — see getMyRingPeers and
48
+ // filterByPartition below, which reuse this value.
49
+ const blockCoord = await hashKey(new TextEncoder().encode(blockId));
50
+
44
51
  // 1. Try my transaction ring peers first
45
- const myPeers = await this.getMyRingPeers(blockId);
52
+ const myPeers = this.getMyRingPeers(blockCoord);
46
53
  const myRingDepth = this.getMyRingDepth();
47
54
 
48
55
  // Avoid dialing self: on a solo/bootstrap node with no listenAddrs, dialing self
@@ -63,13 +70,15 @@ export class RestorationCoordinator {
63
70
  return archive;
64
71
  }
65
72
  }
73
+ // My transaction ring was queried and yielded nothing — count it as one ring failure.
74
+ this.recordFailure(myRingDepth);
66
75
 
67
76
  // 2. Try inner storage rings (broader coverage)
68
77
  for (let ringDepth = myRingDepth - 1; ringDepth >= 0; ringDepth--) {
69
78
  const storagePeers = this.fretAdapter.findPeersAtRing(ringDepth);
70
79
 
71
80
  // Filter to peers responsible for this block's partition (skip self — cannot dial self).
72
- const responsiblePeers = this.filterByPartition(storagePeers, blockId, ringDepth)
81
+ const responsiblePeers = this.filterByPartition(storagePeers, blockCoord, ringDepth)
73
82
  .filter(p => !this.selfPeerId || p !== this.selfPeerId);
74
83
 
75
84
  for (const peerIdStr of responsiblePeers) {
@@ -79,6 +88,8 @@ export class RestorationCoordinator {
79
88
  return archive;
80
89
  }
81
90
  }
91
+ // This inner ring was queried and yielded nothing — count it as one ring failure.
92
+ this.recordFailure(ringDepth);
82
93
  }
83
94
 
84
95
  // No ring had the data
@@ -98,11 +109,10 @@ export class RestorationCoordinator {
98
109
 
99
110
  /**
100
111
  * Get peers in my transaction ring for a given block.
112
+ * @param blockCoord the block id already hashed into the shared coordinate space.
101
113
  */
102
- private async getMyRingPeers(blockId: BlockId): Promise<string[]> {
103
- const blockIdBytes = new TextEncoder().encode(blockId);
104
- const coord = await hashKey(blockIdBytes);
105
- return this.fretAdapter.getFret().assembleCohort(coord, 10);
114
+ private getMyRingPeers(blockCoord: RingCoord): string[] {
115
+ return this.fretAdapter.getFret().assembleCohort(blockCoord, 10);
106
116
  }
107
117
 
108
118
  /**
@@ -115,43 +125,22 @@ export class RestorationCoordinator {
115
125
 
116
126
  /**
117
127
  * Filter peers by partition responsibility.
128
+ * @param blockCoord the block id hashed into the shared coordinate space (same space
129
+ * RingSelector.calculatePartition uses for peer partitions).
118
130
  */
119
- private filterByPartition(peers: string[], blockId: BlockId, ringDepth: number): string[] {
131
+ private filterByPartition(peers: string[], blockCoord: RingCoord, ringDepth: number): string[] {
120
132
  if (ringDepth === 0) {
121
133
  return peers; // Ring 0 covers all blocks
122
134
  }
123
135
 
124
- const blockPrefix = this.extractBlockPrefix(blockId, ringDepth);
125
-
126
136
  return peers.filter(peerId => {
127
137
  const info = this.fretAdapter.getArachnodeInfo(peerId);
128
138
  if (!info || !info.partition) return false;
129
- return info.partition.prefixValue === blockPrefix;
139
+ // Compare block prefix vs peer partition via the shared responsibility derivation.
140
+ return partitionCovers(info.partition, blockCoord);
130
141
  });
131
142
  }
132
143
 
133
- /**
134
- * Extract prefix bits from block ID for partition matching.
135
- */
136
- private extractBlockPrefix(blockId: BlockId, bits: number): number {
137
- const bytes = new TextEncoder().encode(blockId);
138
- // Hash the block ID to get uniform distribution
139
- const hash = new Uint8Array(32);
140
- for (let i = 0; i < Math.min(bytes.length, hash.length); i++) {
141
- hash[i] = bytes[i]!;
142
- }
143
-
144
- // Extract first N bits
145
- let value = 0;
146
- for (let i = 0; i < bits; i++) {
147
- const byteIndex = Math.floor(i / 8);
148
- const bitIndex = 7 - (i % 8);
149
- const bit = (hash[byteIndex]! >> bitIndex) & 1;
150
- value = (value << 1) | bit;
151
- }
152
- return value;
153
- }
154
-
155
144
  /**
156
145
  * Query a specific peer for a block.
157
146
  */
@@ -185,9 +174,18 @@ export class RestorationCoordinator {
185
174
  const prevTotal = this.metrics.averageDurationMs * (totalSuccesses - 1);
186
175
  this.metrics.averageDurationMs = (prevTotal + durationMs) / totalSuccesses;
187
176
 
188
- console.log(
189
- `[Ring ${ringDepth}] Successfully restored block ${blockId} in ${durationMs}ms`
190
- );
177
+ this.log('[Ring %d] Successfully restored block %s in %dms', ringDepth, blockId, durationMs);
178
+ }
179
+
180
+ /**
181
+ * Record that a ring was queried during a restore but did not yield the block.
182
+ * Counterpart to {@link recordSuccess}: the ring — not the individual peer query — is the
183
+ * failure unit, so this increments once per exhausted ring at the points where the peer
184
+ * loops in {@link restore} complete without returning an archive.
185
+ */
186
+ private recordFailure(ringDepth: number): void {
187
+ const count = this.metrics.failureByRing.get(ringDepth) ?? 0;
188
+ this.metrics.failureByRing.set(ringDepth, count + 1);
191
189
  }
192
190
 
193
191
  /**
@@ -199,7 +197,15 @@ export class RestorationCoordinator {
199
197
  failureByRing: Map<number, number>;
200
198
  averageDurationMs: number;
201
199
  } {
202
- return { ...this.metrics };
200
+ // Copy the Maps: a bare `{ ...this.metrics }` spreads only the top level, handing the
201
+ // caller the SAME Map instances this coordinator keeps mutating — the snapshot would
202
+ // then change under the caller's feet, and a caller mutation would corrupt internal state.
203
+ return {
204
+ totalRequests: this.metrics.totalRequests,
205
+ successByRing: new Map(this.metrics.successByRing),
206
+ failureByRing: new Map(this.metrics.failureByRing),
207
+ averageDurationMs: this.metrics.averageDurationMs
208
+ };
203
209
  }
204
210
  }
205
211