@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,4 +1,5 @@
1
- import { parseRpcRequest, rpcEvent, rpcFailure, rpcResult, serializeError, } from "../worker-protocol/index.js";
1
+ import { MAX_SNAPSHOT_STREAM_CHUNK_BYTES } from "../storage/snapshot.js";
2
+ import { parseRpcRequest, MAX_DATABASE_RPC_IN_FLIGHT, rpcEvent, rpcFailure, rpcResult, serializeError, } from "../worker-protocol/index.js";
2
3
  import { MinnowDatabase, } from "./database.js";
3
4
  import { encodeQueryResult, encodeQueryRows } from "./result-wire.js";
4
5
  import { deserializeSchema, serializeMigrationSteps } from "./schema-wire.js";
@@ -22,28 +23,144 @@ class ColumnarResult {
22
23
  this.encoded = encoded;
23
24
  }
24
25
  }
26
+ class CursorBatchResult {
27
+ encoded;
28
+ constructor(encoded) {
29
+ this.encoded = encoded;
30
+ }
31
+ }
32
+ class SnapshotChunkResult {
33
+ chunk;
34
+ constructor(chunk) {
35
+ this.chunk = chunk;
36
+ }
37
+ }
25
38
  class WriteScopeAbortedError extends Error {
26
39
  name = "WriteScopeAbortedError";
27
40
  constructor() {
28
41
  super("Write scope aborted");
29
42
  }
30
43
  }
44
+ /** One-chunk rendezvous. The sender is acknowledged only when the decoder asks for more. */
45
+ class SnapshotImportChunkQueue {
46
+ #queued;
47
+ #consumed;
48
+ #reader;
49
+ #finished = false;
50
+ #failure;
51
+ [Symbol.asyncIterator]() {
52
+ return this;
53
+ }
54
+ push(chunk) {
55
+ if (chunk.byteLength > MAX_SNAPSHOT_STREAM_CHUNK_BYTES) {
56
+ throw new RangeError("Snapshot transfer chunk exceeds the 1 MiB limit");
57
+ }
58
+ if (this.#failure !== undefined)
59
+ return Promise.reject(snapshotQueueError(this.#failure));
60
+ if (this.#finished)
61
+ return Promise.reject(new Error("Snapshot import transfer is closed"));
62
+ if (this.#queued !== undefined) {
63
+ return Promise.reject(new Error("Snapshot import transfer already has a queued chunk"));
64
+ }
65
+ return new Promise((resolve, reject) => {
66
+ const reader = this.#reader;
67
+ if (reader === undefined) {
68
+ this.#queued = { chunk, resolve, reject };
69
+ return;
70
+ }
71
+ this.#reader = undefined;
72
+ this.#consumed = { resolve, reject };
73
+ reader.resolve({ done: false, value: chunk });
74
+ });
75
+ }
76
+ async next() {
77
+ this.#consumed?.resolve();
78
+ this.#consumed = undefined;
79
+ if (this.#failure !== undefined)
80
+ throw snapshotQueueError(this.#failure);
81
+ const queued = this.#queued;
82
+ if (queued !== undefined) {
83
+ this.#queued = undefined;
84
+ this.#consumed = { resolve: queued.resolve, reject: queued.reject };
85
+ return { done: false, value: queued.chunk };
86
+ }
87
+ if (this.#finished)
88
+ return { done: true, value: undefined };
89
+ return new Promise((resolve, reject) => {
90
+ this.#reader = { resolve, reject };
91
+ });
92
+ }
93
+ async return() {
94
+ this.finish();
95
+ return { done: true, value: undefined };
96
+ }
97
+ finish() {
98
+ if (this.#finished)
99
+ return;
100
+ this.#finished = true;
101
+ if (this.#queued === undefined) {
102
+ const reader = this.#reader;
103
+ this.#reader = undefined;
104
+ reader?.resolve({ done: true, value: undefined });
105
+ }
106
+ }
107
+ fail(error) {
108
+ if (this.#failure !== undefined)
109
+ return;
110
+ this.#failure = error;
111
+ this.#finished = true;
112
+ this.#queued?.reject(error);
113
+ this.#queued = undefined;
114
+ this.#consumed?.reject(error);
115
+ this.#consumed = undefined;
116
+ this.#reader?.reject(error);
117
+ this.#reader = undefined;
118
+ }
119
+ }
120
+ function snapshotQueueError(value) {
121
+ return value instanceof Error ? value : new Error("Snapshot transfer failed", { cause: value });
122
+ }
123
+ /** Hard per-connection ceiling for resident worker resources abandoned by a client. */
124
+ export const MAX_WORKER_HANDLES_PER_CONNECTION = 256;
125
+ const DEFAULT_WRITE_HANDLE_IDLE_TIMEOUT_MS = 30_000;
126
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
31
127
  class DatabaseRpcServer {
32
128
  database;
33
129
  scope;
34
130
  options;
35
131
  #handles = new Map();
132
+ #reservedHandleIds = new Set();
133
+ #openingHandlePromises = new Set();
134
+ #settlingWritePromises = new Set();
36
135
  #disposed = false;
136
+ #inFlightRpcCount = 0;
137
+ #inFlightRpcDrain;
138
+ #resolveInFlightRpcDrain;
139
+ #writeHandleIdleTimeoutMs;
37
140
  constructor(database, scope, options) {
38
141
  this.database = database;
39
142
  this.scope = scope;
40
143
  this.options = options;
144
+ this.#writeHandleIdleTimeoutMs =
145
+ options.writeHandleIdleTimeoutMs ?? DEFAULT_WRITE_HANDLE_IDLE_TIMEOUT_MS;
146
+ if (!Number.isSafeInteger(this.#writeHandleIdleTimeoutMs) ||
147
+ this.#writeHandleIdleTimeoutMs <= 0 ||
148
+ this.#writeHandleIdleTimeoutMs > MAX_TIMER_DELAY_MS) {
149
+ throw new RangeError(`Worker write-handle idle timeout must be a positive whole number no greater than ${String(MAX_TIMER_DELAY_MS)}`);
150
+ }
41
151
  }
42
152
  async handle(request) {
43
153
  if (request.kind === "rpc-init") {
44
154
  this.scope.postMessage(rpcResult(request.requestId, { ready: true }));
45
155
  return;
46
156
  }
157
+ const bypassLimit = request.method === "dispose";
158
+ if (!bypassLimit && this.#inFlightRpcCount >= MAX_DATABASE_RPC_IN_FLIGHT) {
159
+ this.scope.postMessage(rpcFailure(request.requestId, new RangeError(`A database worker connection cannot hold more than ${String(MAX_DATABASE_RPC_IN_FLIGHT)} in-flight requests`)));
160
+ return;
161
+ }
162
+ if (!bypassLimit)
163
+ this.#inFlightRpcCount += 1;
47
164
  try {
48
165
  if (this.#disposed)
49
166
  throw new Error("Database connection is disposed");
@@ -55,6 +172,16 @@ class DatabaseRpcServer {
55
172
  transfer: result.encoded.transfer,
56
173
  });
57
174
  }
175
+ else if (result instanceof CursorBatchResult) {
176
+ this.scope.postMessage(rpcResult(request.requestId, result.encoded === undefined
177
+ ? { done: true }
178
+ : { done: false, batch: result.encoded.payload }), result.encoded === undefined ? undefined : { transfer: result.encoded.transfer });
179
+ }
180
+ else if (result instanceof SnapshotChunkResult) {
181
+ this.scope.postMessage(rpcResult(request.requestId, { done: false, chunk: result.chunk }), {
182
+ transfer: [result.chunk.buffer],
183
+ });
184
+ }
58
185
  else {
59
186
  this.scope.postMessage(rpcResult(request.requestId, result));
60
187
  }
@@ -62,6 +189,16 @@ class DatabaseRpcServer {
62
189
  catch (error) {
63
190
  this.scope.postMessage(rpcFailure(request.requestId, error));
64
191
  }
192
+ finally {
193
+ if (!bypassLimit)
194
+ this.#inFlightRpcCount -= 1;
195
+ if (this.#inFlightRpcCount === 0 && this.#resolveInFlightRpcDrain !== undefined) {
196
+ const resolve = this.#resolveInFlightRpcDrain;
197
+ this.#inFlightRpcDrain = undefined;
198
+ this.#resolveInFlightRpcDrain = undefined;
199
+ resolve();
200
+ }
201
+ }
65
202
  }
66
203
  async #callRoot(method, args) {
67
204
  const database = this.database;
@@ -76,10 +213,11 @@ class DatabaseRpcServer {
76
213
  case "updateBatch":
77
214
  case "update":
78
215
  case "deleteBatch":
216
+ case "delete":
79
217
  case "runStatement":
80
218
  case "explain":
81
219
  case "execute":
82
- case "listVisibleSegments":
220
+ case "listVisibleSegmentPage":
83
221
  case "cleanupQuerySpill":
84
222
  case "compactTable":
85
223
  case "compactTableStep":
@@ -90,12 +228,30 @@ class DatabaseRpcServer {
90
228
  case "collectGarbageStep":
91
229
  case "resumeGarbageCollectionJob":
92
230
  case "bufferPoolStats":
231
+ case "maintenanceStatus":
232
+ case "checkIntegrity":
233
+ case "storageStats":
234
+ case "inspectInterruptedImport":
235
+ case "abortInterruptedImport":
93
236
  case "listGarbageCollectionJobs":
94
237
  return database[method]?.(...args);
95
238
  case "query": {
96
239
  const [sql, options] = args;
97
240
  return new ColumnarResult(encodeQueryResult(await this.database.query(sql, options)));
98
241
  }
242
+ case "queryCursorOpen": {
243
+ const handleId = this.#claimHandleId(args[0]);
244
+ const [sql, options] = args.slice(1);
245
+ try {
246
+ const iterator = this.database.queryCursor(sql, options);
247
+ this.#publishHandle(handleId, { type: "query-cursor", iterator });
248
+ }
249
+ catch (error) {
250
+ this.#releaseHandleId(handleId);
251
+ throw error;
252
+ }
253
+ return { handleId };
254
+ }
99
255
  case "run": {
100
256
  const query = args[0];
101
257
  return new ColumnarResult(encodeQueryRows(await this.database.run(query)));
@@ -118,90 +274,91 @@ class DatabaseRpcServer {
118
274
  steps: serializeMigrationSteps(result.steps),
119
275
  };
120
276
  }
121
- case "writeOpen": {
122
- // Same deferred-callback shape as snapshotOpen: the scoped write() stays pending
123
- // until the client commits or aborts, so its staged transaction spans the RPC session.
124
- const handleId = crypto.randomUUID();
125
- let sessionResolve;
126
- const sessionReady = new Promise((resolveSession) => {
127
- sessionResolve = resolveSession;
128
- });
129
- let finish;
130
- const done = this.database
131
- .write(async (session) => {
132
- sessionResolve(session);
133
- await new Promise((resolveCommit, rejectAbort) => {
134
- finish = (commit) => {
135
- if (commit)
136
- resolveCommit();
137
- else
138
- rejectAbort(new WriteScopeAbortedError());
139
- };
277
+ case "writeOpen":
278
+ return this.#trackOpeningHandle(this.#openWriteHandle());
279
+ case "snapshotOpen":
280
+ return this.#trackOpeningHandle(this.#openSnapshotHandle());
281
+ case "exportSnapshotOpen": {
282
+ const handleId = this.#claimGeneratedHandleId();
283
+ const iterator = this.database.exportSnapshotStream()[Symbol.asyncIterator]();
284
+ try {
285
+ this.#publishHandle(handleId, {
286
+ type: "snapshot-export",
287
+ iterator,
288
+ reading: false,
289
+ done: false,
140
290
  });
141
- })
142
- .then((outcome) => ({ version: outcome.version }));
143
- const session = await sessionReady;
144
- this.#handles.set(handleId, { type: "write", session, finish, done });
291
+ }
292
+ catch (error) {
293
+ await iterator.return(undefined);
294
+ this.#releaseHandleId(handleId);
295
+ throw error;
296
+ }
145
297
  return { handleId };
146
298
  }
147
- case "snapshotOpen": {
148
- // The scoped snapshot() holds its lease until the callback settles; the handle keeps
149
- // the callback pending until the client closes it, so the pin spans the RPC session.
150
- const handleId = crypto.randomUUID();
151
- let release;
152
- const version = await new Promise((resolveVersion, rejectVersion) => {
153
- void this.database
154
- .snapshot(async (session) => {
155
- resolveVersion(session.version);
156
- await new Promise((resolveRelease) => {
157
- release = resolveRelease;
158
- });
159
- })
160
- .catch((error) => {
161
- rejectVersion(error instanceof Error ? error : new Error(String(error)));
162
- });
163
- });
164
- this.#handles.set(handleId, { type: "snapshot", release });
165
- return { handleId, version };
166
- }
167
- case "exportSnapshotOpen": {
168
- // The snapshot is encoded here and held by the handle so the client can pull it across
169
- // in slices. One result frame carrying the whole thing would be a structured clone the
170
- // size of the database, and the main thread would sit still for all of it.
171
- const handleId = crypto.randomUUID();
172
- const bytes = await this.database.exportSnapshot();
173
- this.#handles.set(handleId, { type: "snapshot-export", bytes });
174
- return { handleId, byteLength: bytes.byteLength };
175
- }
176
299
  case "importSnapshotOpen": {
177
300
  // Named by the client, like the other event-producing handles, so its progress route
178
301
  // exists before the first frame the load can emit.
179
302
  const handleId = this.#claimHandleId(args[0]);
180
- this.#handles.set(handleId, { type: "snapshot-import", chunks: [], byteLength: 0 });
303
+ const queue = new SnapshotImportChunkQueue();
304
+ const abort = new AbortController();
305
+ const task = this.database.importSnapshotStream(queue, {
306
+ signal: abort.signal,
307
+ onProgress: (progress) => {
308
+ this.scope.postMessage(rpcEvent(handleId, "progress", progress));
309
+ },
310
+ });
311
+ void task.catch((error) => queue.fail(error));
312
+ try {
313
+ this.#publishHandle(handleId, { type: "snapshot-import", queue, task, abort });
314
+ }
315
+ catch (error) {
316
+ abort.abort(error);
317
+ queue.fail(error);
318
+ await task.catch(() => undefined);
319
+ this.#releaseHandleId(handleId);
320
+ throw error;
321
+ }
181
322
  return { handleId };
182
323
  }
183
324
  case "bufferedWriter": {
184
325
  // Event-producing handles are named by the client so its event routes exist before the
185
326
  // first frame the handle can emit.
186
327
  const handleId = this.#claimHandleId(args[0]);
187
- const writer = this.database.bufferedWriter(args[1], {
188
- ...args[2],
189
- onError: (error) => {
190
- this.scope.postMessage(rpcEvent(handleId, "error", serializeError(error)));
191
- },
192
- });
193
- this.#handles.set(handleId, { type: "writer", writer });
328
+ let writer;
329
+ try {
330
+ writer = this.database.bufferedWriter(args[1], {
331
+ ...args[2],
332
+ onError: (error) => {
333
+ this.scope.postMessage(rpcEvent(handleId, "error", serializeError(error)));
334
+ },
335
+ });
336
+ this.#publishHandle(handleId, { type: "writer", writer });
337
+ }
338
+ catch (error) {
339
+ await writer?.close().catch(() => undefined);
340
+ this.#releaseHandleId(handleId);
341
+ throw error;
342
+ }
194
343
  return { handleId };
195
344
  }
196
345
  case "liveQueries": {
197
346
  const handleId = this.#claimHandleId(args[0]);
198
347
  const { pollIntervalMs, channelName } = (args[1] ?? {});
199
348
  // The database owns channelName resolution (and closes the channel with the set).
200
- const set = this.database.liveQueries({
201
- ...(channelName === undefined ? {} : { channelName }),
202
- ...(pollIntervalMs === undefined ? {} : { pollIntervalMs }),
203
- });
204
- this.#handles.set(handleId, { type: "live-set", set, subscriptionIds: new Set() });
349
+ let set;
350
+ try {
351
+ set = this.database.liveQueries({
352
+ ...(channelName === undefined ? {} : { channelName }),
353
+ ...(pollIntervalMs === undefined ? {} : { pollIntervalMs }),
354
+ });
355
+ this.#publishHandle(handleId, { type: "live-set", set, subscriptionIds: new Set() });
356
+ }
357
+ catch (error) {
358
+ set?.close();
359
+ this.#releaseHandleId(handleId);
360
+ throw error;
361
+ }
205
362
  return { handleId };
206
363
  }
207
364
  case "dispose": {
@@ -216,56 +373,142 @@ class DatabaseRpcServer {
216
373
  throw new Error(`Unsupported database method: ${method}`);
217
374
  }
218
375
  }
376
+ async #openWriteHandle() {
377
+ // Same deferred-callback shape as snapshotOpen: the scoped write() stays pending until the
378
+ // client commits or aborts, so its staged transaction spans the RPC session.
379
+ const handleId = this.#claimGeneratedHandleId();
380
+ let sessionResolve;
381
+ let sessionReject;
382
+ const sessionReady = new Promise((resolveSession, rejectSession) => {
383
+ sessionResolve = resolveSession;
384
+ sessionReject = rejectSession;
385
+ });
386
+ let finish;
387
+ const done = this.database
388
+ .write(async (session) => {
389
+ sessionResolve(session);
390
+ await new Promise((resolveCommit, rejectAbort) => {
391
+ finish = (commit) => {
392
+ if (commit)
393
+ resolveCommit();
394
+ else
395
+ rejectAbort(new WriteScopeAbortedError());
396
+ };
397
+ });
398
+ })
399
+ .then((outcome) => ({ version: outcome.version }));
400
+ // beginDeferred can fail before the callback starts (for example at the durable reader-lease
401
+ // ceiling). Reject the open rendezvous too and observe `done` immediately.
402
+ void done.catch((error) => sessionReject(error));
403
+ try {
404
+ const session = await sessionReady;
405
+ if (finish === undefined)
406
+ throw new Error("Write scope did not install its settlement");
407
+ this.#publishHandle(handleId, {
408
+ type: "write",
409
+ session,
410
+ finish,
411
+ done,
412
+ activeCalls: 0,
413
+ activeCallDone: undefined,
414
+ finishActiveCall: undefined,
415
+ open: true,
416
+ idleTimer: undefined,
417
+ });
418
+ return { handleId };
419
+ }
420
+ catch (error) {
421
+ // Publication can lose a race with connection disposal. If the callback started, abort and
422
+ // join it before the opening task releases its lifecycle slot.
423
+ finish?.(false);
424
+ await done.catch(() => undefined);
425
+ this.#releaseHandleId(handleId);
426
+ throw error;
427
+ }
428
+ }
429
+ async #openSnapshotHandle() {
430
+ // The scoped snapshot() holds its lease until the callback settles; the handle keeps the
431
+ // callback pending until close, so the pin spans the RPC session.
432
+ const handleId = this.#claimGeneratedHandleId();
433
+ let release;
434
+ let resolveVersion;
435
+ let rejectVersion;
436
+ const versionReady = new Promise((resolve, reject) => {
437
+ resolveVersion = resolve;
438
+ rejectVersion = reject;
439
+ });
440
+ const done = this.database.snapshot(async (session) => {
441
+ resolveVersion(session.version);
442
+ await new Promise((resolveRelease) => {
443
+ release = resolveRelease;
444
+ });
445
+ });
446
+ // Lease admission may fail before the callback installs `release`. Preserve that typed error
447
+ // through the rendezvous and keep the task observed from creation.
448
+ void done.catch((error) => rejectVersion(error));
449
+ try {
450
+ const version = await versionReady;
451
+ if (release === undefined)
452
+ throw new Error("Snapshot scope did not install its release");
453
+ this.#publishHandle(handleId, { type: "snapshot", release, done });
454
+ return { handleId, version };
455
+ }
456
+ catch (error) {
457
+ release?.();
458
+ await done.catch(() => undefined);
459
+ this.#releaseHandleId(handleId);
460
+ throw error;
461
+ }
462
+ }
463
+ #trackOpeningHandle(opening) {
464
+ this.#openingHandlePromises.add(opening);
465
+ void opening.then(() => this.#openingHandlePromises.delete(opening), () => this.#openingHandlePromises.delete(opening));
466
+ return opening;
467
+ }
219
468
  async #callHandle(handleId, method, args) {
220
469
  const handle = this.#handles.get(handleId);
221
470
  if (handle === undefined)
222
471
  throw new Error(`Unknown handle: ${handleId}`);
472
+ if (handle.type === "write") {
473
+ this.#beginWriteHandleCall(handle);
474
+ try {
475
+ return await this.#callWriteHandle(handleId, handle, method, args);
476
+ }
477
+ finally {
478
+ this.#endWriteHandleCall(handleId, handle);
479
+ }
480
+ }
223
481
  switch (handle.type) {
224
482
  case "snapshot": {
225
483
  if (method !== "close")
226
484
  throw new Error(`Unsupported snapshot method: ${method}`);
227
485
  handle.release();
228
486
  this.#handles.delete(handleId);
487
+ await handle.done;
229
488
  return undefined;
230
489
  }
231
- case "write": {
232
- if (method === "query") {
233
- const [sql, options] = args;
234
- return new ColumnarResult(encodeQueryResult(await handle.session.query(sql, options)));
235
- }
236
- if (method === "execute") {
237
- const [sql, params] = args;
238
- return handle.session.execute(sql, params);
239
- }
240
- if (method === "stage") {
241
- const [op, tableName, input] = args;
242
- if (!isStageOp(op)) {
243
- throw new Error(`Unsupported write stage operation: ${String(op)}`);
244
- }
245
- return handle.session[op](tableName, input);
246
- }
247
- if (method === "commit") {
248
- handle.finish(true);
249
- this.#handles.delete(handleId);
250
- return handle.done;
251
- }
252
- if (method === "abort") {
253
- handle.finish(false);
254
- this.#handles.delete(handleId);
255
- return handle.done.catch((error) => {
256
- if (error instanceof WriteScopeAbortedError)
257
- return undefined;
258
- throw error;
259
- });
260
- }
261
- throw new Error(`Unsupported write method: ${method}`);
262
- }
263
490
  case "snapshot-export":
264
491
  return this.#callSnapshotExport(handleId, handle, method, args);
265
492
  case "snapshot-import":
266
493
  return this.#callSnapshotImport(handleId, handle, method, args);
267
494
  case "writer":
268
495
  return this.#callWriter(handleId, handle.writer, method, args);
496
+ case "query-cursor": {
497
+ if (method === "next") {
498
+ const next = await handle.iterator.next();
499
+ if (next.done) {
500
+ this.#handles.delete(handleId);
501
+ return new CursorBatchResult(undefined);
502
+ }
503
+ return new CursorBatchResult(encodeQueryResult(next.value));
504
+ }
505
+ if (method === "close") {
506
+ await handle.iterator.return?.();
507
+ this.#handles.delete(handleId);
508
+ return undefined;
509
+ }
510
+ throw new Error(`Unsupported query cursor method: ${method}`);
511
+ }
269
512
  case "live-set":
270
513
  return this.#callLiveSet(handleId, handle, method, args);
271
514
  case "live-subscription": {
@@ -280,25 +523,61 @@ class DatabaseRpcServer {
280
523
  }
281
524
  }
282
525
  }
526
+ async #callWriteHandle(handleId, handle, method, args) {
527
+ if (method === "query") {
528
+ const [sql, options] = args;
529
+ return new ColumnarResult(encodeQueryResult(await handle.session.query(sql, options)));
530
+ }
531
+ if (method === "execute") {
532
+ const [sql, params] = args;
533
+ return handle.session.execute(sql, params);
534
+ }
535
+ if (method === "stage") {
536
+ const [op, tableName, input] = args;
537
+ if (!isStageOp(op)) {
538
+ throw new Error(`Unsupported write stage operation: ${String(op)}`);
539
+ }
540
+ return handle.session[op](tableName, input);
541
+ }
542
+ if (method === "commit")
543
+ return this.#settleWriteHandle(handleId, handle, true);
544
+ if (method === "abort")
545
+ return this.#settleWriteHandle(handleId, handle, false);
546
+ throw new Error(`Unsupported write method: ${method}`);
547
+ }
283
548
  /**
284
549
  * Hands one slice of an encoded snapshot back. The slice is copied rather than viewed: a
285
550
  * subarray shares the whole buffer, and structured clone copies the buffer behind a view, not
286
551
  * the window into it — so every read would post the entire snapshot again.
287
552
  */
288
- #callSnapshotExport(handleId, handle, method, args) {
553
+ async #callSnapshotExport(handleId, handle, method, args) {
289
554
  switch (method) {
290
555
  case "read": {
291
- const [offset, length] = args;
292
- if (!Number.isSafeInteger(offset) || offset < 0) {
293
- throw new RangeError("Snapshot read offset must be a non-negative whole number");
556
+ if (args.length !== 0)
557
+ throw new TypeError("Snapshot stream read takes no arguments");
558
+ if (handle.reading)
559
+ throw new Error("Snapshot stream already has a read in flight");
560
+ if (handle.done)
561
+ return { done: true };
562
+ handle.reading = true;
563
+ try {
564
+ const next = await handle.iterator.next();
565
+ if (next.done === true) {
566
+ handle.done = true;
567
+ return { done: true };
568
+ }
569
+ // The iterator may yield a view into a 64 MiB block. Transfer only this bounded slice;
570
+ // detaching the larger backing buffer would corrupt subsequent slices.
571
+ const chunk = new Uint8Array(next.value.byteLength);
572
+ chunk.set(next.value);
573
+ return new SnapshotChunkResult(chunk);
294
574
  }
295
- if (!Number.isSafeInteger(length) || length <= 0) {
296
- throw new RangeError("Snapshot read length must be a positive whole number");
575
+ finally {
576
+ handle.reading = false;
297
577
  }
298
- const start = offset;
299
- return handle.bytes.slice(start, start + length);
300
578
  }
301
579
  case "close": {
580
+ await handle.iterator.return?.();
302
581
  this.#handles.delete(handleId);
303
582
  return undefined;
304
583
  }
@@ -314,26 +593,13 @@ class DatabaseRpcServer {
314
593
  if (!(chunk instanceof Uint8Array)) {
315
594
  throw new TypeError("Snapshot chunk must be a Uint8Array");
316
595
  }
317
- handle.chunks.push(chunk);
318
- handle.byteLength += chunk.byteLength;
596
+ await handle.queue.push(chunk);
319
597
  return undefined;
320
598
  }
321
599
  case "finish": {
322
- const bytes = new Uint8Array(handle.byteLength);
323
- let offset = 0;
324
- for (const chunk of handle.chunks) {
325
- bytes.set(chunk, offset);
326
- offset += chunk.byteLength;
327
- }
328
- // Dropped before the load starts: the decoded snapshot is another copy of these bytes,
329
- // and holding the chunks too would make three of them.
330
- handle.chunks.length = 0;
331
600
  try {
332
- await this.database.importSnapshot(bytes, {
333
- onProgress: (progress) => {
334
- this.scope.postMessage(rpcEvent(handleId, "progress", progress));
335
- },
336
- });
601
+ handle.queue.finish();
602
+ await handle.task;
337
603
  }
338
604
  finally {
339
605
  this.#handles.delete(handleId);
@@ -341,6 +607,15 @@ class DatabaseRpcServer {
341
607
  return undefined;
342
608
  }
343
609
  case "close": {
610
+ handle.queue.fail(new Error("Snapshot import transfer was cancelled"));
611
+ await handle.task.catch(() => undefined);
612
+ this.#handles.delete(handleId);
613
+ return undefined;
614
+ }
615
+ case "cancel": {
616
+ handle.abort.abort(new Error("Snapshot import was cancelled"));
617
+ handle.queue.fail(handle.abort.signal.reason);
618
+ await handle.task.catch(() => undefined);
344
619
  this.#handles.delete(handleId);
345
620
  return undefined;
346
621
  }
@@ -377,29 +652,71 @@ class DatabaseRpcServer {
377
652
  // posted before this call's own result frame.
378
653
  const subscriptionId = this.#claimHandleId(args[0]);
379
654
  const query = args[1];
380
- const subscription = await handle.set.subscribe(query, {
381
- onChange: (result) => {
382
- const encoded = encodeQueryResult(result);
383
- this.scope.postMessage(rpcEvent(subscriptionId, "change", encoded.payload), {
384
- transfer: encoded.transfer,
385
- });
386
- },
387
- onError: (error) => {
388
- this.scope.postMessage(rpcEvent(subscriptionId, "error", serializeError(error)));
389
- },
390
- onComplete: () => {
391
- this.scope.postMessage(rpcEvent(subscriptionId, "complete", null));
392
- this.#handles.delete(subscriptionId);
393
- const parent = this.#handles.get(handleId);
394
- if (parent?.type === "live-set")
395
- parent.subscriptionIds.delete(subscriptionId);
396
- },
397
- });
398
- this.#handles.set(subscriptionId, {
399
- type: "live-subscription",
400
- subscription,
401
- setId: handleId,
402
- });
655
+ let subscription;
656
+ try {
657
+ subscription = await handle.set.subscribe(query, {
658
+ onChange: (result) => {
659
+ const encoded = encodeQueryResult(result);
660
+ this.scope.postMessage(rpcEvent(subscriptionId, "change", encoded.payload), {
661
+ transfer: encoded.transfer,
662
+ });
663
+ },
664
+ onError: (error) => {
665
+ this.scope.postMessage(rpcEvent(subscriptionId, "error", serializeError(error)));
666
+ },
667
+ onComplete: () => {
668
+ this.scope.postMessage(rpcEvent(subscriptionId, "complete", null));
669
+ this.#handles.delete(subscriptionId);
670
+ const parent = this.#handles.get(handleId);
671
+ if (parent?.type === "live-set")
672
+ parent.subscriptionIds.delete(subscriptionId);
673
+ },
674
+ });
675
+ this.#publishHandle(subscriptionId, {
676
+ type: "live-subscription",
677
+ subscription,
678
+ setId: handleId,
679
+ });
680
+ }
681
+ catch (error) {
682
+ subscription?.close();
683
+ this.#releaseHandleId(subscriptionId);
684
+ throw error;
685
+ }
686
+ handle.subscriptionIds.add(subscriptionId);
687
+ return { dependencyTableIds: subscription.dependencyTableIds };
688
+ }
689
+ case "observe": {
690
+ const subscriptionId = this.#claimHandleId(args[0]);
691
+ const query = args[1];
692
+ let subscription;
693
+ try {
694
+ subscription = await handle.set.observe(query, {
695
+ onInvalidate: (invalidation) => {
696
+ this.scope.postMessage(rpcEvent(subscriptionId, "invalidate", invalidation));
697
+ },
698
+ onError: (error) => {
699
+ this.scope.postMessage(rpcEvent(subscriptionId, "error", serializeError(error)));
700
+ },
701
+ onComplete: () => {
702
+ this.scope.postMessage(rpcEvent(subscriptionId, "complete", null));
703
+ this.#handles.delete(subscriptionId);
704
+ const parent = this.#handles.get(handleId);
705
+ if (parent?.type === "live-set")
706
+ parent.subscriptionIds.delete(subscriptionId);
707
+ },
708
+ });
709
+ this.#publishHandle(subscriptionId, {
710
+ type: "live-subscription",
711
+ subscription,
712
+ setId: handleId,
713
+ });
714
+ }
715
+ catch (error) {
716
+ subscription?.close();
717
+ this.#releaseHandleId(subscriptionId);
718
+ throw error;
719
+ }
403
720
  handle.subscriptionIds.add(subscriptionId);
404
721
  return { dependencyTableIds: subscription.dependencyTableIds };
405
722
  }
@@ -421,10 +738,96 @@ class DatabaseRpcServer {
421
738
  if (typeof value !== "string" || value.length === 0) {
422
739
  throw new TypeError("Handle ID must be a non-empty string");
423
740
  }
424
- if (this.#handles.has(value))
741
+ if (this.#handles.has(value) || this.#reservedHandleIds.has(value)) {
425
742
  throw new Error(`Handle ID already exists: ${value}`);
743
+ }
744
+ if (this.#handles.size + this.#reservedHandleIds.size + this.#settlingWritePromises.size >=
745
+ MAX_WORKER_HANDLES_PER_CONNECTION) {
746
+ throw new Error(`Database connection cannot hold more than ${String(MAX_WORKER_HANDLES_PER_CONNECTION)} open handles (active or settling)`);
747
+ }
748
+ this.#reservedHandleIds.add(value);
426
749
  return value;
427
750
  }
751
+ #claimGeneratedHandleId() {
752
+ for (;;) {
753
+ const id = crypto.randomUUID();
754
+ if (!this.#handles.has(id) && !this.#reservedHandleIds.has(id))
755
+ return this.#claimHandleId(id);
756
+ }
757
+ }
758
+ #publishHandle(id, handle) {
759
+ if (this.#disposed)
760
+ throw new Error("Database connection is disposed");
761
+ if (!this.#reservedHandleIds.delete(id))
762
+ throw new Error(`Handle ID was not reserved: ${id}`);
763
+ this.#handles.set(id, handle);
764
+ if (handle.type === "write")
765
+ this.#armWriteHandleIdleTimer(id, handle);
766
+ }
767
+ #releaseHandleId(id) {
768
+ this.#reservedHandleIds.delete(id);
769
+ }
770
+ #beginWriteHandleCall(handle) {
771
+ if (!handle.open)
772
+ throw new Error("Write handle is closed");
773
+ if (handle.activeCalls !== 0) {
774
+ throw new Error("Write handle already has a call in flight");
775
+ }
776
+ handle.activeCalls = 1;
777
+ handle.activeCallDone = new Promise((resolve) => {
778
+ handle.finishActiveCall = resolve;
779
+ });
780
+ if (handle.idleTimer !== undefined) {
781
+ clearTimeout(handle.idleTimer);
782
+ handle.idleTimer = undefined;
783
+ }
784
+ }
785
+ #endWriteHandleCall(handleId, handle) {
786
+ handle.activeCalls -= 1;
787
+ if (handle.activeCalls < 0)
788
+ throw new Error("Write handle activity count underflow");
789
+ const finishActiveCall = handle.finishActiveCall;
790
+ handle.activeCallDone = undefined;
791
+ handle.finishActiveCall = undefined;
792
+ finishActiveCall?.();
793
+ if (handle.open && handle.activeCalls === 0)
794
+ this.#armWriteHandleIdleTimer(handleId, handle);
795
+ }
796
+ #armWriteHandleIdleTimer(handleId, handle) {
797
+ if (!handle.open || handle.activeCalls !== 0 || this.#handles.get(handleId) !== handle)
798
+ return;
799
+ if (handle.idleTimer !== undefined)
800
+ clearTimeout(handle.idleTimer);
801
+ handle.idleTimer = setTimeout(() => {
802
+ handle.idleTimer = undefined;
803
+ if (!handle.open || handle.activeCalls !== 0 || this.#handles.get(handleId) !== handle)
804
+ return;
805
+ // Delete first: a late commit or stage frame must fail rather than revive an expired scope
806
+ // while its asynchronous durable rollback is still joining.
807
+ void this.#settleWriteHandle(handleId, handle, false).catch(() => undefined);
808
+ }, this.#writeHandleIdleTimeoutMs);
809
+ unrefTimer(handle.idleTimer);
810
+ }
811
+ async #settleWriteHandle(handleId, handle, commit) {
812
+ if (!handle.open)
813
+ throw new Error("Write handle is closed");
814
+ handle.open = false;
815
+ if (handle.idleTimer !== undefined) {
816
+ clearTimeout(handle.idleTimer);
817
+ handle.idleTimer = undefined;
818
+ }
819
+ if (this.#handles.get(handleId) === handle)
820
+ this.#handles.delete(handleId);
821
+ handle.finish(commit);
822
+ const settlement = handle.done.catch((error) => {
823
+ if (!commit && error instanceof WriteScopeAbortedError)
824
+ return undefined;
825
+ throw error;
826
+ });
827
+ this.#settlingWritePromises.add(settlement);
828
+ void settlement.then(() => this.#settlingWritePromises.delete(settlement), () => this.#settlingWritePromises.delete(settlement));
829
+ return settlement;
830
+ }
428
831
  #closeLiveSet(handleId, handle) {
429
832
  handle.set.close();
430
833
  for (const subscriptionId of handle.subscriptionIds)
@@ -433,18 +836,51 @@ class DatabaseRpcServer {
433
836
  }
434
837
  async #dispose() {
435
838
  this.#disposed = true;
839
+ this.#reservedHandleIds.clear();
436
840
  const handles = [...this.#handles.entries()];
437
841
  this.#handles.clear();
842
+ const handleCleanup = [];
843
+ // Stop every abandoned-write deadline before waiting on active calls. The cleared handle map
844
+ // also prevents their completions from rearming one while disposal is in progress.
845
+ for (const [, handle] of handles) {
846
+ if (handle.type === "write" && handle.idleTimer !== undefined) {
847
+ clearTimeout(handle.idleTimer);
848
+ handle.idleTimer = undefined;
849
+ }
850
+ }
851
+ // Signal every handle before awaiting any cleanup. Imports and iterators can have an admitted
852
+ // RPC blocked inside them; closing sequentially could wait on the first while never reaching
853
+ // the signal that lets a later one finish.
438
854
  for (const [handleId, handle] of handles) {
439
855
  try {
440
- if (handle.type === "snapshot")
856
+ if (handle.type === "snapshot") {
441
857
  handle.release();
858
+ handleCleanup.push(handle.done.catch(() => undefined));
859
+ }
442
860
  else if (handle.type === "write") {
443
- handle.finish(false);
444
- await handle.done.catch(() => undefined);
861
+ // A session operation owns the transaction revision until it returns. Aborting sooner
862
+ // can race its final journal update and leave an active owner until TTL. Once it exits,
863
+ // abort only if commit/abort/idle expiry has not already chosen a terminal outcome.
864
+ handleCleanup.push((async () => {
865
+ await handle.activeCallDone;
866
+ if (handle.open) {
867
+ await this.#settleWriteHandle(handleId, handle, false).catch(() => undefined);
868
+ }
869
+ })());
445
870
  }
446
871
  else if (handle.type === "writer")
447
- await handle.writer.close();
872
+ handleCleanup.push(handle.writer.close());
873
+ else if (handle.type === "query-cursor") {
874
+ handleCleanup.push(Promise.resolve(handle.iterator.return?.()));
875
+ }
876
+ else if (handle.type === "snapshot-export") {
877
+ handleCleanup.push(Promise.resolve(handle.iterator.return?.()));
878
+ }
879
+ else if (handle.type === "snapshot-import") {
880
+ handle.abort.abort(new Error("Database connection was disposed"));
881
+ handle.queue.fail(new Error("Database connection was disposed"));
882
+ handleCleanup.push(handle.task.catch(() => undefined));
883
+ }
448
884
  else if (handle.type === "live-set")
449
885
  this.#closeLiveSet(handleId, handle);
450
886
  }
@@ -452,9 +888,30 @@ class DatabaseRpcServer {
452
888
  // Dispose every handle even when one close fails; the client is already gone.
453
889
  }
454
890
  }
891
+ await Promise.allSettled(handleCleanup);
892
+ // Commit/abort/expiry removes its handle before durable settlement completes so late RPCs
893
+ // fail closed. Join those detached operations before closing the database or injected store.
894
+ while (this.#settlingWritePromises.size > 0) {
895
+ await Promise.allSettled([...this.#settlingWritePromises]);
896
+ }
897
+ // A handle whose lease admission was already in flight when disposal began is not yet in the
898
+ // handle map. Its opener observes the disposed publication, releases any admitted lease, and
899
+ // settles only after that cleanup is complete.
900
+ while (this.#openingHandlePromises.size > 0) {
901
+ await Promise.allSettled([...this.#openingHandlePromises]);
902
+ }
903
+ await this.#waitForInFlightRpcs();
455
904
  await this.database.close();
456
905
  await this.options.onDispose?.();
457
906
  }
907
+ #waitForInFlightRpcs() {
908
+ if (this.#inFlightRpcCount === 0)
909
+ return Promise.resolve();
910
+ this.#inFlightRpcDrain ??= new Promise((resolve) => {
911
+ this.#resolveInFlightRpcDrain = resolve;
912
+ });
913
+ return this.#inFlightRpcDrain;
914
+ }
458
915
  }
459
916
  /**
460
917
  * Answers client RPC frames against a database you constructed yourself. Use this in a custom
@@ -535,6 +992,9 @@ async function createServer(scope, payload, options) {
535
992
  const store = await createStore(payload.store, options);
536
993
  const database = new MinnowDatabase(store, payload.options ?? {});
537
994
  return new DatabaseRpcServer(database, scope, {
995
+ ...(payload.options?.transactionIdleTimeoutMs === undefined
996
+ ? {}
997
+ : { writeHandleIdleTimeoutMs: payload.options.transactionIdleTimeoutMs }),
538
998
  onDispose: () => store.close(),
539
999
  onVisibility: (visible) => {
540
1000
  store.setForeground?.(visible);
@@ -576,4 +1036,8 @@ function requestIdOf(value) {
576
1036
  }
577
1037
  return undefined;
578
1038
  }
579
- //# sourceMappingURL=worker-host.js.map
1039
+ /** A worker cleanup deadline must not keep a Node process alive by itself. */
1040
+ function unrefTimer(timer) {
1041
+ const candidate = timer;
1042
+ candidate.unref?.();
1043
+ }