@optimystic/db-core 0.14.0 → 0.16.2

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 (301) hide show
  1. package/dist/src/blocks/block-store.d.ts +18 -1
  2. package/dist/src/blocks/block-store.d.ts.map +1 -1
  3. package/dist/src/blocks/helpers.d.ts +2 -2
  4. package/dist/src/blocks/helpers.d.ts.map +1 -1
  5. package/dist/src/blocks/helpers.js +2 -2
  6. package/dist/src/blocks/helpers.js.map +1 -1
  7. package/dist/src/btree/btree.d.ts +27 -6
  8. package/dist/src/btree/btree.d.ts.map +1 -1
  9. package/dist/src/btree/btree.js +104 -61
  10. package/dist/src/btree/btree.js.map +1 -1
  11. package/dist/src/chain/chain.d.ts.map +1 -1
  12. package/dist/src/chain/chain.js +11 -1
  13. package/dist/src/chain/chain.js.map +1 -1
  14. package/dist/src/cluster/index.d.ts +1 -0
  15. package/dist/src/cluster/index.d.ts.map +1 -1
  16. package/dist/src/cluster/index.js +1 -0
  17. package/dist/src/cluster/index.js.map +1 -1
  18. package/dist/src/cluster/membership.d.ts +49 -0
  19. package/dist/src/cluster/membership.d.ts.map +1 -0
  20. package/dist/src/cluster/membership.js +75 -0
  21. package/dist/src/cluster/membership.js.map +1 -0
  22. package/dist/src/cluster/structs.d.ts +50 -1
  23. package/dist/src/cluster/structs.d.ts.map +1 -1
  24. package/dist/src/cluster/structs.js +9 -1
  25. package/dist/src/cluster/structs.js.map +1 -1
  26. package/dist/src/cohort-topic/addressing.d.ts +8 -2
  27. package/dist/src/cohort-topic/addressing.d.ts.map +1 -1
  28. package/dist/src/cohort-topic/addressing.js +11 -3
  29. package/dist/src/cohort-topic/addressing.js.map +1 -1
  30. package/dist/src/cohort-topic/antidos/bootstrap-evidence.d.ts +13 -7
  31. package/dist/src/cohort-topic/antidos/bootstrap-evidence.d.ts.map +1 -1
  32. package/dist/src/cohort-topic/antidos/bootstrap-evidence.js +17 -6
  33. package/dist/src/cohort-topic/antidos/bootstrap-evidence.js.map +1 -1
  34. package/dist/src/cohort-topic/antidos/replay-guard.d.ts +17 -0
  35. package/dist/src/cohort-topic/antidos/replay-guard.d.ts.map +1 -1
  36. package/dist/src/cohort-topic/antidos/replay-guard.js +33 -0
  37. package/dist/src/cohort-topic/antidos/replay-guard.js.map +1 -1
  38. package/dist/src/cohort-topic/antidos/topic-budget.d.ts +7 -0
  39. package/dist/src/cohort-topic/antidos/topic-budget.d.ts.map +1 -1
  40. package/dist/src/cohort-topic/antidos/topic-budget.js +8 -2
  41. package/dist/src/cohort-topic/antidos/topic-budget.js.map +1 -1
  42. package/dist/src/cohort-topic/coldstart.d.ts +34 -14
  43. package/dist/src/cohort-topic/coldstart.d.ts.map +1 -1
  44. package/dist/src/cohort-topic/coldstart.js +25 -11
  45. package/dist/src/cohort-topic/coldstart.js.map +1 -1
  46. package/dist/src/cohort-topic/gossip/bus.d.ts +12 -1
  47. package/dist/src/cohort-topic/gossip/bus.d.ts.map +1 -1
  48. package/dist/src/cohort-topic/gossip/bus.js +27 -1
  49. package/dist/src/cohort-topic/gossip/bus.js.map +1 -1
  50. package/dist/src/cohort-topic/gossip/view.d.ts +8 -0
  51. package/dist/src/cohort-topic/gossip/view.d.ts.map +1 -1
  52. package/dist/src/cohort-topic/gossip/view.js +17 -1
  53. package/dist/src/cohort-topic/gossip/view.js.map +1 -1
  54. package/dist/src/cohort-topic/member-engine.d.ts +10 -3
  55. package/dist/src/cohort-topic/member-engine.d.ts.map +1 -1
  56. package/dist/src/cohort-topic/member-engine.js +29 -14
  57. package/dist/src/cohort-topic/member-engine.js.map +1 -1
  58. package/dist/src/cohort-topic/membership/publisher.d.ts +14 -7
  59. package/dist/src/cohort-topic/membership/publisher.d.ts.map +1 -1
  60. package/dist/src/cohort-topic/membership/publisher.js +15 -27
  61. package/dist/src/cohort-topic/membership/publisher.js.map +1 -1
  62. package/dist/src/cohort-topic/membership/verifier.d.ts +35 -0
  63. package/dist/src/cohort-topic/membership/verifier.d.ts.map +1 -1
  64. package/dist/src/cohort-topic/membership/verifier.js +88 -3
  65. package/dist/src/cohort-topic/membership/verifier.js.map +1 -1
  66. package/dist/src/cohort-topic/promotion.d.ts +7 -0
  67. package/dist/src/cohort-topic/promotion.d.ts.map +1 -1
  68. package/dist/src/cohort-topic/promotion.js +16 -2
  69. package/dist/src/cohort-topic/promotion.js.map +1 -1
  70. package/dist/src/cohort-topic/registration/renewal.d.ts +18 -0
  71. package/dist/src/cohort-topic/registration/renewal.d.ts.map +1 -1
  72. package/dist/src/cohort-topic/registration/renewal.js +54 -0
  73. package/dist/src/cohort-topic/registration/renewal.js.map +1 -1
  74. package/dist/src/cohort-topic/registration/types.d.ts +12 -0
  75. package/dist/src/cohort-topic/registration/types.d.ts.map +1 -1
  76. package/dist/src/cohort-topic/registration/types.js +14 -0
  77. package/dist/src/cohort-topic/registration/types.js.map +1 -1
  78. package/dist/src/cohort-topic/service.d.ts +4 -3
  79. package/dist/src/cohort-topic/service.d.ts.map +1 -1
  80. package/dist/src/cohort-topic/service.js +38 -12
  81. package/dist/src/cohort-topic/service.js.map +1 -1
  82. package/dist/src/cohort-topic/sig/payloads.d.ts +23 -7
  83. package/dist/src/cohort-topic/sig/payloads.d.ts.map +1 -1
  84. package/dist/src/cohort-topic/sig/payloads.js +22 -6
  85. package/dist/src/cohort-topic/sig/payloads.js.map +1 -1
  86. package/dist/src/cohort-topic/traffic.d.ts +6 -0
  87. package/dist/src/cohort-topic/traffic.d.ts.map +1 -1
  88. package/dist/src/cohort-topic/traffic.js +16 -1
  89. package/dist/src/cohort-topic/traffic.js.map +1 -1
  90. package/dist/src/cohort-topic/walk.d.ts +22 -7
  91. package/dist/src/cohort-topic/walk.d.ts.map +1 -1
  92. package/dist/src/cohort-topic/walk.js +78 -18
  93. package/dist/src/cohort-topic/walk.js.map +1 -1
  94. package/dist/src/cohort-topic/wire/codec.d.ts +3 -1
  95. package/dist/src/cohort-topic/wire/codec.d.ts.map +1 -1
  96. package/dist/src/cohort-topic/wire/codec.js +7 -1
  97. package/dist/src/cohort-topic/wire/codec.js.map +1 -1
  98. package/dist/src/cohort-topic/wire/index.d.ts +1 -1
  99. package/dist/src/cohort-topic/wire/index.d.ts.map +1 -1
  100. package/dist/src/cohort-topic/wire/index.js +1 -1
  101. package/dist/src/cohort-topic/wire/index.js.map +1 -1
  102. package/dist/src/cohort-topic/wire/payloads.d.ts +5 -4
  103. package/dist/src/cohort-topic/wire/payloads.d.ts.map +1 -1
  104. package/dist/src/cohort-topic/wire/payloads.js +14 -5
  105. package/dist/src/cohort-topic/wire/payloads.js.map +1 -1
  106. package/dist/src/cohort-topic/wire/primitives.d.ts +65 -0
  107. package/dist/src/cohort-topic/wire/primitives.d.ts.map +1 -0
  108. package/dist/src/cohort-topic/wire/primitives.js +173 -0
  109. package/dist/src/cohort-topic/wire/primitives.js.map +1 -0
  110. package/dist/src/cohort-topic/wire/types.d.ts +132 -5
  111. package/dist/src/cohort-topic/wire/types.d.ts.map +1 -1
  112. package/dist/src/cohort-topic/wire/validate.d.ts +15 -5
  113. package/dist/src/cohort-topic/wire/validate.d.ts.map +1 -1
  114. package/dist/src/cohort-topic/wire/validate.js +146 -123
  115. package/dist/src/cohort-topic/wire/validate.js.map +1 -1
  116. package/dist/src/collection/collection.d.ts +23 -12
  117. package/dist/src/collection/collection.d.ts.map +1 -1
  118. package/dist/src/collection/collection.js +111 -35
  119. package/dist/src/collection/collection.js.map +1 -1
  120. package/dist/src/collection/struct.d.ts +37 -2
  121. package/dist/src/collection/struct.d.ts.map +1 -1
  122. package/dist/src/collection/struct.js +16 -1
  123. package/dist/src/collection/struct.js.map +1 -1
  124. package/dist/src/collections/tree/tree.d.ts +18 -1
  125. package/dist/src/collections/tree/tree.d.ts.map +1 -1
  126. package/dist/src/collections/tree/tree.js +36 -6
  127. package/dist/src/collections/tree/tree.js.map +1 -1
  128. package/dist/src/index.d.ts +1 -0
  129. package/dist/src/index.d.ts.map +1 -1
  130. package/dist/src/index.js +1 -0
  131. package/dist/src/index.js.map +1 -1
  132. package/dist/src/log/log.d.ts +26 -3
  133. package/dist/src/log/log.d.ts.map +1 -1
  134. package/dist/src/log/log.js +34 -7
  135. package/dist/src/log/log.js.map +1 -1
  136. package/dist/src/log/struct.d.ts +5 -4
  137. package/dist/src/log/struct.d.ts.map +1 -1
  138. package/dist/src/log/struct.js.map +1 -1
  139. package/dist/src/matchmaking/wire.d.ts +2 -1
  140. package/dist/src/matchmaking/wire.d.ts.map +1 -1
  141. package/dist/src/matchmaking/wire.js +13 -92
  142. package/dist/src/matchmaking/wire.js.map +1 -1
  143. package/dist/src/network/i-key-network.d.ts +9 -0
  144. package/dist/src/network/i-key-network.d.ts.map +1 -1
  145. package/dist/src/network/i-repo.d.ts +8 -0
  146. package/dist/src/network/i-repo.d.ts.map +1 -1
  147. package/dist/src/network/struct.d.ts +10 -0
  148. package/dist/src/network/struct.d.ts.map +1 -1
  149. package/dist/src/reactivity/push-state.d.ts.map +1 -1
  150. package/dist/src/reactivity/push-state.js +4 -33
  151. package/dist/src/reactivity/push-state.js.map +1 -1
  152. package/dist/src/reactivity/wire-validate.d.ts +7 -22
  153. package/dist/src/reactivity/wire-validate.d.ts.map +1 -1
  154. package/dist/src/reactivity/wire-validate.js +7 -73
  155. package/dist/src/reactivity/wire-validate.js.map +1 -1
  156. package/dist/src/reactivity/wire.js +3 -3
  157. package/dist/src/reactivity/wire.js.map +1 -1
  158. package/dist/src/testing/async-wait.d.ts +23 -0
  159. package/dist/src/testing/async-wait.d.ts.map +1 -0
  160. package/dist/src/testing/async-wait.js +42 -0
  161. package/dist/src/testing/async-wait.js.map +1 -0
  162. package/dist/src/testing/index.d.ts +3 -0
  163. package/dist/src/testing/index.d.ts.map +1 -0
  164. package/dist/src/testing/index.js +3 -0
  165. package/dist/src/testing/index.js.map +1 -0
  166. package/dist/src/testing/test-transactor.d.ts +44 -0
  167. package/dist/src/testing/test-transactor.d.ts.map +1 -0
  168. package/dist/src/testing/test-transactor.js +423 -0
  169. package/dist/src/testing/test-transactor.js.map +1 -0
  170. package/dist/src/transaction/actions-engine.d.ts +6 -3
  171. package/dist/src/transaction/actions-engine.d.ts.map +1 -1
  172. package/dist/src/transaction/actions-engine.js +8 -7
  173. package/dist/src/transaction/actions-engine.js.map +1 -1
  174. package/dist/src/transaction/coordinator.d.ts +43 -22
  175. package/dist/src/transaction/coordinator.d.ts.map +1 -1
  176. package/dist/src/transaction/coordinator.js +443 -178
  177. package/dist/src/transaction/coordinator.js.map +1 -1
  178. package/dist/src/transaction/errors.d.ts +82 -0
  179. package/dist/src/transaction/errors.d.ts.map +1 -0
  180. package/dist/src/transaction/errors.js +93 -0
  181. package/dist/src/transaction/errors.js.map +1 -0
  182. package/dist/src/transaction/index.d.ts +6 -3
  183. package/dist/src/transaction/index.d.ts.map +1 -1
  184. package/dist/src/transaction/index.js +4 -2
  185. package/dist/src/transaction/index.js.map +1 -1
  186. package/dist/src/transaction/operations-hash.d.ts +119 -0
  187. package/dist/src/transaction/operations-hash.d.ts.map +1 -0
  188. package/dist/src/transaction/operations-hash.js +180 -0
  189. package/dist/src/transaction/operations-hash.js.map +1 -0
  190. package/dist/src/transaction/read-dependency-collector.d.ts +43 -0
  191. package/dist/src/transaction/read-dependency-collector.d.ts.map +1 -0
  192. package/dist/src/transaction/read-dependency-collector.js +69 -0
  193. package/dist/src/transaction/read-dependency-collector.js.map +1 -0
  194. package/dist/src/transaction/session.d.ts +12 -3
  195. package/dist/src/transaction/session.d.ts.map +1 -1
  196. package/dist/src/transaction/session.js +35 -9
  197. package/dist/src/transaction/session.js.map +1 -1
  198. package/dist/src/transaction/transaction.d.ts +162 -1
  199. package/dist/src/transaction/transaction.d.ts.map +1 -1
  200. package/dist/src/transaction/transaction.js +85 -3
  201. package/dist/src/transaction/transaction.js.map +1 -1
  202. package/dist/src/transaction/validator.d.ts +3 -11
  203. package/dist/src/transaction/validator.d.ts.map +1 -1
  204. package/dist/src/transaction/validator.js +69 -28
  205. package/dist/src/transaction/validator.js.map +1 -1
  206. package/dist/src/transactor/network-transactor.d.ts +35 -0
  207. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  208. package/dist/src/transactor/network-transactor.js +146 -40
  209. package/dist/src/transactor/network-transactor.js.map +1 -1
  210. package/dist/src/transactor/transactor-source.d.ts +19 -5
  211. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  212. package/dist/src/transactor/transactor-source.js +43 -11
  213. package/dist/src/transactor/transactor-source.js.map +1 -1
  214. package/dist/src/transform/atomic-proxy.d.ts +35 -5
  215. package/dist/src/transform/atomic-proxy.d.ts.map +1 -1
  216. package/dist/src/transform/atomic-proxy.js +39 -7
  217. package/dist/src/transform/atomic-proxy.js.map +1 -1
  218. package/dist/src/transform/cache-source.d.ts +36 -5
  219. package/dist/src/transform/cache-source.d.ts.map +1 -1
  220. package/dist/src/transform/cache-source.js +84 -8
  221. package/dist/src/transform/cache-source.js.map +1 -1
  222. package/dist/src/transform/helpers.d.ts.map +1 -1
  223. package/dist/src/transform/helpers.js +8 -2
  224. package/dist/src/transform/helpers.js.map +1 -1
  225. package/dist/src/transform/tracker.d.ts +17 -2
  226. package/dist/src/transform/tracker.d.ts.map +1 -1
  227. package/dist/src/transform/tracker.js +66 -11
  228. package/dist/src/transform/tracker.js.map +1 -1
  229. package/dist/src/utility/backoff.d.ts +48 -0
  230. package/dist/src/utility/backoff.d.ts.map +1 -0
  231. package/dist/src/utility/backoff.js +60 -0
  232. package/dist/src/utility/backoff.js.map +1 -0
  233. package/dist/src/utility/pending.d.ts +1 -0
  234. package/dist/src/utility/pending.d.ts.map +1 -1
  235. package/dist/src/utility/pending.js +3 -1
  236. package/dist/src/utility/pending.js.map +1 -1
  237. package/package.json +4 -4
  238. package/src/blocks/block-store.ts +19 -1
  239. package/src/blocks/helpers.ts +3 -3
  240. package/src/btree/btree.ts +108 -63
  241. package/src/chain/chain.ts +11 -1
  242. package/src/cluster/index.ts +1 -0
  243. package/src/cluster/membership.ts +85 -0
  244. package/src/cluster/structs.ts +51 -1
  245. package/src/cohort-topic/addressing.ts +12 -4
  246. package/src/cohort-topic/antidos/bootstrap-evidence.ts +21 -9
  247. package/src/cohort-topic/antidos/replay-guard.ts +37 -0
  248. package/src/cohort-topic/antidos/topic-budget.ts +17 -2
  249. package/src/cohort-topic/coldstart.ts +48 -15
  250. package/src/cohort-topic/gossip/bus.ts +39 -2
  251. package/src/cohort-topic/gossip/view.ts +26 -1
  252. package/src/cohort-topic/member-engine.ts +36 -15
  253. package/src/cohort-topic/membership/publisher.ts +25 -33
  254. package/src/cohort-topic/membership/verifier.ts +119 -3
  255. package/src/cohort-topic/promotion.ts +23 -2
  256. package/src/cohort-topic/registration/renewal.ts +73 -1
  257. package/src/cohort-topic/registration/types.ts +15 -0
  258. package/src/cohort-topic/service.ts +41 -15
  259. package/src/cohort-topic/sig/payloads.ts +29 -9
  260. package/src/cohort-topic/traffic.ts +23 -1
  261. package/src/cohort-topic/walk.ts +95 -24
  262. package/src/cohort-topic/wire/codec.ts +12 -0
  263. package/src/cohort-topic/wire/index.ts +2 -0
  264. package/src/cohort-topic/wire/payloads.ts +14 -5
  265. package/src/cohort-topic/wire/primitives.ts +188 -0
  266. package/src/cohort-topic/wire/types.ts +136 -4
  267. package/src/cohort-topic/wire/validate.ts +168 -134
  268. package/src/collection/collection.ts +123 -40
  269. package/src/collection/struct.ts +44 -2
  270. package/src/collections/tree/tree.ts +42 -5
  271. package/src/index.ts +1 -0
  272. package/src/log/log.ts +37 -9
  273. package/src/log/struct.ts +5 -4
  274. package/src/matchmaking/wire.ts +25 -102
  275. package/src/network/i-key-network.ts +10 -0
  276. package/src/network/i-repo.ts +8 -0
  277. package/src/network/struct.ts +10 -0
  278. package/src/reactivity/push-state.ts +4 -37
  279. package/src/reactivity/wire-validate.ts +7 -81
  280. package/src/reactivity/wire.ts +3 -3
  281. package/src/testing/async-wait.ts +65 -0
  282. package/src/testing/index.ts +2 -0
  283. package/src/testing/test-transactor.ts +468 -0
  284. package/src/transaction/actions-engine.ts +8 -6
  285. package/src/transaction/coordinator.ts +498 -229
  286. package/src/transaction/errors.ts +91 -0
  287. package/src/transaction/index.ts +23 -3
  288. package/src/transaction/operations-hash.ts +196 -0
  289. package/src/transaction/read-dependency-collector.ts +78 -0
  290. package/src/transaction/session.ts +37 -9
  291. package/src/transaction/transaction.ts +211 -4
  292. package/src/transaction/validator.ts +73 -48
  293. package/src/transactor/network-transactor.ts +164 -49
  294. package/src/transactor/transactor-source.ts +47 -12
  295. package/src/transform/atomic-proxy.ts +92 -49
  296. package/src/transform/cache-source.ts +87 -9
  297. package/src/transform/helpers.ts +8 -2
  298. package/src/transform/tracker.ts +69 -11
  299. package/src/utility/backoff.ts +95 -0
  300. package/src/utility/pending.ts +3 -1
  301. package/src/transaction/context.ts +0 -103
@@ -1,38 +1,102 @@
1
- import type { IBlock, BlockHeader, BlockId, BlockSource, BlockType, Transforms } from "../index.js";
1
+ import type { IBlock, BlockHeader, BlockId, BlockSource, BlockType, ReadPurpose, Transforms } from "../index.js";
2
2
  import { applyOperation } from "../index.js";
3
3
  import { LruMap } from "../utility/lru-map.js";
4
4
  import { createLogger } from "../logger.js";
5
+ import type { ReadDependencyCollector } from "../transaction/read-dependency-collector.js";
5
6
 
6
7
  const log = createLogger('cache');
7
8
 
8
9
  const DefaultMaxSize = 128;
9
10
 
11
+ /** The revision a source reports for an id, or undefined if the source can't report one.
12
+ * Duck-typed exactly like {@link Tracker}'s getGeneration probe — CacheSource layers over
13
+ * arbitrary BlockSources (including test doubles) that need not implement it. */
14
+ function sourceReadRevision(source: unknown, id: BlockId): number | undefined {
15
+ const src = source as { getReadRevision?: (id: BlockId) => number | undefined };
16
+ return typeof src.getReadRevision === 'function' ? src.getReadRevision(id) : undefined;
17
+ }
18
+
10
19
  export class CacheSource<T extends IBlock> implements BlockSource<T> {
11
20
  protected cache: LruMap<BlockId, T>;
21
+ /** Per-id monotonic counter, bumped whenever the cached content for an id changes.
22
+ * Consumers (e.g. {@link Tracker}'s materialized-block memo) read it via
23
+ * {@link getGeneration} to detect that a cached "source + ops" result has gone stale.
24
+ * Over-bumping is safe (it only forces a re-materialize); under-bumping is a correctness
25
+ * bug, so every content-changing site bumps. A benign LRU evict + reload also bumps. */
26
+ // NOTE: generations is never pruned — it retains one small (id → number) entry per distinct id
27
+ // ever touched, even after LRU eviction from `cache`. Bounded by the number of distinct blocks a
28
+ // collection sees over its lifetime; if that ever grows large enough to matter, evict alongside
29
+ // the LRU (dropping a generation is safe — a reload re-bumps from 0/absent, forcing re-materialize).
30
+ private generations = new Map<BlockId, number>();
31
+ /** Per-id committed revision of the content currently cached for that id. Learned from the
32
+ * source on a miss-load, advanced by {@link transformCache} when a commit folds new content
33
+ * in, and dropped alongside the cached block on delete/clear. Re-emitted on every cache HIT
34
+ * so a hit records a read dependency at the right revision — the whole point of this map, since
35
+ * the underlying source is never consulted on a hit. */
36
+ // NOTE: an LRU-evicted id can leave a stale `revisions` entry (eviction drops `cache` but not
37
+ // this map — see clear()/the LruMap eviction). Benign: the next read of that id is a cache MISS
38
+ // that re-learns the revision from the source and overwrites the entry before recording anything.
39
+ private revisions = new Map<BlockId, number>();
12
40
 
13
41
  constructor(
14
42
  protected readonly source: BlockSource<T>,
15
- maxSize = DefaultMaxSize
43
+ maxSize = DefaultMaxSize,
44
+ /** Shared per-transaction read-dependency accumulator (same instance the collection's
45
+ * TransactorSource holds). Optional: log-walk caches that never form a transaction omit it. */
46
+ private readonly collector?: ReadDependencyCollector,
16
47
  ) {
17
48
  this.cache = new LruMap(maxSize);
18
49
  }
19
50
 
20
- async tryGet(id: BlockId): Promise<T | undefined> {
51
+ private bump(id: BlockId) {
52
+ this.generations.set(id, (this.generations.get(id) ?? 0) + 1);
53
+ }
54
+
55
+ /** The current generation for an id — advances every time this cache's content for
56
+ * the id changes (miss-load, clear, or transformCache). Stable across pure cache hits. */
57
+ getGeneration(id: BlockId): number {
58
+ return this.generations.get(id) ?? 0;
59
+ }
60
+
61
+ async tryGet(id: BlockId, purpose: ReadPurpose = 'value'): Promise<T | undefined> {
21
62
  let block = this.cache.get(id);
22
63
  if (block) {
64
+ // Cache hit: the source is never consulted, so re-emit the revision we learned when this
65
+ // id was first loaded/folded. Without this a block served from cache records NO read
66
+ // dependency (the original bug), so its stale-read check could never fire. Carry the
67
+ // caller's purpose so a navigation-only cache hit stays droppable from the conflict set.
68
+ const rev = this.revisions.get(id);
69
+ if (rev !== undefined) this.collector?.record(id, rev, purpose);
23
70
  log('hit id=%s', id);
24
71
  } else {
25
- block = await this.source.tryGet(id);
72
+ block = await this.source.tryGet(id, purpose);
26
73
  if (block) {
27
74
  this.cache.set(id, block);
75
+ this.bump(id);
76
+ // Learn the revision from the source (which just served it) and record it. On a miss the
77
+ // underlying TransactorSource already recorded the same id@rev/purpose into the shared
78
+ // collector; max-wins (revision) + value-wins (purpose) collapse the two to one entry.
79
+ const rev = sourceReadRevision(this.source, id) ?? 0;
80
+ this.revisions.set(id, rev);
81
+ this.collector?.record(id, rev, purpose);
28
82
  log('miss:loaded id=%s cacheSize=%d', id, this.cache.size);
29
83
  } else {
84
+ // Absent block: record nothing (matches TransactorSource, which skips missing blocks).
30
85
  log('miss:absent id=%s', id);
31
86
  }
32
87
  }
33
88
  return structuredClone(block);
34
89
  }
35
90
 
91
+ /** Upgrade an already-captured read of `id` to a `value` read in the shared collector,
92
+ * retaining it in the conflict set. The B-tree point-lookup descent calls this (through the
93
+ * Tracker, which forwards) to pin the terminal leaf after recording the interior nodes as
94
+ * `navigation`. No-op when no collector is wired (log-walk caches) or the id was never
95
+ * recorded. Duck-typed by the Tracker; keep the name in sync with Tracker.markReadValue. */
96
+ markReadValue(id: BlockId): void {
97
+ this.collector?.markValue(id);
98
+ }
99
+
36
100
  generateId(): BlockId {
37
101
  return this.source.generateId();
38
102
  }
@@ -45,26 +109,40 @@ export class CacheSource<T extends IBlock> implements BlockSource<T> {
45
109
  if (blockIds) {
46
110
  for (const id of blockIds) {
47
111
  this.cache.delete(id);
112
+ this.revisions.delete(id);
113
+ this.bump(id);
48
114
  }
49
115
  } else {
116
+ for (const [id] of this.cache) {
117
+ this.bump(id);
118
+ }
50
119
  this.cache.clear();
120
+ this.revisions.clear();
51
121
  }
52
122
  }
53
123
 
54
- /** Mutates the cache without affecting the source */
55
- transformCache(transform: Transforms) {
124
+ /** Mutates the cache without affecting the source. `revision` is the committed revision this
125
+ * transform lands at; the stored per-id revision advances to it so a later read records a
126
+ * dependency at the NEW revision (recording the old one would spuriously fail validation). */
127
+ transformCache(transform: Transforms, revision: number) {
56
128
  for (const blockId of transform.deletes ?? []) {
57
129
  this.cache.delete(blockId);
130
+ this.revisions.delete(blockId);
131
+ this.bump(blockId);
58
132
  }
59
133
  for (const [, block] of Object.entries(transform.inserts ?? {})) {
60
134
  this.cache.set(block.header.id, structuredClone(block) as T);
135
+ this.revisions.set(block.header.id, revision);
136
+ this.bump(block.header.id);
61
137
  }
62
138
  for (const [blockId, operations] of Object.entries(transform.updates ?? {})) {
63
- for (const op of operations) {
64
- const block = this.cache.get(blockId);
65
- if (block) {
139
+ const block = this.cache.get(blockId);
140
+ if (block) {
141
+ for (const op of operations) {
66
142
  applyOperation(block, op);
143
+ this.bump(blockId);
67
144
  }
145
+ this.revisions.set(blockId, revision);
68
146
  }
69
147
  }
70
148
  }
@@ -72,10 +72,16 @@ export function copyTransforms(transform: Transforms): Transforms {
72
72
  }
73
73
 
74
74
  export function mergeTransforms(a: Transforms, b: Transforms): Transforms {
75
+ // updates: for a block id present in both sides, concatenate a's ops then b's ops
76
+ // (order-preserving) rather than letting b's array replace a's. inserts stay last-wins.
77
+ const updates: { [blockId: BlockId]: BlockOperation[] } = { ...a.updates };
78
+ for (const [id, ops] of Object.entries(b.updates ?? {})) {
79
+ updates[id] = updates[id] ? [...updates[id], ...ops] : ops;
80
+ }
75
81
  return {
76
82
  inserts: { ...a.inserts, ...b.inserts },
77
- updates: { ...a.updates, ...b.updates },
78
- deletes: [...(a.deletes ?? []), ...(b.deletes ?? [])]
83
+ updates,
84
+ deletes: [...new Set([...(a.deletes ?? []), ...(b.deletes ?? [])])]
79
85
  };
80
86
  }
81
87
 
@@ -1,29 +1,75 @@
1
- import type { IBlock, BlockId, BlockStore as IBlockStore, BlockHeader, BlockOperation, BlockType, BlockSource as IBlockSource } from "../index.js";
2
- import { applyOperation, emptyTransforms, blockIdsForTransforms, ensured } from "../index.js";
1
+ import type { IBlock, BlockId, BlockStore as IBlockStore, BlockHeader, BlockOperation, BlockType, BlockSource as IBlockSource, ReadPurpose } from "../index.js";
2
+ import { applyOperation, applyOperations, emptyTransforms, blockIdsForTransforms, ensured } from "../index.js";
3
3
 
4
4
  /** A block store that collects transformations, without applying them to the underlying source.
5
5
  * Transformations are also applied to the retrieved blocks, making it seem like the source has been modified.
6
6
  */
7
7
  export class Tracker<T extends IBlock> implements IBlockStore<T> {
8
+ /** Per-id memo of the materialized (source block + all `updates[id]` ops) result, so a
9
+ * repeated read of a hot op-carrying block is O(block size) instead of O(block size + ops).
10
+ * Kept fresh incrementally on {@link update}, dropped on {@link insert}/{@link delete}/{@link reset},
11
+ * and invalidated when the source's generation for the id advances (external cache mutation).
12
+ * Only populated for sources that expose `getGeneration` — without a drift signal we cannot
13
+ * detect source changes, so those fall back to always-replay. `gen` is the source generation of
14
+ * the base block content the memo was built from. */
15
+ private materialized = new Map<BlockId, { block: T; gen: number }>();
16
+
8
17
  constructor(
9
18
  private readonly source: IBlockSource<T>,
10
19
  /** The collected set of transformations to be applied. Treat as immutable */
11
20
  public transforms = emptyTransforms(),
12
21
  ) { }
13
22
 
14
- async tryGet(id: BlockId): Promise<T | undefined> {
15
- const block = await this.source.tryGet(id);
23
+ /** The source's generation for an id, or undefined if the source cannot report drift. */
24
+ private sourceGeneration(id: BlockId): number | undefined {
25
+ const src = this.source as { getGeneration?: (id: BlockId) => number };
26
+ return typeof src.getGeneration === 'function' ? src.getGeneration(id) : undefined;
27
+ }
28
+
29
+ async tryGet(id: BlockId, purpose: ReadPurpose = 'value'): Promise<T | undefined> {
30
+ // NOTE: precedence here is insert > delete > source+updates. In a well-formed transform an id is
31
+ // never in both `inserts` and `deletes` (insert/delete each clear the other), so order is moot. It
32
+ // only diverges from the canonical `applyTransform` (delete-last-wins, see struct.ts / helpers.ts:132)
33
+ // in the malformed insert+delete state reachable via the phantom-delete bug (double-delete then
34
+ // reinsert). Likewise the insert path intentionally skips `updates[id]` — inserted blocks bake ops
35
+ // in-place via update(); a stale pre-insert `updates[id]` is discarded here but would be re-applied
36
+ // on commit. Both are read-vs-commit inconsistencies confined to malformed states; fix the source
37
+ // bug (phantom delete / stale updates) rather than papering over it here.
38
+ if (this.transforms.inserts && Object.hasOwn(this.transforms.inserts, id)) {
39
+ return structuredClone(this.transforms.inserts[id]) as T;
40
+ }
41
+ if (this.transforms.deletes?.includes(id)) {
42
+ return undefined;
43
+ }
44
+ const gen = this.sourceGeneration(id);
45
+ const memo = this.materialized.get(id);
46
+ if (memo && (gen === undefined || memo.gen === gen)) {
47
+ return structuredClone(memo.block); // O(block size), no replay
48
+ }
49
+ const block = await this.source.tryGet(id, purpose);
16
50
  if (block) {
17
51
  const ops = this.transforms.updates?.[id] ?? [];
18
- ops.forEach(op => applyOperation(block!, op));
19
- if (this.transforms.deletes?.includes(id)) {
20
- return undefined;
52
+ if (ops.length > 0) {
53
+ applyOperations(block, ops);
54
+ // Memoize only when the source can report drift, and stamp with the generation read
55
+ // AFTER the load — the source may bump during tryGet (a cache miss-load), and stamping
56
+ // with the pre-load generation would force a needless reload on the very next read.
57
+ const freshGen = this.sourceGeneration(id);
58
+ if (freshGen !== undefined) {
59
+ this.materialized.set(id, { block, gen: freshGen });
60
+ }
61
+ return structuredClone(block); // clone so callers can't mutate the memo
21
62
  }
22
- } else if (this.transforms.inserts && Object.hasOwn(this.transforms.inserts, id)) {
23
- return structuredClone(this.transforms.inserts[id]) as T;
24
63
  }
64
+ return block; // no-ops path unchanged (source already cloned)
65
+ }
25
66
 
26
- return block;
67
+ /** Forward a leaf-value upgrade down to the source's read collector (duck-typed: only the
68
+ * CacheSource layer implements it). Lets the B-tree point-lookup descent, which reads through
69
+ * this tracker, pin its terminal leaf as a `value` read after tagging interior nodes
70
+ * `navigation`. No-op for sources without a collector (test doubles, log-walk caches). */
71
+ markReadValue(id: BlockId): void {
72
+ (this.source as { markReadValue?: (id: BlockId) => void }).markReadValue?.(id);
27
73
  }
28
74
 
29
75
  generateId(): BlockId {
@@ -37,6 +83,8 @@ export class Tracker<T extends IBlock> implements IBlockStore<T> {
37
83
  insert(block: T) {
38
84
  const inserts = this.transforms.inserts ??= {};
39
85
  inserts[block.header.id] = structuredClone(block);
86
+ // Served from `inserts` now, not source+updates — the materialized memo no longer applies.
87
+ this.materialized.delete(block.header.id);
40
88
  const deletes = this.transforms.deletes;
41
89
  const deleteIndex = deletes?.indexOf(block.header.id) ?? -1;
42
90
  if (deleteIndex >= 0) {
@@ -51,12 +99,21 @@ export class Tracker<T extends IBlock> implements IBlockStore<T> {
51
99
  } else {
52
100
  const updates = this.transforms.updates ??= {};
53
101
  ensured(updates, blockId, () => []).push(structuredClone(op));
102
+ // The memo already equals (base source content + prior ops); applying just the new op
103
+ // keeps it equal to the full ops list — O(1), no full replay. Leave `gen` untouched: it
104
+ // still records the base-content generation, so a later external source change still
105
+ // forces a reload. (Refreshing gen here would mask stale base content.)
106
+ const memo = this.materialized.get(blockId);
107
+ if (memo) {
108
+ applyOperation(memo.block, op);
109
+ }
54
110
  }
55
111
  }
56
112
 
57
113
  delete(blockId: BlockId) {
58
114
  if (this.transforms.inserts) delete this.transforms.inserts[blockId];
59
115
  if (this.transforms.updates) delete this.transforms.updates[blockId];
116
+ this.materialized.delete(blockId);
60
117
  const deletes = this.transforms.deletes ??= [];
61
118
  deletes.push(blockId);
62
119
  }
@@ -64,11 +121,12 @@ export class Tracker<T extends IBlock> implements IBlockStore<T> {
64
121
  reset(newTransform = emptyTransforms()) {
65
122
  const oldTransform = this.transforms;
66
123
  this.transforms = newTransform;
124
+ this.materialized.clear();
67
125
  return oldTransform;
68
126
  }
69
127
 
70
128
  transformedBlockIds(): BlockId[] {
71
- return Array.from(new Set(blockIdsForTransforms(this.transforms)));
129
+ return blockIdsForTransforms(this.transforms);
72
130
  }
73
131
 
74
132
  conflicts(blockIds: Set<BlockId>) {
@@ -0,0 +1,95 @@
1
+ import { randomBytes } from '@noble/hashes/utils.js';
2
+
3
+ /**
4
+ * Jittered exponential backoff shared by the retry loops in db-core
5
+ * ({@link Collection.sync} and {@link TransactionCoordinator.commit}).
6
+ *
7
+ * The point of the jitter is fairness under contention: when many clients lose the SAME
8
+ * optimistic-concurrency race at t=0, a bare exponential curve makes them all re-attempt at the
9
+ * identical next tick — a thundering herd that just re-collides. Multiplying the exponential value
10
+ * by a random factor spreads those re-attempts across a window so offered load per tick sheds
11
+ * instead of cascading. This is the same shape as `cohort-topic`'s `backoffRetryMs`, with the
12
+ * proportional-jitter term added.
13
+ */
14
+
15
+ /** A source of uniform randomness in [0, 1). Injected in tests so a deterministic sequence can be
16
+ * asserted; production uses {@link cryptoRand}. */
17
+ export type RandFn = () => number;
18
+
19
+ /** Uniform value in [0, 1) drawn from the same CSPRNG (`@noble/hashes` `randomBytes`) the rest of
20
+ * db-core uses — deliberately NOT `Math.random`, so backoff jitter shares the package's randomness
21
+ * source rather than introducing a second, weaker one. */
22
+ export function cryptoRand(): number {
23
+ const b = randomBytes(4);
24
+ // Assemble a uint32 (>>> 0 clears the sign bit the shifts would otherwise set), then normalise.
25
+ const u = (((b[0]! << 24) | (b[1]! << 16) | (b[2]! << 8) | b[3]!) >>> 0);
26
+ return u / 0x1_0000_0000;
27
+ }
28
+
29
+ export interface JitteredBackoffConfig {
30
+ /** Base delay (ms) for attempt 0, before jitter. */
31
+ baseMs: number;
32
+ /** Hard ceiling (ms) on the pre-jitter exponential value. */
33
+ capMs: number;
34
+ /** Geometric growth per attempt. Default 2 (doubling). */
35
+ factor?: number;
36
+ /** Fraction of the exponential value that jitter may subtract, in [0, 1]. Default 0.5, so the
37
+ * returned delay lands uniformly in `((1 - jitterFraction) · exp, exp]` — i.e. `(0.5·exp, exp]`. */
38
+ jitterFraction?: number;
39
+ }
40
+
41
+ /**
42
+ * Delay (ms) for retry `attempt` (0-based):
43
+ *
44
+ * exp = min(baseMs · factor^attempt, capMs)
45
+ * delay = exp · (1 - jitterFraction · rand()) // rand() ∈ [0, 1)
46
+ *
47
+ * With the defaults (factor 2, jitterFraction 0.5) the delay is uniform in `(0.5·exp, exp]`: never
48
+ * zero (as long as `baseMs > 0`) and never above `capMs`. `attempt` must be a non-negative integer
49
+ * — pass `consecutiveFailures - 1` so the first retry backs off by ~`baseMs`.
50
+ */
51
+ export function jitteredBackoffMs(
52
+ attempt: number,
53
+ config: JitteredBackoffConfig,
54
+ rand: RandFn = cryptoRand,
55
+ ): number {
56
+ if (!Number.isInteger(attempt) || attempt < 0) {
57
+ throw new RangeError(`attempt must be a non-negative integer, got ${attempt}`);
58
+ }
59
+ const factor = config.factor ?? 2;
60
+ const jitterFraction = config.jitterFraction ?? 0.5;
61
+ const exp = Math.min(config.baseMs * factor ** attempt, config.capMs);
62
+ return exp * (1 - jitterFraction * rand());
63
+ }
64
+
65
+ /** Build an AbortError for a cooperatively-aborted wait. Prefers the signal's own reason when it is
66
+ * an Error (so callers who passed a custom abort reason see it), otherwise a `name='AbortError'`
67
+ * Error. */
68
+ export function makeAbortError(signal?: AbortSignal): Error {
69
+ if (signal && signal.reason instanceof Error) {
70
+ return signal.reason;
71
+ }
72
+ const err = new Error('The operation was aborted');
73
+ err.name = 'AbortError';
74
+ return err;
75
+ }
76
+
77
+ /** Sleep for `ms`, resolving early (rejecting with an AbortError) if `signal` aborts — so a retry
78
+ * loop waiting out a backoff rejects promptly instead of finishing the sleep. */
79
+ export function abortableDelay(ms: number, signal?: AbortSignal): Promise<void> {
80
+ return new Promise<void>((resolve, reject) => {
81
+ if (signal?.aborted) {
82
+ reject(makeAbortError(signal));
83
+ return;
84
+ }
85
+ const onAbort = () => {
86
+ clearTimeout(timer);
87
+ reject(makeAbortError(signal!));
88
+ };
89
+ const timer = setTimeout(() => {
90
+ signal?.removeEventListener('abort', onAbort);
91
+ resolve();
92
+ }, ms);
93
+ signal?.addEventListener('abort', onAbort, { once: true });
94
+ });
95
+ }
@@ -1,11 +1,12 @@
1
1
  export class Pending<T> {
2
2
  response?: T;
3
3
  error?: unknown;
4
+ private _responded = false;
4
5
  t1 = Date.now();
5
6
  duration?: number;
6
7
 
7
8
  get isResponse(): boolean {
8
- return this.response !== undefined;
9
+ return this._responded;
9
10
  }
10
11
 
11
12
  get isError(): boolean {
@@ -32,6 +33,7 @@ export class Pending<T> {
32
33
  promise.then(response => {
33
34
  this.duration = Date.now() - this.t1;
34
35
  this.response = response;
36
+ this._responded = true;
35
37
  return response;
36
38
  }, error => {
37
39
  this.duration = Date.now() - this.t1;
@@ -1,103 +0,0 @@
1
- import type { CollectionId } from "../index.js";
2
- import type { TransactionCoordinator } from "./coordinator.js";
3
- import type { ReadDependency, ExecutionResult } from "./transaction.js";
4
- import type { Action } from "../collection/action.js";
5
-
6
- /**
7
- * Transaction context for accumulating actions and reads.
8
- *
9
- * Usage:
10
- * const txn = coordinator.begin();
11
- * txn.addAction('users', { type: 'insert', data: {...} });
12
- * txn.addAction('users', { type: 'get', data: { key: 1 } });
13
- * const result = await txn.commit();
14
- */
15
- export class TransactionContext {
16
- private readonly collectionActions: Map<CollectionId, Action<any>[]> = new Map();
17
- private readonly reads: ReadDependency[] = [];
18
-
19
- constructor(
20
- private readonly coordinator: TransactionCoordinator,
21
- public readonly transactionId: string,
22
- public readonly engine: string
23
- ) {}
24
-
25
- /**
26
- * Add an action to a collection.
27
- *
28
- * Actions are collection-specific:
29
- * - Tree: 'insert', 'delete', 'get', 'scan'
30
- * - Diary: 'append', 'read'
31
- * - etc.
32
- */
33
- async addAction(collectionId: CollectionId, action: Action<any>): Promise<void> {
34
- // Tag action with transaction reference
35
- const taggedAction = {
36
- ...action,
37
- transaction: this.transactionId
38
- };
39
-
40
- // Get the collection and immediately execute the action to update local snapshot
41
- const collection = this.coordinator['collections'].get(collectionId);
42
- if (collection) {
43
- // Execute through collection to update tracker and pending buffer
44
- await collection.act(taggedAction);
45
- }
46
- // If no collection registered, just buffer the action (for testing or deferred collection creation)
47
-
48
- // Record in transaction context
49
- if (!this.collectionActions.has(collectionId)) {
50
- this.collectionActions.set(collectionId, []);
51
- }
52
- this.collectionActions.get(collectionId)!.push(taggedAction);
53
- }
54
-
55
- /**
56
- * Add a read dependency for optimistic concurrency control.
57
- */
58
- addRead(read: ReadDependency): void {
59
- this.reads.push(read);
60
- }
61
-
62
- /**
63
- * Commit the transaction.
64
- *
65
- * This executes all accumulated actions across all affected collections,
66
- * coordinating with the network as needed.
67
- */
68
- async commit(): Promise<ExecutionResult> {
69
- return await this.coordinator.commitTransaction(this);
70
- }
71
-
72
- /**
73
- * Rollback the transaction (just discard accumulated state).
74
- */
75
- rollback(): void {
76
- this.collectionActions.clear();
77
- this.reads.length = 0;
78
- }
79
-
80
- /**
81
- * Get all accumulated actions by collection.
82
- * Used by coordinator during commit.
83
- */
84
- getCollectionActions(): Map<CollectionId, Action<any>[]> {
85
- return this.collectionActions;
86
- }
87
-
88
- /**
89
- * Get all accumulated read dependencies.
90
- * Used by coordinator during commit.
91
- */
92
- getReads(): ReadDependency[] {
93
- return this.reads;
94
- }
95
-
96
- /**
97
- * Get the set of affected collection IDs.
98
- */
99
- getAffectedCollections(): Set<CollectionId> {
100
- return new Set(this.collectionActions.keys());
101
- }
102
- }
103
-