@minnowdb/core 0.3.0 → 0.4.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.
Files changed (276) hide show
  1. package/README.md +4 -2
  2. package/dist/block-format/block.d.ts +32 -13
  3. package/dist/block-format/block.js +179 -55
  4. package/dist/block-format/checksum.d.ts +2 -1
  5. package/dist/block-format/checksum.js +5 -2
  6. package/dist/block-format/codecs.d.ts +7 -2
  7. package/dist/block-format/codecs.js +53 -12
  8. package/dist/block-format/column.d.ts +3 -2
  9. package/dist/block-format/column.js +96 -30
  10. package/dist/block-format/index.d.ts +2 -2
  11. package/dist/block-format/index.js +2 -2
  12. package/dist/block-format/physical.d.ts +8 -2
  13. package/dist/block-format/physical.js +14 -6
  14. package/dist/block-format/types.d.ts +7 -3
  15. package/dist/block-format/types.js +0 -1
  16. package/dist/block-format/unicode.d.ts +11 -0
  17. package/dist/block-format/unicode.js +46 -0
  18. package/dist/date-value.d.ts +15 -0
  19. package/dist/date-value.js +64 -0
  20. package/dist/engine/artifact-cache.d.ts +0 -1
  21. package/dist/engine/artifact-cache.js +0 -1
  22. package/dist/engine/batch.d.ts +5 -2
  23. package/dist/engine/batch.js +0 -1
  24. package/dist/engine/buffered-writer.d.ts +2 -1
  25. package/dist/engine/buffered-writer.js +34 -9
  26. package/dist/engine/cache-limits.d.ts +25 -0
  27. package/dist/engine/cache-limits.js +25 -0
  28. package/dist/engine/catalog.d.ts +5 -6
  29. package/dist/engine/catalog.js +5 -5
  30. package/dist/engine/client.d.ts +27 -7
  31. package/dist/engine/client.js +197 -31
  32. package/dist/engine/database.d.ts +161 -38
  33. package/dist/engine/database.js +4320 -1523
  34. package/dist/engine/defaults.d.ts +0 -1
  35. package/dist/engine/defaults.js +2 -4
  36. package/dist/engine/errors.d.ts +19 -1
  37. package/dist/engine/errors.js +32 -2
  38. package/dist/engine/fts.d.ts +0 -6
  39. package/dist/engine/fts.js +39 -14
  40. package/dist/engine/group-index.d.ts +0 -1
  41. package/dist/engine/group-index.js +6 -11
  42. package/dist/engine/index.d.ts +16 -11
  43. package/dist/engine/index.js +12 -10
  44. package/dist/engine/join-index.d.ts +0 -1
  45. package/dist/engine/join-index.js +2 -2
  46. package/dist/engine/keyed-live.d.ts +57 -0
  47. package/dist/engine/keyed-live.js +226 -0
  48. package/dist/engine/live-api.d.ts +4 -0
  49. package/dist/engine/live-api.js +4 -0
  50. package/dist/engine/live.d.ts +43 -26
  51. package/dist/engine/live.js +499 -129
  52. package/dist/engine/memory.d.ts +2 -1
  53. package/dist/engine/memory.js +3 -2
  54. package/dist/engine/optimizer.d.ts +0 -1
  55. package/dist/engine/optimizer.js +4 -4
  56. package/dist/engine/query-api.d.ts +3 -0
  57. package/dist/engine/query-api.js +3 -0
  58. package/dist/engine/query-cache.d.ts +0 -1
  59. package/dist/engine/query-cache.js +4 -4
  60. package/dist/engine/query.d.ts +4 -7
  61. package/dist/engine/query.js +348 -155
  62. package/dist/engine/result-wire.d.ts +0 -1
  63. package/dist/engine/result-wire.js +2 -2
  64. package/dist/engine/schema-wire.d.ts +1 -2
  65. package/dist/engine/schema-wire.js +0 -1
  66. package/dist/engine/schema.d.ts +3 -8
  67. package/dist/engine/schema.js +41 -24
  68. package/dist/engine/sort-keys.d.ts +0 -1
  69. package/dist/engine/sort-keys.js +2 -2
  70. package/dist/engine/sql-domains.d.ts +6 -1
  71. package/dist/engine/sql-domains.js +209 -45
  72. package/dist/engine/sql-driver.d.ts +2 -2
  73. package/dist/engine/sql-driver.js +0 -1
  74. package/dist/engine/sql-json.d.ts +0 -1
  75. package/dist/engine/sql-json.js +53 -7
  76. package/dist/engine/sql-semantics.d.ts +8 -5
  77. package/dist/engine/sql-semantics.js +427 -65
  78. package/dist/engine/typed-live.d.ts +67 -0
  79. package/dist/engine/typed-live.js +349 -0
  80. package/dist/engine/vector.d.ts +7 -1
  81. package/dist/engine/vector.js +202 -58
  82. package/dist/engine/worker-host.d.ts +8 -2
  83. package/dist/engine/worker-host.js +616 -152
  84. package/dist/engine/worker.d.ts +0 -1
  85. package/dist/engine/worker.js +3 -3
  86. package/dist/engine/write-block-planner.d.ts +18 -0
  87. package/dist/engine/write-block-planner.js +112 -0
  88. package/dist/index.d.ts +0 -1
  89. package/dist/index.js +0 -1
  90. package/dist/plan/index.d.ts +0 -1
  91. package/dist/plan/index.js +0 -1
  92. package/dist/storage/index.d.ts +1 -1
  93. package/dist/storage/index.js +1 -1
  94. package/dist/storage/indexeddb.d.ts +74 -65
  95. package/dist/storage/indexeddb.js +12301 -2076
  96. package/dist/storage/memory.d.ts +56 -66
  97. package/dist/storage/memory.js +1004 -139
  98. package/dist/storage/opfs/files.d.ts +18 -7
  99. package/dist/storage/opfs/files.js +115 -28
  100. package/dist/storage/opfs/index.d.ts +1 -1
  101. package/dist/storage/opfs/index.js +1 -1
  102. package/dist/storage/opfs/leader.d.ts +403 -91
  103. package/dist/storage/opfs/leader.js +4306 -602
  104. package/dist/storage/opfs/rpc.d.ts +15 -3
  105. package/dist/storage/opfs/rpc.js +223 -2
  106. package/dist/storage/opfs/snapshot-ledger.d.ts +40 -0
  107. package/dist/storage/opfs/snapshot-ledger.js +281 -0
  108. package/dist/storage/opfs/store.d.ts +26 -121
  109. package/dist/storage/opfs/store.js +436 -388
  110. package/dist/storage/persistence.d.ts +37 -0
  111. package/dist/storage/persistence.js +78 -0
  112. package/dist/storage/snapshot-stream.d.ts +24 -0
  113. package/dist/storage/snapshot-stream.js +897 -0
  114. package/dist/storage/snapshot.d.ts +9 -99
  115. package/dist/storage/snapshot.js +33 -309
  116. package/dist/storage/toolkit/extents.d.ts +41 -7
  117. package/dist/storage/toolkit/extents.js +402 -39
  118. package/dist/storage/toolkit/index.d.ts +3 -4
  119. package/dist/storage/toolkit/index.js +27 -3
  120. package/dist/storage/toolkit/record-core.d.ts +174 -61
  121. package/dist/storage/toolkit/record-core.js +5401 -849
  122. package/dist/storage/toolkit/sync-file.d.ts +10 -1
  123. package/dist/storage/toolkit/sync-file.js +50 -2
  124. package/dist/storage/toolkit/wal.d.ts +23 -5
  125. package/dist/storage/toolkit/wal.js +89 -27
  126. package/dist/storage/toolkit/wire.d.ts +4 -6
  127. package/dist/storage/toolkit/wire.js +80 -25
  128. package/dist/storage/types.d.ts +1271 -218
  129. package/dist/storage/types.js +1471 -138
  130. package/dist/testing/block-store-conformance.d.ts +0 -1
  131. package/dist/testing/block-store-conformance.js +877 -138
  132. package/dist/testing/index.d.ts +42 -31
  133. package/dist/testing/index.js +100 -68
  134. package/dist/testing/opfs-shim.d.ts +13 -3
  135. package/dist/testing/opfs-shim.js +40 -6
  136. package/dist/testing/simulator.d.ts +0 -1
  137. package/dist/testing/simulator.js +43 -12
  138. package/dist/testing/sqllogictest.d.ts +0 -1
  139. package/dist/testing/sqllogictest.js +0 -1
  140. package/dist/transactions/index.d.ts +81 -20
  141. package/dist/transactions/index.js +1050 -268
  142. package/dist/worker-protocol/index.d.ts +3 -2
  143. package/dist/worker-protocol/index.js +2 -5
  144. package/package.json +49 -1
  145. package/dist/block-format/block.d.ts.map +0 -1
  146. package/dist/block-format/block.js.map +0 -1
  147. package/dist/block-format/checksum.d.ts.map +0 -1
  148. package/dist/block-format/checksum.js.map +0 -1
  149. package/dist/block-format/codecs.d.ts.map +0 -1
  150. package/dist/block-format/codecs.js.map +0 -1
  151. package/dist/block-format/column.d.ts.map +0 -1
  152. package/dist/block-format/column.js.map +0 -1
  153. package/dist/block-format/index.d.ts.map +0 -1
  154. package/dist/block-format/index.js.map +0 -1
  155. package/dist/block-format/physical.d.ts.map +0 -1
  156. package/dist/block-format/physical.js.map +0 -1
  157. package/dist/block-format/types.d.ts.map +0 -1
  158. package/dist/block-format/types.js.map +0 -1
  159. package/dist/engine/artifact-cache.d.ts.map +0 -1
  160. package/dist/engine/artifact-cache.js.map +0 -1
  161. package/dist/engine/batch.d.ts.map +0 -1
  162. package/dist/engine/batch.js.map +0 -1
  163. package/dist/engine/buffered-writer.d.ts.map +0 -1
  164. package/dist/engine/buffered-writer.js.map +0 -1
  165. package/dist/engine/catalog.d.ts.map +0 -1
  166. package/dist/engine/catalog.js.map +0 -1
  167. package/dist/engine/client.d.ts.map +0 -1
  168. package/dist/engine/client.js.map +0 -1
  169. package/dist/engine/coordinator.d.ts +0 -17
  170. package/dist/engine/coordinator.d.ts.map +0 -1
  171. package/dist/engine/coordinator.js +0 -60
  172. package/dist/engine/coordinator.js.map +0 -1
  173. package/dist/engine/database.d.ts.map +0 -1
  174. package/dist/engine/database.js.map +0 -1
  175. package/dist/engine/defaults.d.ts.map +0 -1
  176. package/dist/engine/defaults.js.map +0 -1
  177. package/dist/engine/errors.d.ts.map +0 -1
  178. package/dist/engine/errors.js.map +0 -1
  179. package/dist/engine/fts.d.ts.map +0 -1
  180. package/dist/engine/fts.js.map +0 -1
  181. package/dist/engine/group-index.d.ts.map +0 -1
  182. package/dist/engine/group-index.js.map +0 -1
  183. package/dist/engine/index.d.ts.map +0 -1
  184. package/dist/engine/index.js.map +0 -1
  185. package/dist/engine/join-index.d.ts.map +0 -1
  186. package/dist/engine/join-index.js.map +0 -1
  187. package/dist/engine/live.d.ts.map +0 -1
  188. package/dist/engine/live.js.map +0 -1
  189. package/dist/engine/memory.d.ts.map +0 -1
  190. package/dist/engine/memory.js.map +0 -1
  191. package/dist/engine/optimizer.d.ts.map +0 -1
  192. package/dist/engine/optimizer.js.map +0 -1
  193. package/dist/engine/query-cache.d.ts.map +0 -1
  194. package/dist/engine/query-cache.js.map +0 -1
  195. package/dist/engine/query.d.ts.map +0 -1
  196. package/dist/engine/query.js.map +0 -1
  197. package/dist/engine/result-wire.d.ts.map +0 -1
  198. package/dist/engine/result-wire.js.map +0 -1
  199. package/dist/engine/schema-wire.d.ts.map +0 -1
  200. package/dist/engine/schema-wire.js.map +0 -1
  201. package/dist/engine/schema.d.ts.map +0 -1
  202. package/dist/engine/schema.js.map +0 -1
  203. package/dist/engine/sort-keys.d.ts.map +0 -1
  204. package/dist/engine/sort-keys.js.map +0 -1
  205. package/dist/engine/sql-domains.d.ts.map +0 -1
  206. package/dist/engine/sql-domains.js.map +0 -1
  207. package/dist/engine/sql-driver.d.ts.map +0 -1
  208. package/dist/engine/sql-driver.js.map +0 -1
  209. package/dist/engine/sql-json.d.ts.map +0 -1
  210. package/dist/engine/sql-json.js.map +0 -1
  211. package/dist/engine/sql-semantics.d.ts.map +0 -1
  212. package/dist/engine/sql-semantics.js.map +0 -1
  213. package/dist/engine/vector.d.ts.map +0 -1
  214. package/dist/engine/vector.js.map +0 -1
  215. package/dist/engine/worker-host.d.ts.map +0 -1
  216. package/dist/engine/worker-host.js.map +0 -1
  217. package/dist/engine/worker.d.ts.map +0 -1
  218. package/dist/engine/worker.js.map +0 -1
  219. package/dist/index.d.ts.map +0 -1
  220. package/dist/index.js.map +0 -1
  221. package/dist/plan/index.d.ts.map +0 -1
  222. package/dist/plan/index.js.map +0 -1
  223. package/dist/storage/fixture-shape.d.ts +0 -42
  224. package/dist/storage/fixture-shape.d.ts.map +0 -1
  225. package/dist/storage/fixture-shape.js +0 -146
  226. package/dist/storage/fixture-shape.js.map +0 -1
  227. package/dist/storage/index.d.ts.map +0 -1
  228. package/dist/storage/index.js.map +0 -1
  229. package/dist/storage/indexeddb.d.ts.map +0 -1
  230. package/dist/storage/indexeddb.js.map +0 -1
  231. package/dist/storage/memory.d.ts.map +0 -1
  232. package/dist/storage/memory.js.map +0 -1
  233. package/dist/storage/opfs/files.d.ts.map +0 -1
  234. package/dist/storage/opfs/files.js.map +0 -1
  235. package/dist/storage/opfs/index.d.ts.map +0 -1
  236. package/dist/storage/opfs/index.js.map +0 -1
  237. package/dist/storage/opfs/leader.d.ts.map +0 -1
  238. package/dist/storage/opfs/leader.js.map +0 -1
  239. package/dist/storage/opfs/rpc.d.ts.map +0 -1
  240. package/dist/storage/opfs/rpc.js.map +0 -1
  241. package/dist/storage/opfs/store.d.ts.map +0 -1
  242. package/dist/storage/opfs/store.js.map +0 -1
  243. package/dist/storage/snapshot.d.ts.map +0 -1
  244. package/dist/storage/snapshot.js.map +0 -1
  245. package/dist/storage/toolkit/extents.d.ts.map +0 -1
  246. package/dist/storage/toolkit/extents.js.map +0 -1
  247. package/dist/storage/toolkit/index.d.ts.map +0 -1
  248. package/dist/storage/toolkit/index.js.map +0 -1
  249. package/dist/storage/toolkit/record-core.d.ts.map +0 -1
  250. package/dist/storage/toolkit/record-core.js.map +0 -1
  251. package/dist/storage/toolkit/sync-file.d.ts.map +0 -1
  252. package/dist/storage/toolkit/sync-file.js.map +0 -1
  253. package/dist/storage/toolkit/wal.d.ts.map +0 -1
  254. package/dist/storage/toolkit/wal.js.map +0 -1
  255. package/dist/storage/toolkit/wire.d.ts.map +0 -1
  256. package/dist/storage/toolkit/wire.js.map +0 -1
  257. package/dist/storage/types.d.ts.map +0 -1
  258. package/dist/storage/types.js.map +0 -1
  259. package/dist/testing/block-store-conformance.d.ts.map +0 -1
  260. package/dist/testing/block-store-conformance.js.map +0 -1
  261. package/dist/testing/index.d.ts.map +0 -1
  262. package/dist/testing/index.js.map +0 -1
  263. package/dist/testing/opfs-shim.d.ts.map +0 -1
  264. package/dist/testing/opfs-shim.js.map +0 -1
  265. package/dist/testing/seeds.d.ts +0 -11
  266. package/dist/testing/seeds.d.ts.map +0 -1
  267. package/dist/testing/seeds.js +0 -50
  268. package/dist/testing/seeds.js.map +0 -1
  269. package/dist/testing/simulator.d.ts.map +0 -1
  270. package/dist/testing/simulator.js.map +0 -1
  271. package/dist/testing/sqllogictest.d.ts.map +0 -1
  272. package/dist/testing/sqllogictest.js.map +0 -1
  273. package/dist/transactions/index.d.ts.map +0 -1
  274. package/dist/transactions/index.js.map +0 -1
  275. package/dist/worker-protocol/index.d.ts.map +0 -1
  276. package/dist/worker-protocol/index.js.map +0 -1
@@ -1,95 +1,245 @@
1
+ import { readFully, writeFully } from "./sync-file.js";
2
+ import { crc32 } from "../../block-format/index.js";
3
+ /** Opaque O(1) rollback point for bytes not yet published by a WAL frame. */
4
+ export class ExtentBatchMark {
5
+ tailExtentId;
6
+ nextExtentId;
7
+ tailOffset;
8
+ /** @internal Original counters are captured lazily only for extents the batch touches. */
9
+ _liveBytesBefore = new Map();
10
+ constructor(tailExtentId, nextExtentId, tailOffset) {
11
+ this.tailExtentId = tailExtentId;
12
+ this.nextExtentId = nextExtentId;
13
+ this.tailOffset = tailOffset;
14
+ }
15
+ }
1
16
  const SEAL_BYTES = 8 * 1024 * 1024;
2
17
  const READ_HANDLE_CACHE = 12;
3
18
  export class ExtentPool {
4
19
  #tree;
20
+ #checksum;
5
21
  #tailId;
6
22
  #nextId;
7
23
  #tailOffset;
8
24
  #tailHandle;
25
+ #tailDirty = false;
9
26
  #liveBytes = new Map();
10
27
  /** Sealed-extent read handles, LRU by insertion order. The tail is never in here. */
11
28
  #readHandles = new Map();
12
- constructor(tree, meta, tailHandle) {
29
+ #openingReadHandles = new Map();
30
+ #deletingExtents = new Set();
31
+ #activeBatch;
32
+ #closed = false;
33
+ constructor(tree, meta, tailHandle, checksum) {
13
34
  this.#tree = tree;
35
+ this.#checksum = checksum;
14
36
  this.#tailId = meta.tailId;
15
37
  this.#nextId = meta.nextId;
16
38
  this.#tailOffset = meta.tailOffset;
17
39
  this.#tailHandle = tailHandle;
18
40
  }
19
41
  /** Opens (or creates) the tail extent. `meta` comes from the checkpoint, if any. */
20
- static async open(tree, meta) {
42
+ static async open(tree, meta, options = {}) {
43
+ if (meta !== undefined)
44
+ assertValidExtentMeta(meta);
21
45
  const tailId = meta?.tailExtentId ?? 0;
22
46
  const nextId = meta?.nextExtentId ?? 1;
23
47
  const tailHandle = await tree.openHandle(extentPath(tailId), { create: true });
24
- const pool = new ExtentPool(tree, { tailId, nextId, tailOffset: meta?.tailOffset ?? 0 }, tailHandle);
48
+ const pool = new ExtentPool(tree, { tailId, nextId, tailOffset: meta?.tailOffset ?? 0 }, tailHandle, options._checksumForTests ?? crc32);
25
49
  for (const [id, bytes] of meta?.liveBytes ?? [])
26
50
  pool.#liveBytes.set(id, bytes);
51
+ if (tailHandle.getSize() < pool.#tailOffset) {
52
+ pool.close();
53
+ throw new Error(`OPFS tail extent ${String(tailId)} is shorter than its checkpointed offset: ` +
54
+ `${String(tailHandle.getSize())} < ${String(pool.#tailOffset)}`);
55
+ }
27
56
  return pool;
28
57
  }
58
+ /** Captures the exact append/accounting state before an unpublished multi-payload batch. */
59
+ markBatch() {
60
+ if (this.#activeBatch !== undefined)
61
+ throw new Error("An extent append batch is already active");
62
+ const mark = new ExtentBatchMark(this.#tailId, this.#nextId, this.#tailOffset);
63
+ this.#activeBatch = mark;
64
+ return mark;
65
+ }
66
+ /** Publishes a successful batch; constant-time and allocation-free. */
67
+ commitBatch(mark) {
68
+ if (this.#activeBatch !== mark)
69
+ throw new Error("Extent batch mark is not active");
70
+ this.#activeBatch = undefined;
71
+ }
72
+ /**
73
+ * Rolls back every append and accounting mutation since `mark`. This is deliberately an
74
+ * async slow path; successful appends retain the allocation-free hot path.
75
+ */
76
+ async rollbackBatch(mark) {
77
+ if (this.#activeBatch !== mark)
78
+ throw new Error("Extent batch mark is not active");
79
+ if (mark.nextExtentId > this.#nextId) {
80
+ throw new Error("Cannot roll an extent pool forward to a future batch mark");
81
+ }
82
+ this.#tailHandle.close();
83
+ // Every id allocated after the mark is unpublished by definition. Delete it before
84
+ // restoring the former tail so repeated refused batches cannot accumulate sealed files.
85
+ for (let id = mark.nextExtentId; id < this.#nextId; id += 1) {
86
+ await this.#closeCachedReadHandle(id);
87
+ await this.#tree.deleteFile(extentPath(id));
88
+ }
89
+ await this.#closeCachedReadHandle(mark.tailExtentId);
90
+ this.#tailHandle = await this.#tree.openHandle(extentPath(mark.tailExtentId), { create: true });
91
+ this.#tailHandle.truncate(mark.tailOffset);
92
+ this.#tailId = mark.tailExtentId;
93
+ this.#nextId = mark.nextExtentId;
94
+ this.#tailOffset = mark.tailOffset;
95
+ this.#tailDirty = false;
96
+ for (const [id, bytes] of mark._liveBytesBefore) {
97
+ if (bytes === undefined)
98
+ this.#liveBytes.delete(id);
99
+ else
100
+ this.#liveBytes.set(id, bytes);
101
+ }
102
+ this.#activeBatch = undefined;
103
+ }
104
+ async #closeCachedReadHandle(id) {
105
+ const opening = this.#openingReadHandles.get(id);
106
+ if (opening !== undefined)
107
+ await opening.promise.catch(() => undefined);
108
+ const cached = this.#readHandles.get(id);
109
+ if (cached !== undefined && cached.readers > 0) {
110
+ await new Promise((resolve) => cached.drained.push(resolve));
111
+ }
112
+ this.#readHandles.get(id)?.handle.close();
113
+ this.#readHandles.delete(id);
114
+ }
115
+ /** Removes an unpublished physical suffix left by a crash before its WAL publication. */
116
+ truncateTailToPublishedOffset() {
117
+ const size = this.#tailHandle.getSize();
118
+ if (size < this.#tailOffset) {
119
+ throw new Error(`OPFS tail extent ${String(this.#tailId)} is shorter than its published offset: ` +
120
+ `${String(size)} < ${String(this.#tailOffset)}`);
121
+ }
122
+ if (size > this.#tailOffset)
123
+ this.#tailHandle.truncate(this.#tailOffset);
124
+ }
29
125
  /**
30
126
  * Replay hook: a WAL entry recorded this placement after the checkpoint was taken. Advances
31
127
  * the tail position and live-byte accounting to match what the writer had done.
32
128
  */
33
129
  restorePlacement(placement) {
34
- this.#liveBytes.set(placement.extent, (this.#liveBytes.get(placement.extent) ?? 0) + placement.length);
130
+ assertValidPlacement(placement);
131
+ const liveBytes = safeExtentSum(this.#liveBytes.get(placement.extent) ?? 0, placement.length, "Extent live bytes");
132
+ this.#liveBytes.set(placement.extent, liveBytes);
35
133
  if (placement.extent === this.#tailId) {
36
- this.#tailOffset = Math.max(this.#tailOffset, placement.offset + placement.length);
134
+ this.#tailOffset = Math.max(this.#tailOffset, safeExtentSum(placement.offset, placement.length, "Extent tail offset"));
37
135
  }
38
136
  if (placement.extent >= this.#nextId) {
39
137
  // The writer sealed past the checkpointed tail; adopt its numbering.
40
- this.#nextId = placement.extent + 1;
138
+ this.#nextId = safeExtentSuccessor(placement.extent, "Extent ID");
41
139
  }
42
140
  }
43
141
  /** Replay hook: the tail recorded by placements may belong to a newer extent than ours. */
44
142
  async adoptTail(extentId, offset) {
143
+ assertValidExtentCoordinate(extentId, "Extent ID");
144
+ assertValidExtentCoordinate(offset, "Extent tail offset");
145
+ const nextId = safeExtentSuccessor(extentId, "Extent ID");
45
146
  if (extentId === this.#tailId) {
46
147
  this.#tailOffset = Math.max(this.#tailOffset, offset);
47
148
  return;
48
149
  }
49
150
  this.#tailHandle.close();
151
+ // Recovery validation may have opened the newer extent through the read cache before it
152
+ // was known to be the tail. Release that shared-read role before taking the tail lock.
153
+ this.#readHandles.get(extentId)?.handle.close();
154
+ this.#readHandles.delete(extentId);
50
155
  this.#tailHandle = await this.#tree.openHandle(extentPath(extentId), { create: true });
51
156
  this.#tailId = extentId;
52
157
  this.#tailOffset = offset;
53
- this.#nextId = Math.max(this.#nextId, extentId + 1);
158
+ this.#nextId = Math.max(this.#nextId, nextId);
54
159
  }
55
160
  get tailExtentId() {
56
161
  return this.#tailId;
57
162
  }
58
163
  /** Appends synchronously; seals and rolls the tail first when it is full (the async part). */
59
164
  async append(bytes, flush) {
60
- if (this.#tailOffset > 0 && this.#tailOffset + bytes.byteLength > SEAL_BYTES) {
165
+ const initialEnd = safeExtentSum(this.#tailOffset, bytes.byteLength, "Extent tail offset");
166
+ if (this.#tailOffset > 0 && initialEnd > SEAL_BYTES) {
61
167
  await this.#seal();
62
168
  }
169
+ const nextOffset = safeExtentSum(this.#tailOffset, bytes.byteLength, "Extent tail offset");
170
+ const nextLiveBytes = safeExtentSum(this.#liveBytes.get(this.#tailId) ?? 0, bytes.byteLength, "Extent live bytes");
63
171
  const placement = {
64
172
  extent: this.#tailId,
65
173
  offset: this.#tailOffset,
66
174
  length: bytes.byteLength,
175
+ checksum: this.#checksum(bytes),
67
176
  };
68
- this.#tailHandle.write(bytes, { at: placement.offset });
177
+ writeFully(this.#tailHandle, bytes, placement.offset, `appending extent ${String(placement.extent)}`);
178
+ this.#tailDirty = true;
69
179
  if (flush)
70
- this.#tailHandle.flush();
71
- this.#tailOffset += bytes.byteLength;
72
- this.#liveBytes.set(this.#tailId, (this.#liveBytes.get(this.#tailId) ?? 0) + bytes.byteLength);
180
+ this.flush();
181
+ this.#tailOffset = nextOffset;
182
+ this.#rememberLiveBytes(this.#tailId);
183
+ this.#liveBytes.set(this.#tailId, nextLiveBytes);
73
184
  return placement;
74
185
  }
75
186
  async #seal() {
187
+ const nextId = safeExtentSuccessor(this.#nextId, "Extent ID");
188
+ // Relaxed durability batches flushes instead of flushing every append. Sealing is the last
189
+ // chance to make this immutable extent durable before its held handle is released.
190
+ this.flush();
76
191
  this.#tailHandle.close();
77
192
  // The now-sealed extent may still be read; it re-opens on demand through the cache.
78
193
  this.#tailId = this.#nextId;
79
- this.#nextId += 1;
194
+ this.#nextId = nextId;
80
195
  this.#tailOffset = 0;
81
196
  this.#tailHandle = await this.#tree.openHandle(extentPath(this.#tailId), { create: true });
82
197
  }
83
198
  async read(placement) {
84
- const bytes = new Uint8Array(placement.length);
85
- if (placement.extent === this.#tailId) {
86
- this.#tailHandle.read(bytes, { at: placement.offset });
87
- return bytes;
199
+ assertValidPlacement(placement);
200
+ if (placement.extent === this.#tailId)
201
+ return this.#readFromHandle(placement, this.#tailHandle);
202
+ const acquired = this.#acquireReadHandle(placement.extent);
203
+ const handle = isHandlePromise(acquired) ? await acquired : acquired;
204
+ try {
205
+ return this.#readFromHandle(placement, handle);
206
+ }
207
+ finally {
208
+ this.#releaseReadHandle(placement.extent);
209
+ }
210
+ }
211
+ /** Reads and verifies one complete opaque payload with exactly one checksum scan. */
212
+ async readVerified(placement) {
213
+ const bytes = await this.read(placement);
214
+ if (this.#checksum(bytes) !== placement.checksum) {
215
+ throw new Error(`Extent placement checksum mismatch: ${String(placement.extent)}:` +
216
+ `${String(placement.offset)}+${String(placement.length)}`);
88
217
  }
89
- const handle = await this.#readHandle(placement.extent);
90
- handle.read(bytes, { at: placement.offset });
91
218
  return bytes;
92
219
  }
220
+ /** True when the complete placement currently exists; used to bound crash replay. */
221
+ async contains(placement) {
222
+ if (!validPlacement(placement))
223
+ return false;
224
+ try {
225
+ if (placement.extent === this.#tailId) {
226
+ return placement.offset + placement.length <= this.#tailHandle.getSize();
227
+ }
228
+ const acquired = this.#acquireReadHandle(placement.extent);
229
+ const handle = isHandlePromise(acquired) ? await acquired : acquired;
230
+ try {
231
+ return placement.offset + placement.length <= handle.getSize();
232
+ }
233
+ finally {
234
+ this.#releaseReadHandle(placement.extent);
235
+ }
236
+ }
237
+ catch (error) {
238
+ if (error instanceof DOMException && error.name === "NotFoundError")
239
+ return false;
240
+ throw error;
241
+ }
242
+ }
93
243
  /**
94
244
  * Returns the sealed extent with the most dead space once less than half its physical bytes
95
245
  * remain live. Relocating that extent keeps sealed-file bytes below twice live payload bytes;
@@ -100,7 +250,15 @@ export class ExtentPool {
100
250
  for (const [id, liveBytes] of this.#liveBytes) {
101
251
  if (id === this.#tailId || liveBytes === 0)
102
252
  continue;
103
- const size = (await this.#readHandle(id)).getSize();
253
+ const acquired = this.#acquireReadHandle(id);
254
+ const handle = isHandlePromise(acquired) ? await acquired : acquired;
255
+ let size;
256
+ try {
257
+ size = handle.getSize();
258
+ }
259
+ finally {
260
+ this.#releaseReadHandle(id);
261
+ }
104
262
  if (size === 0 || liveBytes * 2 >= size)
105
263
  continue;
106
264
  const wasteRatio = (size - liveBytes) / size;
@@ -110,30 +268,141 @@ export class ExtentPool {
110
268
  }
111
269
  return candidate?.id;
112
270
  }
113
- async #readHandle(extent) {
271
+ /** Actual bytes of every checkpoint-known extent, including the held tail handle. */
272
+ async physicalByteLengths() {
273
+ const sizes = new Map();
274
+ const ids = new Set([...this.#liveBytes.keys(), this.#tailId]);
275
+ for (const id of ids) {
276
+ if (id === this.#tailId) {
277
+ sizes.set(id, this.#tailHandle.getSize());
278
+ continue;
279
+ }
280
+ const acquired = this.#acquireReadHandle(id);
281
+ const handle = isHandlePromise(acquired) ? await acquired : acquired;
282
+ try {
283
+ sizes.set(id, handle.getSize());
284
+ }
285
+ finally {
286
+ this.#releaseReadHandle(id);
287
+ }
288
+ }
289
+ return sizes;
290
+ }
291
+ /** Cached hits return the handle directly: no promise allocation or extra await on reads. */
292
+ #acquireReadHandle(extent) {
293
+ if (this.#closed)
294
+ throw new Error("This extent pool is closed");
295
+ if (this.#deletingExtents.has(extent)) {
296
+ throw new Error(`Extent ${String(extent)} is being deleted`);
297
+ }
114
298
  const cached = this.#readHandles.get(extent);
115
299
  if (cached !== undefined) {
116
300
  // Refresh recency.
117
301
  this.#readHandles.delete(extent);
118
302
  this.#readHandles.set(extent, cached);
119
- return cached;
120
- }
121
- const handle = await this.#tree.openHandle(extentPath(extent), { create: false });
122
- this.#readHandles.set(extent, handle);
123
- if (this.#readHandles.size > READ_HANDLE_CACHE) {
124
- const [oldest] = this.#readHandles.keys();
125
- if (oldest !== undefined) {
126
- this.#readHandles.get(oldest)?.close();
127
- this.#readHandles.delete(oldest);
303
+ cached.readers += 1;
304
+ return cached.handle;
305
+ }
306
+ const opening = this.#openingReadHandles.get(extent);
307
+ if (opening !== undefined) {
308
+ opening.reservations += 1;
309
+ return opening.promise;
310
+ }
311
+ const pending = { reservations: 1 };
312
+ pending.promise = this.#tree.openHandle(extentPath(extent), { create: false }).then((handle) => {
313
+ this.#openingReadHandles.delete(extent);
314
+ // An open already in flight owns reader reservations. A deletion that starts later
315
+ // blocks new acquisitions and awaits this promise plus those readers; rejecting the
316
+ // earlier reservations here would let relocation invalidate a selected placement.
317
+ if (this.#closed) {
318
+ handle.close();
319
+ throw new Error(`Extent ${String(extent)} became unavailable while opening`);
128
320
  }
321
+ this.#readHandles.set(extent, {
322
+ handle,
323
+ readers: pending.reservations,
324
+ drained: [],
325
+ });
326
+ this.#evictReadHandles();
327
+ return handle;
328
+ }, (error) => {
329
+ this.#openingReadHandles.delete(extent);
330
+ throw error;
331
+ });
332
+ this.#openingReadHandles.set(extent, pending);
333
+ return pending.promise;
334
+ }
335
+ #releaseReadHandle(extent) {
336
+ const cached = this.#readHandles.get(extent);
337
+ if (cached === undefined)
338
+ return;
339
+ cached.readers -= 1;
340
+ if (cached.readers < 0)
341
+ throw new Error(`Extent ${String(extent)} read lease underflow`);
342
+ if (cached.readers === 0) {
343
+ for (const resolve of cached.drained.splice(0))
344
+ resolve();
345
+ this.#evictReadHandles();
346
+ }
347
+ }
348
+ #evictReadHandles() {
349
+ while (this.#readHandles.size > READ_HANDLE_CACHE) {
350
+ const idle = [...this.#readHandles].find(([, cached]) => cached.readers === 0);
351
+ if (idle === undefined)
352
+ return;
353
+ const [extent, cached] = idle;
354
+ cached.handle.close();
355
+ this.#readHandles.delete(extent);
129
356
  }
130
- return handle;
357
+ }
358
+ #readFromHandle(placement, handle) {
359
+ const end = placement.offset + placement.length;
360
+ const size = handle.getSize();
361
+ if (end > size) {
362
+ throw new Error(`Extent placement exceeds its file: ${String(placement.extent)}:` +
363
+ `${String(placement.offset)}+${String(placement.length)} > ${String(size)}`);
364
+ }
365
+ const bytes = new Uint8Array(placement.length);
366
+ readFully(handle, bytes, placement.offset, `reading extent ${String(placement.extent)} placement`);
367
+ return bytes;
131
368
  }
132
369
  /** Live-byte accounting for reclaimed payloads; reports extents now safe to delete. */
133
370
  release(placements) {
371
+ const decrements = new Map();
372
+ const ranges = new Map();
134
373
  for (const placement of placements) {
135
- const remaining = (this.#liveBytes.get(placement.extent) ?? 0) - placement.length;
136
- this.#liveBytes.set(placement.extent, Math.max(0, remaining));
374
+ assertValidPlacement(placement);
375
+ decrements.set(placement.extent, (decrements.get(placement.extent) ?? 0) + placement.length);
376
+ if (placement.length > 0) {
377
+ const extentRanges = ranges.get(placement.extent) ?? [];
378
+ extentRanges.push({ start: placement.offset, end: placement.offset + placement.length });
379
+ ranges.set(placement.extent, extentRanges);
380
+ }
381
+ }
382
+ for (const [id, extentRanges] of ranges) {
383
+ extentRanges.sort((left, right) => left.start - right.start || left.end - right.end);
384
+ for (let index = 1; index < extentRanges.length; index += 1) {
385
+ const previous = extentRanges[index - 1];
386
+ const current = extentRanges[index];
387
+ if (previous !== undefined && current !== undefined && current.start < previous.end) {
388
+ throw new Error(`Extent ${String(id)} release contains overlapping or duplicate placements: ` +
389
+ `${String(previous.start)}..${String(previous.end)} and ` +
390
+ `${String(current.start)}..${String(current.end)}`);
391
+ }
392
+ }
393
+ }
394
+ // Validate the entire batch before mutating one counter. Underflow is an invariant
395
+ // violation, never evidence that an extent is empty and safe to delete.
396
+ for (const [id, decrement] of decrements) {
397
+ const live = this.#liveBytes.get(id);
398
+ if (live === undefined || decrement > live) {
399
+ throw new Error(`Extent ${String(id)} live-byte accounting underflow: release ${String(decrement)} ` +
400
+ `from ${live === undefined ? "an unknown extent" : String(live)}`);
401
+ }
402
+ }
403
+ for (const [id, decrement] of decrements) {
404
+ this.#rememberLiveBytes(id);
405
+ this.#liveBytes.set(id, (this.#liveBytes.get(id) ?? 0) - decrement);
137
406
  }
138
407
  const deletable = [];
139
408
  for (const [id, bytes] of this.#liveBytes) {
@@ -144,12 +413,30 @@ export class ExtentPool {
144
413
  }
145
414
  /** Removes a drained extent's file and forgets it. Idempotent. */
146
415
  async deleteExtent(id) {
416
+ if (this.#activeBatch !== undefined) {
417
+ throw new Error("Cannot delete an extent while an unpublished batch is active");
418
+ }
147
419
  if (id === this.#tailId)
148
420
  return;
149
- this.#readHandles.get(id)?.close();
150
- this.#readHandles.delete(id);
151
- this.#liveBytes.delete(id);
152
- await this.#tree.deleteFile(extentPath(id));
421
+ this.#deletingExtents.add(id);
422
+ try {
423
+ const opening = this.#openingReadHandles.get(id);
424
+ if (opening !== undefined)
425
+ await opening.promise.catch(() => undefined);
426
+ const cached = this.#readHandles.get(id);
427
+ if (cached !== undefined && cached.readers > 0) {
428
+ await new Promise((resolve) => cached.drained.push(resolve));
429
+ }
430
+ this.#readHandles.get(id)?.handle.close();
431
+ this.#readHandles.delete(id);
432
+ await this.#tree.deleteFile(extentPath(id));
433
+ // Preserve the zero-live entry when the physical delete is refused. The caller can then
434
+ // retry it and account the cleanup debt instead of silently orphaning an untracked file.
435
+ this.#liveBytes.delete(id);
436
+ }
437
+ finally {
438
+ this.#deletingExtents.delete(id);
439
+ }
153
440
  }
154
441
  meta() {
155
442
  return {
@@ -160,16 +447,92 @@ export class ExtentPool {
160
447
  };
161
448
  }
162
449
  flush() {
450
+ if (!this.#tailDirty)
451
+ return;
163
452
  this.#tailHandle.flush();
453
+ this.#tailDirty = false;
164
454
  }
165
455
  close() {
456
+ this.#closed = true;
166
457
  this.#tailHandle.close();
167
- for (const handle of this.#readHandles.values())
458
+ for (const { handle } of this.#readHandles.values())
168
459
  handle.close();
169
460
  this.#readHandles.clear();
170
461
  }
462
+ #rememberLiveBytes(id) {
463
+ const batch = this.#activeBatch;
464
+ if (batch === undefined || batch._liveBytesBefore.has(id))
465
+ return;
466
+ batch._liveBytesBefore.set(id, this.#liveBytes.get(id));
467
+ }
171
468
  }
172
469
  export function extentPath(id) {
173
470
  return ["extents", String(id).padStart(6, "0")];
174
471
  }
175
- //# sourceMappingURL=extents.js.map
472
+ export function validPlacement(placement) {
473
+ return (Number.isSafeInteger(placement.extent) &&
474
+ placement.extent >= 0 &&
475
+ placement.extent < Number.MAX_SAFE_INTEGER &&
476
+ Number.isSafeInteger(placement.offset) &&
477
+ placement.offset >= 0 &&
478
+ Number.isSafeInteger(placement.length) &&
479
+ placement.length >= 0 &&
480
+ Number.isSafeInteger(placement.checksum) &&
481
+ placement.checksum >= 0 &&
482
+ placement.checksum <= 0xffff_ffff &&
483
+ placement.offset + placement.length <= Number.MAX_SAFE_INTEGER);
484
+ }
485
+ export function assertValidPlacement(placement) {
486
+ if (!validPlacement(placement)) {
487
+ throw new TypeError(`Invalid extent placement: ${String(placement.extent)}:${String(placement.offset)}+` +
488
+ `${String(placement.length)} checksum ${String(placement.checksum)}`);
489
+ }
490
+ }
491
+ export function assertValidExtentMeta(meta) {
492
+ if (!Number.isSafeInteger(meta.nextExtentId) ||
493
+ meta.nextExtentId < 1 ||
494
+ !Number.isSafeInteger(meta.tailExtentId) ||
495
+ meta.tailExtentId < 0 ||
496
+ meta.tailExtentId >= meta.nextExtentId ||
497
+ !Number.isSafeInteger(meta.tailOffset) ||
498
+ meta.tailOffset < 0 ||
499
+ !Array.isArray(meta.liveBytes)) {
500
+ throw new TypeError("Invalid OPFS extent checkpoint metadata");
501
+ }
502
+ const ids = new Set();
503
+ for (const entry of meta.liveBytes) {
504
+ const [id, bytes] = entry;
505
+ if (!Number.isSafeInteger(id) ||
506
+ id < 0 ||
507
+ id >= meta.nextExtentId ||
508
+ ids.has(id) ||
509
+ !Number.isSafeInteger(bytes) ||
510
+ bytes < 0) {
511
+ throw new TypeError(`Invalid OPFS extent live-byte entry: ${String(id)} => ${String(bytes)}`);
512
+ }
513
+ ids.add(id);
514
+ }
515
+ }
516
+ function isHandlePromise(value) {
517
+ return typeof value.then === "function";
518
+ }
519
+ function assertValidExtentCoordinate(value, label) {
520
+ if (!Number.isSafeInteger(value) || value < 0)
521
+ throw new RangeError(`${label} is out of range`);
522
+ }
523
+ function safeExtentSuccessor(value, label) {
524
+ if (!Number.isSafeInteger(value) || value < 0 || value >= Number.MAX_SAFE_INTEGER) {
525
+ throw new RangeError(`${label} cannot exceed the safe integer range`);
526
+ }
527
+ return value + 1;
528
+ }
529
+ function safeExtentSum(left, right, label) {
530
+ if (!Number.isSafeInteger(left) ||
531
+ left < 0 ||
532
+ !Number.isSafeInteger(right) ||
533
+ right < 0 ||
534
+ left > Number.MAX_SAFE_INTEGER - right) {
535
+ throw new RangeError(`${label} cannot exceed the safe integer range`);
536
+ }
537
+ return left + right;
538
+ }
@@ -22,9 +22,8 @@
22
22
  * `FileSystemSyncAccessHandle` satisfies it as-is, and so does anything with positioned
23
23
  * synchronous I/O. See `/docs/storage/custom` for the guide.
24
24
  */
25
- export type { SyncFileHandle } from "./sync-file.js";
25
+ export { readFully, writeFully, type SyncFileHandle } from "./sync-file.js";
26
26
  export { RecordCore, type PhysicalBlocks, type RecordCoreState } from "./record-core.js";
27
- export { WalWriter, replayWalFrames } from "./wal.js";
28
- export { ExtentPool, extentPath, type ExtentFiles, type ExtentMeta, type Placement, } from "./extents.js";
27
+ export { MAX_WAL_FRAME_BYTES, WalWriter, iterateWalFrames, replayWalFrames, type ReplayedWalFrame, } from "./wal.js";
28
+ export { ExtentPool, assertValidExtentMeta, assertValidPlacement, extentPath, validPlacement, type ExtentBatchMark, type ExtentFiles, type ExtentMeta, type ExtentPoolOptions, type Placement, } from "./extents.js";
29
29
  export { LOG_FORMAT_VERSION, decodeChunk, decodePostingChunk, decodeRecordJson, decodeSyncCheckpoint, encodeChunk, encodePostingChunk, encodeRecordJson, encodeSyncCheckpoint, } from "./wire.js";
30
- //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,29 @@
1
+ /**
2
+ * The storage adapter toolkit: optional building blocks for implementing `BlockStore` against
3
+ * a new substrate. Nothing here is part of the storage contract — the contract is the
4
+ * `BlockStore` capability interfaces, their documented rules, and the conformance kit in
5
+ * `@minnowdb/core/testing` — and the engine never references this module. An adapter is free
6
+ * to ignore all of it and store records however its platform does best.
7
+ *
8
+ * What it offers, and which shipped adapter proves each piece:
9
+ *
10
+ * - `RecordCore` — the synchronous record-state machine behind `MemoryBlockStore` and
11
+ * `OpfsBlockStore`: every record family, every validation order, every typed conflict the
12
+ * contract requires, with `dump()`/`load()` for checkpointing. An adapter that owns a
13
+ * single-writer view of the records (a leader, a lone process) can wrap it and spend its
14
+ * own code purely on persistence.
15
+ * - `WalWriter` / `replayWalFrames` — checksummed write-ahead-log frames over any
16
+ * `SyncFileHandle`.
17
+ * - `ExtentPool` — packed append-only files for bulk bytes, addressed by `Placement`.
18
+ * - `encodeRecordJson` and the envelope codecs — JSON with bigint support, checksummed and
19
+ * versioned, for frames, checkpoints, and immutable chunks.
20
+ *
21
+ * `SyncFileHandle` is the only substrate assumption the file-shaped pieces make; the browser's
22
+ * `FileSystemSyncAccessHandle` satisfies it as-is, and so does anything with positioned
23
+ * synchronous I/O. See `/docs/storage/custom` for the guide.
24
+ */
25
+ export { readFully, writeFully } from "./sync-file.js";
1
26
  export { RecordCore } from "./record-core.js";
2
- export { WalWriter, replayWalFrames } from "./wal.js";
3
- export { ExtentPool, extentPath, } from "./extents.js";
27
+ export { MAX_WAL_FRAME_BYTES, WalWriter, iterateWalFrames, replayWalFrames, } from "./wal.js";
28
+ export { ExtentPool, assertValidExtentMeta, assertValidPlacement, extentPath, validPlacement, } from "./extents.js";
4
29
  export { LOG_FORMAT_VERSION, decodeChunk, decodePostingChunk, decodeRecordJson, decodeSyncCheckpoint, encodeChunk, encodePostingChunk, encodeRecordJson, encodeSyncCheckpoint, } from "./wire.js";
5
- //# sourceMappingURL=index.js.map