@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,8 +1,9 @@
1
- import { validateId, validateTempRunPage, validateTempRunPageIdentity, } from "../toolkit/record-core.js";
1
+ import { assertStorageBulkReadItems, assertTempRunPageBatchLimits, OpfsUncertainOutcomeError, StorageCorruptionError, StorageFormatVersionError, validateStorageDatabaseName, } from "../types.js";
2
+ import { validateTempRunPage, validateTempRunPageIdentity } from "../toolkit/record-core.js";
2
3
  import { OpfsTree, encodeSegment, isDomError } from "./files.js";
3
4
  import { LOG_FORMAT_VERSION } from "../toolkit/wire.js";
4
5
  import { OpfsLeader } from "./leader.js";
5
- import { rehydrateStoreError, serializeStoreError, } from "./rpc.js";
6
+ import { rehydrateStoreError, estimateRpcValueBytes, fingerprintStoreRequest, MAX_OPFS_RPC_MESSAGE_BYTES, parseStoreRpcMessage, serializeStoreError, } from "./rpc.js";
6
7
  /** How long a follower waits for a leader's answer before assuming it is gone. */
7
8
  const RPC_TIMEOUT_MS = 1_000;
8
9
  /** How long a ping waits for a leadership announcement. */
@@ -13,59 +14,81 @@ const DISPATCH_ATTEMPTS = 10;
13
14
  const YIELD_COOLDOWN_MS = 3_000;
14
15
  /** Results remembered for retried requests whose acknowledgement was lost. */
15
16
  const DEDUPE_CACHE_SIZE = 512;
17
+ /** Requests admitted concurrently by either side of the follower protocol. */
18
+ const RPC_IN_FLIGHT_LIMIT = 512;
19
+ const RPC_IN_FLIGHT_MUTATION_BYTES = 128 * 1024 * 1024;
20
+ const RPC_IN_FLIGHT_READ_BYTES = 256 * 1024 * 1024;
21
+ const RPC_SETTLED_OUTCOME_BYTES = 128 * 1024 * 1024;
22
+ const RPC_SERVER_ADMISSION_LIMIT = 1024;
23
+ const RPC_SERVER_ADMISSION_BYTES = 256 * 1024 * 1024;
16
24
  /** Channels into requesters' inboxes a leader keeps open between answers. */
17
25
  const ANSWER_CHANNEL_CACHE_SIZE = 64;
26
+ const BULK_READ_ARGUMENT_INDEX = {
27
+ getBlocks: 0,
28
+ getTransactions: 0,
29
+ getExistingUniqueKeys: 1,
30
+ hasManifestBlocks: 1,
31
+ };
18
32
  /** Methods a follower may invoke on the leader. Temp-page IO is instance-local by design. */
19
33
  const RPC_METHODS = new Set([
20
- "addBlock",
21
- "addBlocks",
22
34
  "getBlock",
23
35
  "getBlocks",
24
- "removeBlock",
25
- "listBlockIds",
36
+ "readManifestBlock",
37
+ "hasManifestBlocks",
38
+ "listManifestBlockPage",
39
+ "listRetiredManifestBlockPage",
26
40
  "addTable",
27
41
  "getTable",
28
42
  "getTableByName",
29
43
  "listTables",
30
44
  "updateTable",
31
45
  "removeTable",
46
+ "dropTable",
47
+ "dropTableColumn",
32
48
  "removeFtsColumn",
33
49
  "writeFtsBase",
34
50
  "beginFtsBaseBuild",
51
+ "renewFtsBaseBuild",
35
52
  "writeFtsBaseBuildChunk",
36
53
  "finishFtsBaseBuild",
37
54
  "abortFtsBaseBuild",
38
55
  "readFtsCandidates",
39
56
  "readFtsPostings",
40
- "addSegment",
41
57
  "getSegment",
42
- "listSegments",
43
58
  "listSegmentPage",
44
- "removeSegment",
59
+ "listTableSegmentPage",
60
+ "removeAbortedSegment",
61
+ "adoptAbortedSegment",
45
62
  "reserveRowIds",
46
63
  "reserveAutoIncrement",
47
64
  "getExistingUniqueKeys",
65
+ "beginUniqueKeyBuild",
66
+ "getUniqueKeyBuild",
67
+ "renewUniqueKeyBuild",
68
+ "appendUniqueKeyBuildChunk",
69
+ "finishUniqueKeyBuild",
70
+ "abortUniqueKeyBuild",
48
71
  "getCurrentManifest",
49
72
  "getCurrentManifestVersion",
50
73
  "getCatalogProbe",
51
- "getQueryCatalogState",
52
74
  "getManifest",
53
- "listManifests",
54
75
  "listManifestPage",
55
- "publishManifest",
56
76
  "createTransaction",
77
+ "renewTransaction",
78
+ "abortTransactionIfExpired",
57
79
  "beginTransaction",
58
80
  "getTransaction",
59
81
  "getTransactions",
60
- "listTransactions",
61
82
  "listTransactionPage",
62
83
  "updateTransaction",
63
84
  "stageTransactionArtifacts",
85
+ "rollbackTransactionArtifacts",
64
86
  "commitTransaction",
65
87
  "writeTransaction",
66
88
  "createLease",
67
89
  "getLease",
68
90
  "listLeases",
91
+ "listExpiredLeasePage",
69
92
  "renewLease",
70
93
  "moveLease",
71
94
  "removeLeaseIfExpired",
@@ -78,20 +101,35 @@ const RPC_METHODS = new Set([
78
101
  "cancelCompactionJob",
79
102
  "removeCompactionJob",
80
103
  "createGarbageCollectionJob",
104
+ "updateGarbageCollectionPlanning",
81
105
  "getGarbageCollectionJob",
82
106
  "listGarbageCollectionJobs",
107
+ "listGarbageCollectionJobPage",
83
108
  "runGarbageCollectionStep",
84
109
  "removePrunedManifestRecords",
85
110
  "removeGarbageCollectionJob",
86
111
  "createTempOwner",
87
112
  "getTempOwner",
113
+ "putTempRunPage",
114
+ "putTempRunPages",
115
+ "getTempRunPage",
116
+ "removeTempRun",
88
117
  "renewTempOwner",
89
118
  "removeTempOwnerIfExpired",
90
119
  "removeTempOwner",
91
120
  "listTempOwnerIdsPage",
121
+ "listExpiredTempOwnerPage",
92
122
  "getLogicalStorageBytes",
93
- "exportSnapshot",
94
- "importSnapshot",
123
+ "getStorageStats",
124
+ "checkIntegrity",
125
+ "beginSnapshotFrameExport",
126
+ "readSnapshotExportFrame",
127
+ "closeSnapshotFrameExport",
128
+ "beginSnapshotFrameImport",
129
+ "renewSnapshotFrameImport",
130
+ "appendSnapshotImportFrames",
131
+ "finishSnapshotFrameImport",
132
+ "cancelSnapshotFrameImport",
95
133
  ]);
96
134
  /**
97
135
  * Reads are idempotent — a lost-acknowledgement retry can simply run again — so only
@@ -101,70 +139,56 @@ const RPC_METHODS = new Set([
101
139
  const READ_METHODS = new Set([
102
140
  "getBlock",
103
141
  "getBlocks",
104
- "listBlockIds",
142
+ "readManifestBlock",
143
+ "hasManifestBlocks",
144
+ "listManifestBlockPage",
145
+ "listRetiredManifestBlockPage",
105
146
  "getTable",
106
147
  "getTableByName",
107
148
  "listTables",
108
149
  "readFtsCandidates",
109
150
  "readFtsPostings",
110
151
  "getSegment",
111
- "listSegments",
112
152
  "listSegmentPage",
153
+ "listTableSegmentPage",
113
154
  "getExistingUniqueKeys",
155
+ "getUniqueKeyBuild",
114
156
  "getCurrentManifest",
115
157
  "getCurrentManifestVersion",
116
158
  "getCatalogProbe",
117
- "getQueryCatalogState",
118
159
  "getManifest",
119
- "listManifests",
120
160
  "listManifestPage",
121
161
  "getTransaction",
122
162
  "getTransactions",
123
- "listTransactions",
124
163
  "listTransactionPage",
125
164
  "getLease",
126
165
  "listLeases",
166
+ "listExpiredLeasePage",
127
167
  "getCompactionJob",
128
168
  "listCompactionJobs",
129
169
  "listCompactionJobPage",
130
170
  "getGarbageCollectionJob",
131
171
  "listGarbageCollectionJobs",
172
+ "listGarbageCollectionJobPage",
132
173
  "getTempOwner",
174
+ "getTempRunPage",
133
175
  "listTempOwnerIdsPage",
176
+ "listExpiredTempOwnerPage",
134
177
  "getLogicalStorageBytes",
135
- "exportSnapshot",
178
+ "getStorageStats",
179
+ "checkIntegrity",
180
+ "readSnapshotExportFrame",
136
181
  ]);
137
- /**
138
- * The OPFS block store: one leader per database holds every file handle and does all storage
139
- * work at held-handle speed; other connections are thin followers whose operations travel a
140
- * `BroadcastChannel` to it.
141
- *
142
- * Two kinds of channel carry the protocol. One shared channel per database carries what every
143
- * connection must hear: leader announcements, pings, bids, yields, releases. Every connection
144
- * also owns an inbox — a channel named for its instance id — where the messages meant for it
145
- * alone arrive: operations at the leader, results and busy notices at the requester. A block
146
- * read's bytes are therefore structured-cloned once, into the tab that asked, rather than into
147
- * every tab of the origin.
148
- *
149
- * Leadership is the write-ahead log's own exclusive sync-access handle — enforced by the
150
- * browser against the actual resource, released the instant its holder dies. Elections are
151
- * simply attempts to open it. Correctness never rides on a message: an operation is
152
- * acknowledged only after the leader's WAL holds it, a lost message costs a retry (request
153
- * ids deduplicate retries while the same leader serves), and a dead leader costs a failover
154
- * in which the next acquirer replays checkpoint-plus-WAL. A retry that crosses a failover is
155
- * the one case a duplicate can execute — the same "committed but the response was lost"
156
- * class the IndexedDB store has — and it resolves the same way: compare-and-swap operations
157
- * surface a conflict the engine's recovery already reconciles. The channel affects how fast
158
- * multi-tab work moves, never whether it is right.
159
- *
160
- * `setForeground(true)` marks this connection as the one the user is looking at; a background
161
- * leader yields to a foreground bidder, so the tab doing the work is normally the tab holding
162
- * the microsecond-fast path.
163
- */
182
+ function sameServedRequest(remembered, message, signature) {
183
+ return remembered.method === message.method && remembered.signature === signature;
184
+ }
185
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
164
186
  export class OpfsBlockStore {
187
+ liveQueryChannelName;
165
188
  #tree;
166
189
  #durability;
167
190
  #checkpointEntries;
191
+ #cleanupLimitBytes;
168
192
  #rpcTimeoutMs;
169
193
  #instanceId = crypto.randomUUID();
170
194
  #channelName;
@@ -181,14 +205,26 @@ export class OpfsBlockStore {
181
205
  #lastYieldAt = 0;
182
206
  #electing;
183
207
  #pending = new Map();
184
- #dedupe = new Map();
208
+ #inFlightMutations = new Map();
209
+ #settledMutations = new Map();
210
+ #servedRequestLocks = new Map();
211
+ #inFlightMutationBytes = 0;
212
+ #inFlightReadBytes = 0;
213
+ #settledMutationBytes = 0;
214
+ #pendingRpcBytes = 0;
215
+ #servedRequestCount = 0;
216
+ #servedRequestBytes = 0;
217
+ #servedMutationGateForTests;
218
+ #dropNextRpcResultForTests = false;
185
219
  #reacquireTimer;
186
220
  constructor(tree, options) {
187
221
  this.#tree = tree;
188
- this.#durability = options.durability ?? "relaxed";
222
+ this.#durability = options.durability ?? "strict";
189
223
  this.#checkpointEntries = options.checkpointEntries;
224
+ this.#cleanupLimitBytes = options.cleanupLimitBytes;
190
225
  this.#rpcTimeoutMs = options.rpcTimeoutMs ?? RPC_TIMEOUT_MS;
191
226
  this.#channelName = `minnowdb-store:${options.name}`;
227
+ this.liveQueryChannelName = `minnowdb-live:opfs:${options.name}`;
192
228
  }
193
229
  static async open(options) {
194
230
  const tree = new OpfsTree(await resolveDatabaseRoot(options));
@@ -196,7 +232,9 @@ export class OpfsBlockStore {
196
232
  await store.#ensureFormatMarker();
197
233
  if (typeof BroadcastChannel === "function") {
198
234
  const onMessage = (event) => {
199
- store.#onMessage(event.data);
235
+ const message = parseStoreRpcMessage(event.data, RPC_METHODS);
236
+ if (message !== undefined)
237
+ store.#onMessage(message);
200
238
  };
201
239
  const channel = new BroadcastChannel(store.#channelName);
202
240
  channel.onmessage = onMessage;
@@ -244,7 +282,7 @@ export class OpfsBlockStore {
244
282
  // them in sequence; the brief retry covers the gap.
245
283
  slotA = await this.#openWithRetry(["checkpoint-a"]);
246
284
  slotB = await this.#openWithRetry(["checkpoint-b"]);
247
- this.#leader = await OpfsLeader.recover(this.#tree, this.#durability === "strict", { wal, slotA, slotB }, this.#checkpointEntries);
285
+ this.#leader = await OpfsLeader.recover(this.#tree, this.#durability === "strict", { wal, slotA, slotB }, this.#checkpointEntries, this.#cleanupLimitBytes);
248
286
  }
249
287
  catch (error) {
250
288
  wal.close();
@@ -320,15 +358,31 @@ export class OpfsBlockStore {
320
358
  return;
321
359
  switch (message.kind) {
322
360
  case "op": {
323
- if (this.#leader !== undefined)
324
- void this.#serveOp(message);
361
+ if (this.#leader !== undefined) {
362
+ const requestBytes = estimateRpcValueBytes(message.args);
363
+ if (this.#servedRequestCount >= RPC_SERVER_ADMISSION_LIMIT ||
364
+ this.#servedRequestBytes + requestBytes > RPC_SERVER_ADMISSION_BYTES) {
365
+ this.#answer(message.from, {
366
+ kind: "result",
367
+ requestId: message.requestId,
368
+ ok: false,
369
+ error: { name: "Error", message: "The OPFS leader RPC queue is full" },
370
+ });
371
+ return;
372
+ }
373
+ this.#servedRequestCount += 1;
374
+ this.#servedRequestBytes += requestBytes;
375
+ void this.#serveOp(message).finally(() => {
376
+ this.#servedRequestCount = Math.max(0, this.#servedRequestCount - 1);
377
+ this.#servedRequestBytes = Math.max(0, this.#servedRequestBytes - requestBytes);
378
+ });
379
+ }
325
380
  return;
326
381
  }
327
382
  case "result": {
328
- const pending = this.#pending.get(message.requestId);
383
+ const pending = this.#takePending(message.requestId);
329
384
  if (pending === undefined)
330
385
  return;
331
- this.#pending.delete(message.requestId);
332
386
  clearTimeout(pending.timer);
333
387
  if (message.ok)
334
388
  pending.resolve(message.value);
@@ -342,8 +396,8 @@ export class OpfsBlockStore {
342
396
  return;
343
397
  clearTimeout(pending.timer);
344
398
  pending.timer = setTimeout(() => {
345
- this.#pending.delete(message.requestId);
346
- pending.reject(RPC_TIMED_OUT);
399
+ const expired = this.#takePending(message.requestId);
400
+ expired?.reject(RPC_TIMED_OUT);
347
401
  }, this.#rpcTimeoutMs);
348
402
  return;
349
403
  }
@@ -353,12 +407,17 @@ export class OpfsBlockStore {
353
407
  clearTimeout(this.#reacquireTimer);
354
408
  this.#reacquireTimer = undefined;
355
409
  }
356
- // A new leader never saw our in-flight requests; send those again (ids deduplicate).
357
- // Requests already sitting with this same leader stay put — every ping triggers an
358
- // announce, and blind re-sends would race their own in-flight executions.
359
- for (const pending of this.#pending.values()) {
410
+ // Reads can move to the new leader. A mutation may already be durable on the old one;
411
+ // never execute it twice merely because its acknowledgement was lost.
412
+ for (const [requestId, pending] of this.#pending) {
360
413
  if (pending.sentTo === message.leaderId)
361
414
  continue;
415
+ if (!READ_METHODS.has(pending.message.method)) {
416
+ this.#takePending(requestId);
417
+ clearTimeout(pending.timer);
418
+ pending.reject(new OpfsUncertainOutcomeError(pending.message.method));
419
+ continue;
420
+ }
362
421
  pending.sentTo = message.leaderId;
363
422
  this.#send(message.leaderId, pending.message);
364
423
  }
@@ -396,31 +455,133 @@ export class OpfsBlockStore {
396
455
  }
397
456
  }
398
457
  }
458
+ #takePending(requestId) {
459
+ const pending = this.#pending.get(requestId);
460
+ if (pending === undefined)
461
+ return undefined;
462
+ this.#pending.delete(requestId);
463
+ this.#pendingRpcBytes -= pending.retainedBytes;
464
+ return pending;
465
+ }
399
466
  async #serveOp(message) {
467
+ const requestKey = `${String(message.from.length)}:${message.from}${message.requestId}`;
468
+ // Once an identity is admitted, duplicates can compare their fingerprint immediately and
469
+ // either attach to the exact execution or fail closed. The short lock below exists only for
470
+ // the pre-admission fingerprint race between two first deliveries.
471
+ if (this.#inFlightMutations.has(requestKey) || this.#settledMutations.has(requestKey)) {
472
+ await this.#serveOpLocked(message, requestKey);
473
+ return;
474
+ }
475
+ const previous = this.#servedRequestLocks.get(requestKey) ?? Promise.resolve();
476
+ let release;
477
+ const hold = new Promise((resolve) => {
478
+ release = resolve;
479
+ });
480
+ const tail = previous.then(() => hold);
481
+ this.#servedRequestLocks.set(requestKey, tail);
482
+ await previous;
483
+ try {
484
+ await this.#serveOpLocked(message, requestKey);
485
+ }
486
+ finally {
487
+ release();
488
+ if (this.#servedRequestLocks.get(requestKey) === tail) {
489
+ this.#servedRequestLocks.delete(requestKey);
490
+ }
491
+ }
492
+ }
493
+ async #serveOpLocked(message, requestKey) {
400
494
  const leader = this.#leader;
401
495
  if (leader === undefined || !RPC_METHODS.has(message.method))
402
496
  return;
403
- let outcome = this.#dedupe.get(message.requestId);
404
- if (outcome === undefined) {
405
- outcome = this.#executeServedOp(leader, message);
406
- // Only mutations need at-most-once; reads simply run again on a retried request, and
407
- // caching their results (block bytes included) would be pure memory waste.
408
- if (!READ_METHODS.has(message.method)) {
409
- this.#dedupe.set(message.requestId, outcome);
410
- if (this.#dedupe.size > DEDUPE_CACHE_SIZE) {
411
- const [oldest] = this.#dedupe.keys();
412
- if (oldest !== undefined)
413
- this.#dedupe.delete(oldest);
414
- }
497
+ const isRead = READ_METHODS.has(message.method);
498
+ let fingerprint;
499
+ try {
500
+ fingerprint = await fingerprintStoreRequest(message.method, message.args);
501
+ }
502
+ catch {
503
+ return;
504
+ }
505
+ const remembered = isRead ? undefined : this.#settledMutations.get(requestKey);
506
+ let settled;
507
+ if (remembered !== undefined) {
508
+ if (!sameServedRequest(remembered, message, fingerprint.signature)) {
509
+ this.#rejectReusedRequestIdentity(message);
510
+ return;
415
511
  }
512
+ // Refresh bounded settled outcomes as an LRU. This is the lost-ack retry path.
513
+ this.#settledMutations.delete(requestKey);
514
+ this.#settledMutations.set(requestKey, remembered);
515
+ settled = remembered.outcome;
416
516
  }
417
- else {
418
- // A duplicate of a request that is still running (or already remembered): attach to it
419
- // rather than executing twice, and reset the caller's patience meanwhile — the original
420
- // may legitimately take longer than one timeout (a snapshot import, a checkpoint pause).
517
+ else if (!isRead && this.#inFlightMutations.has(requestKey)) {
518
+ const inFlight = this.#inFlightMutations.get(requestKey);
519
+ if (inFlight === undefined)
520
+ throw new Error("In-flight RPC identity disappeared");
521
+ if (!sameServedRequest(inFlight, message, fingerprint.signature)) {
522
+ this.#rejectReusedRequestIdentity(message);
523
+ return;
524
+ }
525
+ // Never evict an in-flight mutation. A duplicate attaches to the one execution and resets
526
+ // the requester's patience while it remains queued or running.
421
527
  this.#answer(message.from, { kind: "busy", requestId: message.requestId });
528
+ settled = await inFlight.outcome;
529
+ }
530
+ else {
531
+ if (!isRead &&
532
+ (this.#inFlightMutations.size >= RPC_IN_FLIGHT_LIMIT ||
533
+ this.#inFlightMutationBytes + fingerprint.retainedBytes > RPC_IN_FLIGHT_MUTATION_BYTES)) {
534
+ this.#answer(message.from, {
535
+ kind: "result",
536
+ requestId: message.requestId,
537
+ ok: false,
538
+ error: { name: "Error", message: "The OPFS leader mutation queue is full" },
539
+ });
540
+ return;
541
+ }
542
+ if (isRead) {
543
+ const reservation = Math.max(fingerprint.retainedBytes, MAX_OPFS_RPC_MESSAGE_BYTES);
544
+ if (this.#inFlightReadBytes + reservation > RPC_IN_FLIGHT_READ_BYTES) {
545
+ this.#answer(message.from, {
546
+ kind: "result",
547
+ requestId: message.requestId,
548
+ ok: false,
549
+ error: { name: "Error", message: "The OPFS leader read queue is full" },
550
+ });
551
+ return;
552
+ }
553
+ this.#inFlightReadBytes += reservation;
554
+ try {
555
+ settled = await this.#executeServedOpAfterGate(leader, message, true);
556
+ }
557
+ finally {
558
+ this.#inFlightReadBytes = Math.max(0, this.#inFlightReadBytes - reservation);
559
+ }
560
+ }
561
+ else {
562
+ this.#inFlightMutationBytes += fingerprint.retainedBytes;
563
+ const execution = this.#executeServedOpAfterGate(leader, message, false);
564
+ const outcome = execution.then((result) => {
565
+ this.#inFlightMutations.delete(requestKey);
566
+ this.#inFlightMutationBytes = Math.max(0, this.#inFlightMutationBytes - fingerprint.retainedBytes);
567
+ if (!this.#closed) {
568
+ this.#rememberSettledMutation(requestKey, message.method, fingerprint, result);
569
+ }
570
+ return result;
571
+ });
572
+ this.#inFlightMutations.set(requestKey, {
573
+ method: message.method,
574
+ signature: fingerprint.signature,
575
+ requestBytes: fingerprint.retainedBytes,
576
+ outcome,
577
+ });
578
+ settled = await outcome;
579
+ }
580
+ }
581
+ if (this.#dropNextRpcResultForTests) {
582
+ this.#dropNextRpcResultForTests = false;
583
+ return;
422
584
  }
423
- const settled = await outcome;
424
585
  this.#answer(message.from, settled.ok
425
586
  ? { kind: "result", requestId: message.requestId, ok: true, value: settled.value }
426
587
  : {
@@ -430,6 +591,45 @@ export class OpfsBlockStore {
430
591
  error: settled.error ?? { name: "Error", message: "unknown" },
431
592
  });
432
593
  }
594
+ #rememberSettledMutation(requestKey, method, fingerprint, outcome) {
595
+ const outcomeBytes = estimateRpcValueBytes(outcome);
596
+ while (this.#settledMutations.size >= DEDUPE_CACHE_SIZE ||
597
+ this.#settledMutationBytes + outcomeBytes > RPC_SETTLED_OUTCOME_BYTES) {
598
+ const oldest = this.#settledMutations.keys().next().value;
599
+ if (oldest === undefined)
600
+ break;
601
+ const removed = this.#settledMutations.get(oldest);
602
+ this.#settledMutations.delete(oldest);
603
+ this.#settledMutationBytes -= removed?.outcomeBytes ?? 0;
604
+ }
605
+ if (outcomeBytes > RPC_SETTLED_OUTCOME_BYTES)
606
+ return;
607
+ this.#settledMutations.set(requestKey, {
608
+ method,
609
+ signature: fingerprint.signature,
610
+ requestBytes: fingerprint.retainedBytes,
611
+ outcome,
612
+ outcomeBytes,
613
+ });
614
+ this.#settledMutationBytes += outcomeBytes;
615
+ }
616
+ #rejectReusedRequestIdentity(message) {
617
+ this.#answer(message.from, {
618
+ kind: "result",
619
+ requestId: message.requestId,
620
+ ok: false,
621
+ error: {
622
+ name: "Error",
623
+ message: "The OPFS RPC request identity was reused with different contents",
624
+ },
625
+ });
626
+ }
627
+ async #executeServedOpAfterGate(leader, message, isRead) {
628
+ const gate = this.#servedMutationGateForTests;
629
+ if (!isRead && gate !== undefined)
630
+ await gate;
631
+ return this.#executeServedOp(leader, message);
632
+ }
433
633
  async #executeServedOp(leader, message) {
434
634
  try {
435
635
  const method = leader[message.method];
@@ -551,6 +751,8 @@ export class OpfsBlockStore {
551
751
  catch (error) {
552
752
  if (error === RPC_TIMED_OUT) {
553
753
  this.#knownLeader = undefined;
754
+ if (!READ_METHODS.has(method))
755
+ throw new OpfsUncertainOutcomeError(method);
554
756
  continue;
555
757
  }
556
758
  throw error;
@@ -563,14 +765,33 @@ export class OpfsBlockStore {
563
765
  return this.#knownLeader !== undefined;
564
766
  }
565
767
  #rpc(requestId, method, args) {
768
+ let retainedBytes;
769
+ try {
770
+ retainedBytes = estimateRpcValueBytes(args);
771
+ }
772
+ catch (error) {
773
+ return Promise.reject(error instanceof Error ? error : new Error(String(error)));
774
+ }
775
+ if (this.#pending.size >= RPC_IN_FLIGHT_LIMIT ||
776
+ this.#pendingRpcBytes + retainedBytes > RPC_IN_FLIGHT_MUTATION_BYTES) {
777
+ return Promise.reject(new Error("The OPFS follower request queue is full"));
778
+ }
566
779
  return new Promise((resolve, reject) => {
567
780
  const message = { kind: "op", requestId, from: this.#instanceId, method, args };
568
781
  const timer = setTimeout(() => {
569
- this.#pending.delete(requestId);
570
- reject(RPC_TIMED_OUT);
782
+ const expired = this.#takePending(requestId);
783
+ expired?.reject(RPC_TIMED_OUT);
571
784
  }, this.#rpcTimeoutMs);
572
785
  const leaderId = this.#knownLeader;
573
- this.#pending.set(requestId, { message, sentTo: leaderId, resolve, reject, timer });
786
+ this.#pending.set(requestId, {
787
+ message,
788
+ retainedBytes,
789
+ sentTo: leaderId,
790
+ resolve,
791
+ reject,
792
+ timer,
793
+ });
794
+ this.#pendingRpcBytes += retainedBytes;
574
795
  // The dispatch loop only gets here with a leader known; should it have slipped away in
575
796
  // between, the timeout (or the next leader's announcement, which re-sends) takes over.
576
797
  if (leaderId !== undefined)
@@ -580,307 +801,35 @@ export class OpfsBlockStore {
580
801
  // ---------------------------------------------------------------------------------------
581
802
  // The BlockStore surface.
582
803
  // ---------------------------------------------------------------------------------------
583
- async addBlock(id, bytes) {
584
- await this.#dispatch("addBlock", [id, bytes]);
585
- }
586
- async addBlocks(blocks) {
587
- await this.#dispatch("addBlocks", [blocks]);
588
- }
589
- async getBlock(id) {
590
- return (await this.#dispatch("getBlock", [id]));
591
- }
592
- async getBlocks(ids) {
593
- return (await this.#dispatch("getBlocks", [ids]));
594
- }
595
- async removeBlock(id) {
596
- await this.#dispatch("removeBlock", [id]);
597
- }
598
- async listBlockIds() {
599
- return (await this.#dispatch("listBlockIds", []));
600
- }
601
804
  async putTempRunPage(page) {
602
805
  validateTempRunPage(page);
603
- await this.#tree.writeFile(["temp", encodeSegment(page.ownerId), encodeSegment(page.runId), String(page.pageIndex)], page.bytes);
806
+ await this.#dispatch("putTempRunPage", [page]);
604
807
  }
605
808
  async putTempRunPages(pages) {
606
809
  // Spill pages are instance-local files; the batch is a convenience, not a round-trip win.
810
+ assertTempRunPageBatchLimits(pages);
607
811
  for (const page of pages)
608
- await this.putTempRunPage(page);
812
+ validateTempRunPage(page);
813
+ await this.#dispatch("putTempRunPages", [pages]);
609
814
  }
610
815
  async getTempRunPage(ownerId, runId, pageIndex) {
611
816
  validateTempRunPageIdentity(ownerId, runId, pageIndex);
612
- return this.#tree.readFile([
613
- "temp",
614
- encodeSegment(ownerId),
615
- encodeSegment(runId),
616
- String(pageIndex),
617
- ]);
817
+ return (await this.#dispatch("getTempRunPage", [ownerId, runId, pageIndex]));
618
818
  }
619
819
  async removeTempRun(ownerId, runId) {
620
820
  validateTempRunPageIdentity(ownerId, runId, 0);
621
- await this.#tree.deleteTree(["temp", encodeSegment(ownerId), encodeSegment(runId)]);
622
- }
623
- async removeTempOwner(ownerId) {
624
- validateId(ownerId);
625
- await this.#dispatch("removeTempOwner", [ownerId]);
626
- await this.#tree.deleteTree(["temp", encodeSegment(ownerId)]);
627
- }
628
- async createTempOwner(record) {
629
- await this.#dispatch("createTempOwner", [record]);
630
- }
631
- async getTempOwner(ownerId) {
632
- return (await this.#dispatch("getTempOwner", [ownerId]));
633
- }
634
- async renewTempOwner(ownerId, expectedRevision, expiresAt) {
635
- return (await this.#dispatch("renewTempOwner", [
636
- ownerId,
637
- expectedRevision,
638
- expiresAt,
639
- ]));
640
- }
641
- async removeTempOwnerIfExpired(ownerId, expiresAtCutoff) {
642
- const removed = (await this.#dispatch("removeTempOwnerIfExpired", [
643
- ownerId,
644
- expiresAtCutoff,
645
- ]));
646
- if (removed)
647
- await this.#tree.deleteTree(["temp", encodeSegment(ownerId)]);
648
- return removed;
649
- }
650
- async listTempOwnerIdsPage(afterOwnerId, limit) {
651
- return (await this.#dispatch("listTempOwnerIdsPage", [afterOwnerId, limit]));
652
- }
653
- async addTable(record) {
654
- await this.#dispatch("addTable", [record]);
655
- }
656
- async getTable(id) {
657
- return (await this.#dispatch("getTable", [id]));
658
- }
659
- async getTableByName(name) {
660
- return (await this.#dispatch("getTableByName", [name]));
661
- }
662
- async listTables() {
663
- return (await this.#dispatch("listTables", []));
664
- }
665
- async updateTable(id, expectedRevision, update) {
666
- return (await this.#dispatch("updateTable", [id, expectedRevision, update]));
667
- }
668
- async removeTable(id, expectedRevision) {
669
- await this.#dispatch("removeTable", [id, expectedRevision]);
670
- }
671
- async writeFtsBase(tableId, columnId, input) {
672
- await this.#dispatch("writeFtsBase", [tableId, columnId, input]);
673
- }
674
- async removeFtsColumn(tableId, columnId) {
675
- await this.#dispatch("removeFtsColumn", [tableId, columnId]);
676
- }
677
- async beginFtsBaseBuild(tableId, columnId, buildId) {
678
- await this.#dispatch("beginFtsBaseBuild", [tableId, columnId, buildId]);
679
- }
680
- async writeFtsBaseBuildChunk(tableId, columnId, buildId, ordinal, chunk) {
681
- await this.#dispatch("writeFtsBaseBuildChunk", [tableId, columnId, buildId, ordinal, chunk]);
682
- }
683
- async finishFtsBaseBuild(tableId, columnId, buildId, input) {
684
- await this.#dispatch("finishFtsBaseBuild", [tableId, columnId, buildId, input]);
685
- }
686
- async abortFtsBaseBuild(tableId, columnId, buildId) {
687
- await this.#dispatch("abortFtsBaseBuild", [tableId, columnId, buildId]);
688
- }
689
- async readFtsCandidates(tableId, columnId, terms, upToVersion) {
690
- return (await this.#dispatch("readFtsCandidates", [
691
- tableId,
692
- columnId,
693
- terms,
694
- upToVersion,
695
- ]));
696
- }
697
- async readFtsPostings(tableId, columnId, upToVersion) {
698
- return this.#dispatch("readFtsPostings", [tableId, columnId, upToVersion]);
699
- }
700
- async addSegment(record) {
701
- await this.#dispatch("addSegment", [record]);
702
- }
703
- async getSegment(id) {
704
- return (await this.#dispatch("getSegment", [id]));
705
- }
706
- async listSegments(tableId) {
707
- return (await this.#dispatch("listSegments", [tableId]));
708
- }
709
- async listSegmentPage(afterId, limit) {
710
- return (await this.#dispatch("listSegmentPage", [afterId, limit]));
711
- }
712
- async removeSegment(id) {
713
- await this.#dispatch("removeSegment", [id]);
714
- }
715
- async reserveRowIds(tableId, count) {
716
- return (await this.#dispatch("reserveRowIds", [tableId, count]));
717
- }
718
- async reserveAutoIncrement(tableId, columnId, count, atLeast) {
719
- return (await this.#dispatch("reserveAutoIncrement", [
720
- tableId,
721
- columnId,
722
- count,
723
- atLeast,
724
- ]));
725
- }
726
- async getExistingUniqueKeys(tableId, keyTokens) {
727
- return (await this.#dispatch("getExistingUniqueKeys", [tableId, keyTokens]));
728
- }
729
- async getCurrentManifest() {
730
- return (await this.#dispatch("getCurrentManifest", []));
731
- }
732
- async getCurrentManifestVersion() {
733
- return (await this.#dispatch("getCurrentManifestVersion", []));
734
- }
735
- async getCatalogProbe() {
736
- return (await this.#dispatch("getCatalogProbe", []));
737
- }
738
- async getQueryCatalogState(tableNames) {
739
- return (await this.#dispatch("getQueryCatalogState", [tableNames]));
740
- }
741
- async getManifest(version) {
742
- return (await this.#dispatch("getManifest", [version]));
743
- }
744
- async listManifests() {
745
- return (await this.#dispatch("listManifests", []));
746
- }
747
- async listManifestPage(afterVersion, limit) {
748
- return (await this.#dispatch("listManifestPage", [afterVersion, limit]));
749
- }
750
- async publishManifest(input) {
751
- return (await this.#dispatch("publishManifest", [input]));
752
- }
753
- async createTransaction(record) {
754
- await this.#dispatch("createTransaction", [record]);
755
- }
756
- async beginTransaction(input) {
757
- return (await this.#dispatch("beginTransaction", [input]));
758
- }
759
- async getTransaction(id) {
760
- return (await this.#dispatch("getTransaction", [id]));
761
- }
762
- async getTransactions(ids) {
763
- return (await this.#dispatch("getTransactions", [ids]));
764
- }
765
- async listTransactions() {
766
- return (await this.#dispatch("listTransactions", []));
767
- }
768
- async listTransactionPage(afterId, limit) {
769
- return (await this.#dispatch("listTransactionPage", [afterId, limit]));
770
- }
771
- async updateTransaction(id, expectedRevision, update) {
772
- return (await this.#dispatch("updateTransaction", [
773
- id,
774
- expectedRevision,
775
- update,
776
- ]));
777
- }
778
- async stageTransactionArtifacts(input) {
779
- return (await this.#dispatch("stageTransactionArtifacts", [input]));
780
- }
781
- async commitTransaction(input) {
782
- return (await this.#dispatch("commitTransaction", [input]));
783
- }
784
- async writeTransaction(input) {
785
- return (await this.#dispatch("writeTransaction", [input]));
786
- }
787
- async createLease(record) {
788
- await this.#dispatch("createLease", [record]);
789
- }
790
- async getLease(id) {
791
- return (await this.#dispatch("getLease", [id]));
792
- }
793
- async listLeases() {
794
- return (await this.#dispatch("listLeases", []));
795
- }
796
- async renewLease(id, expectedRevision, expiresAt) {
797
- return (await this.#dispatch("renewLease", [id, expectedRevision, expiresAt]));
798
- }
799
- async moveLease(id, expectedRevision, manifestVersion, expiresAt) {
800
- return (await this.#dispatch("moveLease", [
801
- id,
802
- expectedRevision,
803
- manifestVersion,
804
- expiresAt,
805
- ]));
806
- }
807
- async removeLeaseIfExpired(id, expectedRevision, expiresAtCutoff) {
808
- return (await this.#dispatch("removeLeaseIfExpired", [
809
- id,
810
- expectedRevision,
811
- expiresAtCutoff,
812
- ]));
813
- }
814
- async removeLease(id) {
815
- await this.#dispatch("removeLease", [id]);
816
- }
817
- async createCompactionJob(record) {
818
- await this.#dispatch("createCompactionJob", [record]);
819
- }
820
- async getCompactionJob(id) {
821
- return (await this.#dispatch("getCompactionJob", [id]));
822
- }
823
- async listCompactionJobs(tableId) {
824
- return (await this.#dispatch("listCompactionJobs", [tableId]));
825
- }
826
- async listCompactionJobPage(afterId, limit) {
827
- return (await this.#dispatch("listCompactionJobPage", [afterId, limit]));
828
- }
829
- async updateCompactionJob(id, expectedRevision, update) {
830
- return (await this.#dispatch("updateCompactionJob", [
831
- id,
832
- expectedRevision,
833
- update,
834
- ]));
835
- }
836
- async cancelCompactionJob(id, expectedRevision, cancelledAt) {
837
- return (await this.#dispatch("cancelCompactionJob", [
838
- id,
839
- expectedRevision,
840
- cancelledAt,
841
- ]));
842
- }
843
- async removeCompactionJob(id) {
844
- await this.#dispatch("removeCompactionJob", [id]);
845
- }
846
- async createGarbageCollectionJob(input) {
847
- return (await this.#dispatch("createGarbageCollectionJob", [
848
- input,
849
- ]));
850
- }
851
- async getGarbageCollectionJob(id) {
852
- return (await this.#dispatch("getGarbageCollectionJob", [id]));
853
- }
854
- async listGarbageCollectionJobs() {
855
- return (await this.#dispatch("listGarbageCollectionJobs", []));
856
- }
857
- async runGarbageCollectionStep(input) {
858
- return (await this.#dispatch("runGarbageCollectionStep", [
859
- input,
860
- ]));
861
- }
862
- async removeGarbageCollectionJob(id) {
863
- await this.#dispatch("removeGarbageCollectionJob", [id]);
864
- }
865
- async removePrunedManifestRecords() {
866
- return (await this.#dispatch("removePrunedManifestRecords", []));
867
- }
868
- async getLogicalStorageBytes() {
869
- return (await this.#dispatch("getLogicalStorageBytes", []));
870
- }
871
- async exportSnapshot() {
872
- return (await this.#dispatch("exportSnapshot", []));
873
- }
874
- async importSnapshot(snapshot, options = {}) {
875
- const leader = this.#leader;
876
- if (leader !== undefined) {
877
- return leader.importSnapshot(snapshot, options);
821
+ await this.#dispatch("removeTempRun", [ownerId, runId]);
822
+ }
823
+ /** @internal Shared implementation installed for every ordinary RPC method below. */
824
+ async _dispatchGenerated(method, args) {
825
+ const bulkIndex = BULK_READ_ARGUMENT_INDEX[method];
826
+ if (bulkIndex !== undefined) {
827
+ const bulkItems = args[bulkIndex];
828
+ if (!Array.isArray(bulkItems))
829
+ throw new TypeError(`${method} items must be an array`);
830
+ assertStorageBulkReadItems(bulkItems, `${method} request`);
878
831
  }
879
- // The progress callback cannot cross the channel; report the two ends locally.
880
- const totalBytes = snapshot.blocks.reduce((total, block) => total + block.bytes.byteLength, 0);
881
- options.onProgress?.({ phase: "blocks", writtenBytes: 0, totalBytes });
882
- await this.#dispatch("importSnapshot", [snapshot, {}]);
883
- options.onProgress?.({ phase: "done", writtenBytes: totalBytes, totalBytes });
832
+ return this.#dispatch(method, args);
884
833
  }
885
834
  close() {
886
835
  if (this.#closed)
@@ -893,7 +842,15 @@ export class OpfsBlockStore {
893
842
  pending.reject(new Error("This OPFS store connection is closed"));
894
843
  }
895
844
  this.#pending.clear();
896
- this.#dedupe.clear();
845
+ this.#inFlightMutations.clear();
846
+ this.#settledMutations.clear();
847
+ this.#servedRequestLocks.clear();
848
+ this.#pendingRpcBytes = 0;
849
+ this.#inFlightMutationBytes = 0;
850
+ this.#inFlightReadBytes = 0;
851
+ this.#settledMutationBytes = 0;
852
+ this.#servedRequestCount = 0;
853
+ this.#servedRequestBytes = 0;
897
854
  const leader = this.#leader;
898
855
  this.#leader = undefined;
899
856
  if (leader !== undefined) {
@@ -918,12 +875,42 @@ export class OpfsBlockStore {
918
875
  _instanceIdForTests() {
919
876
  return this.#instanceId;
920
877
  }
878
+ /** Test-only: simulates an acknowledgement lost after the served operation settles. */
879
+ _dropNextRpcResultForTests() {
880
+ this.#dropNextRpcResultForTests = true;
881
+ }
882
+ /** Test-only: holds newly admitted served mutations until the returned release is called. */
883
+ _holdServedMutationsForTests() {
884
+ if (this.#servedMutationGateForTests !== undefined) {
885
+ throw new Error("Served mutations are already held");
886
+ }
887
+ let release;
888
+ this.#servedMutationGateForTests = new Promise((resolve) => {
889
+ release = resolve;
890
+ });
891
+ return () => {
892
+ this.#servedMutationGateForTests = undefined;
893
+ release();
894
+ };
895
+ }
896
+ /** Test-only: retransmits the oldest request with its stable deduplication identity. */
897
+ _resendOldestPendingForTests() {
898
+ const pending = this.#pending.values().next().value;
899
+ if (pending?.sentTo !== undefined)
900
+ this.#send(pending.sentTo, pending.message);
901
+ }
921
902
  /** Test-only counters that pin the connection's bounded RPC state and close-time cleanup. */
922
903
  _residentStateForTests() {
923
904
  return {
924
905
  answerChannels: this.#answerChannels.size,
925
- dedupeEntries: this.#dedupe.size,
906
+ dedupeEntries: this.#settledMutations.size,
907
+ inFlightMutations: this.#inFlightMutations.size,
926
908
  pendingRequests: this.#pending.size,
909
+ retainedRpcBytes: this.#pendingRpcBytes +
910
+ this.#inFlightMutationBytes +
911
+ this.#inFlightReadBytes +
912
+ this.#settledMutationBytes +
913
+ this.#servedRequestBytes,
927
914
  closed: this.#closed,
928
915
  };
929
916
  }
@@ -935,47 +922,110 @@ export class OpfsBlockStore {
935
922
  for (const pending of this.#pending.values())
936
923
  clearTimeout(pending.timer);
937
924
  this.#pending.clear();
938
- this.#dedupe.clear();
925
+ this.#inFlightMutations.clear();
926
+ this.#settledMutations.clear();
927
+ this.#servedRequestLocks.clear();
928
+ this.#pendingRpcBytes = 0;
929
+ this.#inFlightMutationBytes = 0;
930
+ this.#inFlightReadBytes = 0;
931
+ this.#settledMutationBytes = 0;
932
+ this.#servedRequestCount = 0;
933
+ this.#servedRequestBytes = 0;
939
934
  this.#leader?.crash();
940
935
  this.#leader = undefined;
941
936
  this.#closeChannels();
942
937
  }
943
938
  async #ensureFormatMarker() {
944
- const existing = await this.#tree.readFile(["format.json"], { lockedMeansAbsent: true });
939
+ const existing = await this.#tree.readFile(["format.json"], {
940
+ lockedMeansAbsent: true,
941
+ maxBytes: 1024,
942
+ });
945
943
  if (existing !== undefined) {
946
944
  try {
947
- const parsed = JSON.parse(new TextDecoder().decode(existing));
948
- if (typeof parsed.formatVersion === "number" &&
949
- parsed.formatVersion !== LOG_FORMAT_VERSION) {
950
- throw new Error(`This database uses OPFS layout version ${String(parsed.formatVersion)}; this build ` +
951
- `reads version ${String(LOG_FORMAT_VERSION)}. Delete it with deleteOpfsDatabase() — ` +
952
- `earlier layouts were experimental and carry no compatibility promise.`);
953
- }
945
+ this.#validateFormatMarker(existing);
954
946
  return;
955
947
  }
956
948
  catch (error) {
957
949
  if (!(error instanceof SyntaxError))
958
950
  throw error;
959
- // A torn marker from a crashed first open: rewrite it below.
951
+ // A torn marker from a crashed first open is repairable only while no database
952
+ // artifacts exist. Never stamp the current version over an unversioned WAL.
953
+ }
954
+ }
955
+ let artifact;
956
+ for await (const name of this.#tree.iterateNames([])) {
957
+ if (name !== "format.json") {
958
+ artifact = name;
959
+ break;
960
960
  }
961
961
  }
962
+ if (artifact !== undefined) {
963
+ throw new StorageCorruptionError("opfs", "format.json", `The OPFS format marker is ${existing === undefined ? "missing" : "torn"}, but the ` +
964
+ `database directory already contains storage artifacts (${artifact}). ` +
965
+ `Refusing to guess their layout version.`);
966
+ }
962
967
  const bytes = new TextEncoder().encode(JSON.stringify({ formatVersion: LOG_FORMAT_VERSION }));
963
968
  try {
964
969
  await this.#tree.writeFile(["format.json"], bytes, { flush: true });
965
970
  }
966
971
  catch (error) {
967
- // A concurrent open won the marker; theirs says the same thing.
968
972
  if (!isLockContention(error))
969
973
  throw error;
974
+ // Another opener owns the marker write. Do not assume it writes this build's version:
975
+ // wait until its handle closes, then validate the bytes it actually published.
976
+ for (let attempt = 0; attempt < 40; attempt += 1) {
977
+ await sleep(5 + Math.random() * 10);
978
+ const published = await this.#tree.readFile(["format.json"], {
979
+ lockedMeansAbsent: true,
980
+ maxBytes: 1024,
981
+ });
982
+ if (published === undefined)
983
+ continue;
984
+ this.#validateFormatMarker(published);
985
+ return;
986
+ }
987
+ throw new Error("A concurrent opener did not publish a readable OPFS format marker", {
988
+ cause: error,
989
+ });
970
990
  }
971
991
  }
992
+ #validateFormatMarker(bytes) {
993
+ const text = new TextDecoder().decode(bytes);
994
+ const parsed = JSON.parse(text);
995
+ if (typeof parsed !== "object" ||
996
+ parsed === null ||
997
+ Array.isArray(parsed) ||
998
+ !Number.isSafeInteger(parsed.formatVersion)) {
999
+ throw new StorageCorruptionError("opfs", "format.json", "The OPFS format marker is invalid: formatVersion must be a safe integer");
1000
+ }
1001
+ const formatVersion = parsed.formatVersion;
1002
+ if (formatVersion !== LOG_FORMAT_VERSION) {
1003
+ throw new StorageFormatVersionError("opfs", "format.json", formatVersion, LOG_FORMAT_VERSION, formatVersion < LOG_FORMAT_VERSION ? "older" : "newer");
1004
+ }
1005
+ if (Object.keys(parsed).length !== 1 ||
1006
+ !Object.hasOwn(parsed, "formatVersion") ||
1007
+ text !== JSON.stringify({ formatVersion })) {
1008
+ throw new StorageCorruptionError("opfs", "format.json", "The OPFS format marker is not the canonical layout marker");
1009
+ }
1010
+ }
1011
+ }
1012
+ for (const method of RPC_METHODS) {
1013
+ if (method in OpfsBlockStore.prototype)
1014
+ continue;
1015
+ Object.defineProperty(OpfsBlockStore.prototype, method, {
1016
+ configurable: true,
1017
+ value(...args) {
1018
+ return this._dispatchGenerated(method, args);
1019
+ },
1020
+ });
972
1021
  }
973
1022
  /** Removes every file of a database created by `OpfsBlockStore.open` under this name. */
974
1023
  export async function deleteOpfsDatabase(options) {
1024
+ const encodedName = encodeSegment(validateStorageDatabaseName(options.name));
975
1025
  const root = options.root ?? (await navigator.storage.getDirectory());
976
1026
  try {
977
1027
  const namespace = await root.getDirectoryHandle("minnowdb");
978
- await namespace.removeEntry(encodeSegment(options.name), { recursive: true });
1028
+ await namespace.removeEntry(encodedName, { recursive: true });
979
1029
  }
980
1030
  catch (error) {
981
1031
  if (!isDomError(error, "NotFoundError"))
@@ -984,11 +1034,10 @@ export async function deleteOpfsDatabase(options) {
984
1034
  }
985
1035
  const RPC_TIMED_OUT = new Error("The leader did not answer in time");
986
1036
  async function resolveDatabaseRoot(options) {
987
- if (options.name.length === 0)
988
- throw new TypeError("Database name cannot be empty");
1037
+ const encodedName = encodeSegment(validateStorageDatabaseName(options.name));
989
1038
  const root = options.root ?? (await navigator.storage.getDirectory());
990
1039
  const namespace = await root.getDirectoryHandle("minnowdb", { create: true });
991
- return namespace.getDirectoryHandle(encodeSegment(options.name), { create: true });
1040
+ return namespace.getDirectoryHandle(encodedName, { create: true });
992
1041
  }
993
1042
  function isLockContention(error) {
994
1043
  return isDomError(error, "NoModificationAllowedError") || isDomError(error, "InvalidStateError");
@@ -996,4 +1045,3 @@ function isLockContention(error) {
996
1045
  function sleep(ms) {
997
1046
  return new Promise((resolve) => setTimeout(resolve, ms));
998
1047
  }
999
- //# sourceMappingURL=store.js.map