toiljs 0.0.115 → 0.0.117

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 (326) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +16 -7
  3. package/TYPESCRIPT_LAW.md +1946 -2073
  4. package/build/backend/.tsbuildinfo +1 -1
  5. package/build/cli/.tsbuildinfo +1 -1
  6. package/build/cli/index.js +2375 -258
  7. package/build/client/.tsbuildinfo +1 -1
  8. package/build/client/dev/devtools.js +1 -1
  9. package/build/client/dev/error-overlay.js +1 -1
  10. package/build/client/routing/mount.js +1 -1
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/auth-emails.js +7 -3
  13. package/build/compiler/emails.js +1 -1
  14. package/build/compiler/index.d.ts +1 -0
  15. package/build/compiler/index.js +25 -4
  16. package/build/compiler/pages.js +2 -3
  17. package/build/compiler/prerender.d.ts +3 -8
  18. package/build/compiler/prerender.js +88 -127
  19. package/build/compiler/ssg.js +3 -4
  20. package/build/compiler/template-build.js +3 -4
  21. package/build/compiler/toil-docs.generated.js +6 -5
  22. package/build/compiler/vite.js +3 -6
  23. package/build/devserver/.tsbuildinfo +1 -1
  24. package/build/devserver/db/database.js +7 -3
  25. package/build/devserver/db/types.js +1 -1
  26. package/build/devserver/production.js +4 -3
  27. package/build/devserver/runtime/crypto.d.ts +1 -1
  28. package/build/io/.tsbuildinfo +1 -1
  29. package/build/logger/.tsbuildinfo +1 -1
  30. package/build/shared/.tsbuildinfo +1 -1
  31. package/examples/basic/client/routes/analytics.tsx +7 -5
  32. package/examples/basic/server/tsconfig.json +1 -3
  33. package/package.json +45 -44
  34. package/presets/no-uint8array-tostring.js +120 -153
  35. package/presets/oxlint.d.ts +5 -0
  36. package/presets/oxlint.js +224 -0
  37. package/server/runtime/tsconfig.json +1 -3
  38. package/tsconfig.json +2 -11
  39. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
  40. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
  41. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  42. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  43. package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
  44. package/.github/changelog-config.json +0 -45
  45. package/.github/dependabot.yml +0 -27
  46. package/.github/workflows/ci.yml +0 -222
  47. package/.vscode/settings.json +0 -10
  48. package/docs/README.md +0 -107
  49. package/docs/auth/README.md +0 -104
  50. package/docs/auth/configuration.md +0 -105
  51. package/docs/auth/emails.md +0 -92
  52. package/docs/auth/extending.md +0 -202
  53. package/docs/auth/how-it-works.md +0 -138
  54. package/docs/auth/usage.md +0 -188
  55. package/docs/backend/README.md +0 -143
  56. package/docs/backend/data.md +0 -351
  57. package/docs/backend/rest.md +0 -402
  58. package/docs/backend/rpc.md +0 -226
  59. package/docs/background/README.md +0 -114
  60. package/docs/background/daemons.md +0 -276
  61. package/docs/background/derive.md +0 -179
  62. package/docs/cli/README.md +0 -379
  63. package/docs/concepts/ai-guide.md +0 -132
  64. package/docs/concepts/config.md +0 -416
  65. package/docs/concepts/decorators.md +0 -127
  66. package/docs/concepts/security.md +0 -108
  67. package/docs/concepts/tiers.md +0 -166
  68. package/docs/concepts/types.md +0 -216
  69. package/docs/database/README.md +0 -143
  70. package/docs/database/capacity.md +0 -350
  71. package/docs/database/counters.md +0 -174
  72. package/docs/database/documents.md +0 -277
  73. package/docs/database/events.md +0 -307
  74. package/docs/database/membership.md +0 -246
  75. package/docs/database/setup.md +0 -155
  76. package/docs/database/unique.md +0 -216
  77. package/docs/database/views.md +0 -246
  78. package/docs/frontend/README.md +0 -104
  79. package/docs/frontend/components.md +0 -286
  80. package/docs/frontend/data-fetching.md +0 -260
  81. package/docs/frontend/images.md +0 -148
  82. package/docs/frontend/metadata.md +0 -344
  83. package/docs/frontend/navigation.md +0 -296
  84. package/docs/frontend/rendering.md +0 -236
  85. package/docs/frontend/routing.md +0 -346
  86. package/docs/frontend/scripts.md +0 -118
  87. package/docs/frontend/search.md +0 -191
  88. package/docs/frontend/styling.md +0 -147
  89. package/docs/frontend/toil-global.md +0 -216
  90. package/docs/getting-started/README.md +0 -81
  91. package/docs/getting-started/create-project.md +0 -131
  92. package/docs/getting-started/deploy.md +0 -101
  93. package/docs/getting-started/first-app.md +0 -215
  94. package/docs/getting-started/installation.md +0 -124
  95. package/docs/getting-started/migrating.md +0 -125
  96. package/docs/getting-started/project-structure.md +0 -163
  97. package/docs/introduction/README.md +0 -66
  98. package/docs/introduction/design-principles.md +0 -81
  99. package/docs/introduction/distributed.md +0 -91
  100. package/docs/introduction/how-it-works.md +0 -121
  101. package/docs/introduction/hyperscale.md +0 -70
  102. package/docs/introduction/modern-stack.md +0 -85
  103. package/docs/introduction/vs-other-frameworks.md +0 -64
  104. package/docs/introduction/why-toil.md +0 -99
  105. package/docs/llms.txt +0 -95
  106. package/docs/realtime/README.md +0 -102
  107. package/docs/realtime/channels.md +0 -211
  108. package/docs/realtime/streams.md +0 -369
  109. package/docs/services/README.md +0 -60
  110. package/docs/services/analytics.md +0 -279
  111. package/docs/services/caching.md +0 -175
  112. package/docs/services/cookies.md +0 -235
  113. package/docs/services/crypto.md +0 -209
  114. package/docs/services/email.md +0 -289
  115. package/docs/services/environment.md +0 -141
  116. package/docs/services/ratelimit.md +0 -174
  117. package/docs/services/time.md +0 -85
  118. package/eslint.config.js +0 -57
  119. package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
  120. package/presets/eslint.js +0 -88
  121. package/src/backend/index.ts +0 -204
  122. package/src/cli/configure.ts +0 -363
  123. package/src/cli/create.ts +0 -924
  124. package/src/cli/db.ts +0 -158
  125. package/src/cli/diagnostics.ts +0 -665
  126. package/src/cli/doctor.ts +0 -982
  127. package/src/cli/features.ts +0 -170
  128. package/src/cli/index.ts +0 -326
  129. package/src/cli/notify.ts +0 -120
  130. package/src/cli/proc.ts +0 -56
  131. package/src/cli/ui.ts +0 -169
  132. package/src/cli/update.ts +0 -248
  133. package/src/cli/updates.ts +0 -92
  134. package/src/cli/validate.ts +0 -31
  135. package/src/cli/version-check.ts +0 -125
  136. package/src/client/auth.ts +0 -869
  137. package/src/client/channel/channel.ts +0 -146
  138. package/src/client/components/Form.tsx +0 -65
  139. package/src/client/components/Image.tsx +0 -160
  140. package/src/client/components/Script.tsx +0 -113
  141. package/src/client/components/Slot.tsx +0 -21
  142. package/src/client/dev/devtools.tsx +0 -1089
  143. package/src/client/dev/error-overlay.tsx +0 -225
  144. package/src/client/errors.ts +0 -11
  145. package/src/client/head/head.ts +0 -182
  146. package/src/client/head/metadata.ts +0 -110
  147. package/src/client/index.ts +0 -130
  148. package/src/client/navigation/Link.tsx +0 -108
  149. package/src/client/navigation/NavLink.tsx +0 -86
  150. package/src/client/navigation/navigation.ts +0 -235
  151. package/src/client/navigation/prefetch.ts +0 -169
  152. package/src/client/navigation/scroll.ts +0 -53
  153. package/src/client/routing/Router.tsx +0 -216
  154. package/src/client/routing/action.ts +0 -122
  155. package/src/client/routing/error-boundary.tsx +0 -43
  156. package/src/client/routing/hooks.ts +0 -140
  157. package/src/client/routing/lazy.ts +0 -77
  158. package/src/client/routing/loader.ts +0 -381
  159. package/src/client/routing/match.ts +0 -47
  160. package/src/client/routing/mount.tsx +0 -90
  161. package/src/client/routing/params-context.ts +0 -10
  162. package/src/client/routing/slot-context.ts +0 -7
  163. package/src/client/rpc.ts +0 -97
  164. package/src/client/search/search.ts +0 -189
  165. package/src/client/search/use-page-search.ts +0 -73
  166. package/src/client/ssr/markers.tsx +0 -172
  167. package/src/client/stream/client.ts +0 -237
  168. package/src/client/types.ts +0 -73
  169. package/src/compiler/auth-emails.ts +0 -312
  170. package/src/compiler/config.ts +0 -369
  171. package/src/compiler/docs.ts +0 -127
  172. package/src/compiler/email-preview.ts +0 -314
  173. package/src/compiler/emails.ts +0 -404
  174. package/src/compiler/fonts.ts +0 -89
  175. package/src/compiler/generate.ts +0 -471
  176. package/src/compiler/image-blur.ts +0 -42
  177. package/src/compiler/image-report.ts +0 -88
  178. package/src/compiler/index.ts +0 -1362
  179. package/src/compiler/pages.ts +0 -49
  180. package/src/compiler/plugin.ts +0 -391
  181. package/src/compiler/prerender.ts +0 -263
  182. package/src/compiler/routes.ts +0 -170
  183. package/src/compiler/seo.ts +0 -415
  184. package/src/compiler/sri.ts +0 -144
  185. package/src/compiler/ssg.ts +0 -177
  186. package/src/compiler/ssr-codegen.ts +0 -85
  187. package/src/compiler/template-build.ts +0 -676
  188. package/src/compiler/template.ts +0 -262
  189. package/src/compiler/toil-docs.generated.ts +0 -77
  190. package/src/compiler/vite.ts +0 -245
  191. package/src/devserver/analytics/index.ts +0 -317
  192. package/src/devserver/config/dotenv.ts +0 -102
  193. package/src/devserver/config/env.ts +0 -30
  194. package/src/devserver/config/ratelimit.ts +0 -123
  195. package/src/devserver/daemon/catalog.ts +0 -120
  196. package/src/devserver/daemon/cron.ts +0 -87
  197. package/src/devserver/daemon/host.ts +0 -134
  198. package/src/devserver/daemon/index.ts +0 -369
  199. package/src/devserver/daemon/runtime.ts +0 -48
  200. package/src/devserver/db/catalog.ts +0 -109
  201. package/src/devserver/db/database.ts +0 -1834
  202. package/src/devserver/db/derives.ts +0 -121
  203. package/src/devserver/db/index.ts +0 -20
  204. package/src/devserver/db/routeKinds.ts +0 -191
  205. package/src/devserver/db/types.ts +0 -168
  206. package/src/devserver/email/caps.ts +0 -0
  207. package/src/devserver/email/config.ts +0 -141
  208. package/src/devserver/email/console.ts +0 -348
  209. package/src/devserver/email/index.ts +0 -155
  210. package/src/devserver/email/providers.ts +0 -131
  211. package/src/devserver/email/status.ts +0 -23
  212. package/src/devserver/email/validate.ts +0 -37
  213. package/src/devserver/email/wire.ts +0 -55
  214. package/src/devserver/http/cache.ts +0 -0
  215. package/src/devserver/http/envelope.ts +0 -157
  216. package/src/devserver/http/proxy.ts +0 -172
  217. package/src/devserver/http/runtime.ts +0 -387
  218. package/src/devserver/index.ts +0 -43
  219. package/src/devserver/production-ipc.ts +0 -63
  220. package/src/devserver/production-worker.ts +0 -83
  221. package/src/devserver/production.ts +0 -718
  222. package/src/devserver/runtime/crypto.ts +0 -587
  223. package/src/devserver/runtime/host.ts +0 -427
  224. package/src/devserver/runtime/module.ts +0 -442
  225. package/src/devserver/server.ts +0 -251
  226. package/src/devserver/ssr.ts +0 -208
  227. package/src/devserver/stream/catalog.ts +0 -106
  228. package/src/devserver/stream/host.ts +0 -42
  229. package/src/devserver/stream/index.ts +0 -301
  230. package/src/devserver/stream/manager.ts +0 -167
  231. package/src/devserver/stream/router.ts +0 -101
  232. package/src/devserver/stream/wire.ts +0 -58
  233. package/src/devserver/stream/ws.ts +0 -78
  234. package/src/devserver/wasm/sections.ts +0 -59
  235. package/src/devserver/wasm/surface.ts +0 -115
  236. package/src/io/FastMap.ts +0 -151
  237. package/src/io/FastSet.ts +0 -113
  238. package/src/io/codec.ts +0 -362
  239. package/src/io/index.ts +0 -10
  240. package/src/io/lengths.ts +0 -14
  241. package/src/io/types.ts +0 -19
  242. package/src/logger/index.ts +0 -22
  243. package/src/shared/index.ts +0 -46
  244. package/test/analytics-dev.test.ts +0 -113
  245. package/test/assembly/aspect-shim.ts +0 -33
  246. package/test/assembly/cookie.spec.ts +0 -303
  247. package/test/assembly/example.spec.ts +0 -22
  248. package/test/assembly/ssr.spec.ts +0 -95
  249. package/test/assembly.test.ts +0 -59
  250. package/test/auth-emails.test.ts +0 -118
  251. package/test/built-ssr.test.ts +0 -148
  252. package/test/channel.test.ts +0 -21
  253. package/test/config-host-port.test.ts +0 -59
  254. package/test/configure.test.ts +0 -94
  255. package/test/daemon-build.test.ts +0 -278
  256. package/test/daemon-catalog.test.ts +0 -349
  257. package/test/daemon-emulation.test.ts +0 -256
  258. package/test/db.test.ts +0 -0
  259. package/test/devserver-database.test.ts +0 -1102
  260. package/test/devserver-email.test.ts +0 -306
  261. package/test/devserver-pqauth.test.ts +0 -93
  262. package/test/devserver-secrets.test.ts +0 -63
  263. package/test/devserver.test.ts +0 -259
  264. package/test/doctor.test.ts +0 -298
  265. package/test/dom/Image.test.tsx +0 -144
  266. package/test/dom/Link.test.tsx +0 -67
  267. package/test/dom/NavLink.test.tsx +0 -37
  268. package/test/dom/Script.test.tsx +0 -48
  269. package/test/dom/action.test.tsx +0 -146
  270. package/test/dom/error-overlay.test.tsx +0 -44
  271. package/test/dom/loader.test.tsx +0 -121
  272. package/test/dom/navigation.test.ts +0 -59
  273. package/test/dom/revalidate.test.tsx +0 -38
  274. package/test/dom/route-head.test.tsx +0 -56
  275. package/test/dom/router-loading.test.tsx +0 -44
  276. package/test/dom/scroll.test.ts +0 -56
  277. package/test/dom/slot.test.tsx +0 -131
  278. package/test/dom/use-metadata.test.tsx +0 -58
  279. package/test/dom/view-transitions.test.tsx +0 -53
  280. package/test/dynamic-routing.test.ts +0 -132
  281. package/test/email-console.test.ts +0 -86
  282. package/test/email-preview.test.ts +0 -73
  283. package/test/emails.test.ts +0 -58
  284. package/test/errors.test.ts +0 -21
  285. package/test/example-guestbook.test.ts +0 -123
  286. package/test/features.test.ts +0 -149
  287. package/test/fixtures/bignum-wire/spec.ts +0 -25
  288. package/test/fixtures/daemon-app.ts +0 -65
  289. package/test/fixtures/stream-echo.ts +0 -26
  290. package/test/fixtures/stream-gate.ts +0 -28
  291. package/test/fixtures/stream-trap.ts +0 -18
  292. package/test/fixtures/stream-typed.ts +0 -41
  293. package/test/fonts.test.ts +0 -28
  294. package/test/global-setup.ts +0 -17
  295. package/test/head.test.ts +0 -42
  296. package/test/image-blur.test.ts +0 -50
  297. package/test/io.test.ts +0 -117
  298. package/test/metadata.test.ts +0 -40
  299. package/test/navlink.test.ts +0 -28
  300. package/test/pages.test.ts +0 -105
  301. package/test/placeholder.test.ts +0 -9
  302. package/test/pqauth-e2e.test.ts +0 -216
  303. package/test/pqauth-email.test.ts +0 -801
  304. package/test/prerender.test.ts +0 -116
  305. package/test/prettier-plugin.test.ts +0 -46
  306. package/test/routes.test.ts +0 -76
  307. package/test/rpc-bignum-wire.test.ts +0 -164
  308. package/test/rpc-dispatch.test.ts +0 -132
  309. package/test/rpc-kinds.test.ts +0 -18
  310. package/test/rpc.test.ts +0 -66
  311. package/test/search.test.ts +0 -114
  312. package/test/seo.test.ts +0 -175
  313. package/test/slot-layouts.test.ts +0 -69
  314. package/test/sri.test.ts +0 -129
  315. package/test/ssg.test.ts +0 -36
  316. package/test/ssr-hydration.test.tsx +0 -122
  317. package/test/ssr-render.test.ts +0 -197
  318. package/test/ssr-template.test.tsx +0 -482
  319. package/test/stream-emulation.test.ts +0 -479
  320. package/test/ui.test.ts +0 -40
  321. package/test/update.test.ts +0 -58
  322. package/test/validate.test.ts +0 -42
  323. package/test/version-check.test.ts +0 -130
  324. package/tests/data-parity/generated-parity.ts +0 -99
  325. package/tests/data-parity/parity.ts +0 -80
  326. package/tests/data-parity/spec.ts +0 -46
@@ -1,1834 +0,0 @@
1
- /**
2
- * DEV emulation of the ToilDB record-family data API (`env::data.*`).
3
- *
4
- * The compiler-emitted guest (`@database` + `App.users.get/create/...`) calls
5
- * these; under `toiljs dev` we back them with a single-process in-memory store.
6
- * The production edge backs the SAME imports with ScyllaDB via the off-core
7
- * "scylla rail" (toil-backend). The wire ABI mirrors the edge exactly
8
- * (toildb/ABI.md): every byte region is a (ptr,len) into guest memory; returns
9
- * are `>= 0` success (a length/handle/flag), `-1` too-small, `-2` absent,
10
- * `<= -1000` a typed error (`-(1000+TDLnnn)`); variable-length results use the
11
- * two-step `take_result` pull.
12
- *
13
- * This is a DEV store: single-process, single-tenant, lost on restart - never a
14
- * production path. All seven families (record/view/unique/events/membership/
15
- * counter/capacity) live on the {@link DevDatabase} class; one process shares a
16
- * single instance ({@link devDb}), since the dev store is one per process.
17
- */
18
-
19
- import fs from 'node:fs';
20
- import { createHash } from 'node:crypto';
21
- import path from 'node:path';
22
-
23
- import { DataReader, DataWriter } from 'toiljs/io';
24
- import type { MemoryRef } from '../runtime/host.js';
25
- import { parseCatalog } from './catalog.js';
26
- import {
27
- ABSENT,
28
- ALREADY_EXISTS,
29
- type CapLedger,
30
- CODEC_ERR,
31
- CollectionFamily,
32
- CONFLICT,
33
- type DbCatalogState,
34
- type DbDevState,
35
- type DbSnapshot,
36
- DbFunctionKind,
37
- DEFAULT_FILL_WAIT_MS,
38
- type DevCollectionHandle,
39
- INVALID_HANDLE,
40
- MAX_KEY,
41
- MAX_FILL_WAIT_MS,
42
- MAX_NAME,
43
- MAX_RESERVATION_TTL_MS,
44
- MAX_RESERVATIONS,
45
- MAX_VALUE,
46
- OP_NOT_ALLOWED_FOR_FAMILY,
47
- OP_NOT_ALLOWED_IN_KIND,
48
- type RecordOutcomeSnapshot,
49
- type Reservation,
50
- satI64,
51
- SCHEMA_UNAVAILABLE,
52
- TOO_MANY_KEYS,
53
- TOO_SMALL,
54
- UNAVAILABLE,
55
- isCollectionFamily,
56
- } from './types.js';
57
-
58
- // ---- schema versions: the dev equivalent of the edge binding the row's
59
- // schema_version. Writes STAMP the value type's CURRENT version (from the loaded
60
- // wasm's catalog); reads SURFACE the stamp. When a @data type evolves and the wasm
61
- // is rebuilt, the catalog version changes but data on disk keeps its old stamp, so
62
- // a read reports the old version and the guest's woven decoder runs the @migrate.
63
-
64
- function mem(ref: MemoryRef): Buffer {
65
- if (!ref.memory) throw new Error('data host import called before memory was bound');
66
- return Buffer.from(ref.memory.buffer);
67
- }
68
-
69
- /** Bounds-checked read that COPIES out of guest memory (the buffer is reused). */
70
- function readCopy(ref: MemoryRef, ptr: number, len: number): Buffer {
71
- const m = mem(ref);
72
- if (ptr < 0 || len < 0 || ptr + len > m.length)
73
- throw new Error(`data read out of bounds: ptr=${String(ptr)} len=${String(len)}`);
74
- return Buffer.from(m.subarray(ptr, ptr + len));
75
- }
76
-
77
- /** Read + length-cap a KEY. The edge traps a key over MAX_KEY on EVERY op (via
78
- * `bound` in prepare_key); enforce it uniformly here so a dev read of an over-cap
79
- * key fails the same way instead of silently succeeding. */
80
- function readKey(ref: MemoryRef, ptr: number, len: number): Buffer {
81
- if (len > MAX_KEY) throw new Error('data: key too long');
82
- return readCopy(ref, ptr, len);
83
- }
84
-
85
- function storeKey(collection: string, key: Buffer): string {
86
- return collection + '\0' + key.toString('latin1');
87
- }
88
-
89
- type RecordOutcome =
90
- | { kind: 'unit' }
91
- | { kind: 'value'; value: Buffer; schemaVersion: number }
92
- | { kind: 'absent' }
93
- | { kind: 'already_exists' }
94
- | { kind: 'not_found' }
95
- | { kind: 'conflict' };
96
-
97
- interface RecordIdemRow {
98
- requestHash: string;
99
- outcome: RecordOutcome | null;
100
- }
101
-
102
- function readIdem(ref: MemoryRef, ptr: number): Buffer | null {
103
- if (ptr === 0) return null;
104
- return readCopy(ref, ptr, 16);
105
- }
106
-
107
- function u64le(n: number): Buffer {
108
- const b = Buffer.allocUnsafe(8);
109
- b.writeBigUInt64LE(BigInt(n));
110
- return b;
111
- }
112
-
113
- const RECORD_OP_CREATE = 4;
114
- const RECORD_OP_PATCH = 5;
115
- const RECORD_OP_DELETE = 6;
116
- const RECORD_OP_GET_DELETE = 7;
117
- const RECORD_OP_ENQUEUE = 8;
118
-
119
- function recordRequestHash(op: number, key: Buffer, value: Buffer): string {
120
- return createHash('sha256')
121
- .update('toildb/record-idempotency/request/v1')
122
- .update(Buffer.from([op]))
123
- .update(u64le(key.length))
124
- .update(key)
125
- .update(u64le(value.length))
126
- .update(value)
127
- .digest('hex');
128
- }
129
-
130
- function idemKey(coll: DevCollectionHandle, key: Buffer, op: string, idem: Buffer): string {
131
- return `${storeKey(coll.name, key)}\0${op}\0${idem.toString('hex')}`;
132
- }
133
-
134
- function reservationIdFromIdem(coll: DevCollectionHandle, key: Buffer, idem: Buffer): bigint {
135
- const digest = createHash('sha256')
136
- .update('toildb/capacity-reservation-id/v1')
137
- .update(coll.name)
138
- .update('\0')
139
- .update(key)
140
- .update(idem)
141
- .digest();
142
- return digest.readBigUInt64LE(0) | (1n << 63n);
143
- }
144
-
145
- function snapshotOutcome(outcome: RecordOutcome): RecordOutcomeSnapshot {
146
- switch (outcome.kind) {
147
- case 'value':
148
- return {
149
- kind: 'value',
150
- v: outcome.value.toString('base64'),
151
- sv: outcome.schemaVersion,
152
- };
153
- default:
154
- return { kind: outcome.kind };
155
- }
156
- }
157
-
158
- function loadOutcome(outcome: RecordOutcomeSnapshot): RecordOutcome {
159
- if (outcome.kind === 'value') {
160
- return {
161
- kind: 'value',
162
- value: Buffer.from(outcome.v, 'base64'),
163
- schemaVersion: outcome.sv,
164
- };
165
- }
166
- return { kind: outcome.kind };
167
- }
168
-
169
- function collOf(db: DbDevState, handle: number): DevCollectionHandle | null {
170
- return handle >= 0 && handle < db.handles.length ? db.handles[handle] : null;
171
- }
172
-
173
- function collOfFamily(
174
- db: DbDevState,
175
- handle: number,
176
- ...families: CollectionFamily[]
177
- ): DevCollectionHandle | number {
178
- const coll = collOf(db, handle);
179
- if (coll === null) return INVALID_HANDLE;
180
- return families.includes(coll.family) ? coll : OP_NOT_ALLOWED_FOR_FAMILY;
181
- }
182
-
183
- enum DbOp {
184
- Get,
185
- GetMany,
186
- Exists,
187
- Create,
188
- Patch,
189
- Upsert,
190
- Delete,
191
- GetDelete,
192
- Enqueue,
193
- Append,
194
- AppendOnce,
195
- Latest,
196
- CounterGet,
197
- CounterAdd,
198
- MembershipContains,
199
- MembershipAdd,
200
- MembershipRemove,
201
- MembershipList,
202
- UniqueLookup,
203
- UniqueClaim,
204
- UniqueRelease,
205
- CapacityAvailable,
206
- CapacityReserve,
207
- CapacityConfirm,
208
- CapacityCancel,
209
- ViewGet,
210
- ViewPublish,
211
- CapacitySetTotal,
212
- EventsSince,
213
- }
214
-
215
- /** `data.upsert` return tags (ABI.md), mirroring the guest `UpsertResult` enum. */
216
- enum UpsertResult {
217
- Created = 0,
218
- Updated = 1,
219
- Conflict = 2,
220
- }
221
-
222
- function isReadOp(op: DbOp): boolean {
223
- return (
224
- op === DbOp.Get ||
225
- op === DbOp.GetMany ||
226
- op === DbOp.Exists ||
227
- op === DbOp.ViewGet ||
228
- op === DbOp.CounterGet ||
229
- op === DbOp.MembershipContains ||
230
- op === DbOp.MembershipList ||
231
- op === DbOp.UniqueLookup ||
232
- op === DbOp.Latest ||
233
- op === DbOp.EventsSince ||
234
- op === DbOp.CapacityAvailable
235
- );
236
- }
237
-
238
- function isScanOp(op: DbOp): boolean {
239
- return op === DbOp.Latest || op === DbOp.MembershipList || op === DbOp.EventsSince;
240
- }
241
-
242
- function kindAllows(kind: DbFunctionKind, op: DbOp): boolean {
243
- if (kind === DbFunctionKind.Query) return isReadOp(op) && !isScanOp(op);
244
- if (kind === DbFunctionKind.Action) {
245
- return (
246
- (isReadOp(op) && !isScanOp(op)) ||
247
- op === DbOp.Create ||
248
- op === DbOp.Patch ||
249
- op === DbOp.Upsert ||
250
- op === DbOp.Delete ||
251
- op === DbOp.GetDelete ||
252
- op === DbOp.Enqueue ||
253
- op === DbOp.Append ||
254
- op === DbOp.AppendOnce ||
255
- op === DbOp.CounterAdd ||
256
- op === DbOp.MembershipAdd ||
257
- op === DbOp.MembershipRemove ||
258
- op === DbOp.UniqueClaim ||
259
- op === DbOp.UniqueRelease ||
260
- op === DbOp.CapacityReserve ||
261
- op === DbOp.CapacityConfirm ||
262
- op === DbOp.CapacityCancel
263
- );
264
- }
265
- if (kind === DbFunctionKind.Derive)
266
- return (
267
- isReadOp(op) || op === DbOp.ViewPublish || op === DbOp.Append || op === DbOp.CounterAdd
268
- );
269
- if (kind === DbFunctionKind.Job) return true;
270
- return false;
271
- }
272
-
273
- function collForOp(
274
- db: DbDevState,
275
- handle: number,
276
- op: DbOp,
277
- ...families: CollectionFamily[]
278
- ): DevCollectionHandle | number {
279
- const coll = collOfFamily(db, handle, ...families);
280
- if (typeof coll === 'number') return coll;
281
- return kindAllows(db.functionKind, op) ? coll : OP_NOT_ALLOWED_IN_KIND;
282
- }
283
-
284
- type CatalogSeedEntry =
285
- | number
286
- | {
287
- family?: number;
288
- schemaVersion?: number;
289
- replication?: number;
290
- placement?: number;
291
- fillMaxWaitMs?: number;
292
- fillAllowStale?: boolean;
293
- };
294
-
295
- /**
296
- * The single-process dev data store: the seven ToilDB families, their per-row
297
- * schema_versions, the loaded wasm's catalog, and optional on-disk persistence.
298
- * Process-lifetime, shared across dispatches via the module singleton {@link devDb}.
299
- */
300
- export class DevDatabase {
301
- /** Process-lifetime store: `"collection\0keyLatin1"` -> value. Shared across dispatches. */
302
- private readonly store = new Map<string, Buffer>();
303
- /** Record-family idempotency claims/outcomes: collection+key+op+idem -> row. */
304
- private readonly recordIdem = new Map<string, RecordIdemRow>();
305
- /** Unique-claim request idempotency bytes: `"collection\0key"` -> hex idem. */
306
- private readonly uniqueIdem = new Map<string, string>();
307
- /** View family: `"collection\0key"` -> the latest published view blob. */
308
- private readonly views = new Map<string, Buffer>();
309
- /** Membership family: `"collection\0setKey"` -> (memberLatin1 -> member bytes). */
310
- private readonly members = new Map<string, Map<string, Buffer>>();
311
- /** Counter family: `"collection\0key"` -> saturating i64 sum of deltas. */
312
- private readonly counters = new Map<string, bigint>();
313
- /** Counter idempotency: collection+key+idem -> original delta. */
314
- private readonly counterIdem = new Map<string, bigint>();
315
- /** Events family: `"collection\0key"` -> append-ordered event blobs (oldest first). */
316
- private readonly events = new Map<string, Buffer[]>();
317
- /** Durable resumable `events.since` cursors, keyed `"<deriveId>\0<storeKey>"` -> count of events folded.
318
- * A derive run stages its advances in `db.pendingCheckpoints` and only commits them here after its
319
- * `derive_run` returns (publish-then-persist, mirroring the edge), so a trapped fold never advances the
320
- * cursor past unpublished events. Persisted in the snapshot so a restart resumes the fold. */
321
- private readonly deriveCheckpoints = new Map<string, number>();
322
- /** append_once dedup: `"collection\0key"` -> set of eventIds already appended. */
323
- private readonly eventDedup = new Map<string, Set<string>>();
324
- /** Capacity family: `"collection\0key"` -> an escrow ledger (ceiling + reservations). */
325
- private readonly capacity = new Map<string, CapLedger>();
326
- /** `"collection\0key"` -> the schema_version the record/view/unique-owner was last
327
- * written under (single-value families; the edge stores it per StoredValue). */
328
- private readonly versions = new Map<string, number>();
329
- /** Per-event schema_version, parallel to `events[sk]` (append order). */
330
- private readonly eventVersions = new Map<string, number[]>();
331
- /** Per-member schema_version: `sk` -> (memberLatin1 -> version), parallel to `members`. */
332
- private readonly memberVersions = new Map<string, Map<string, number>>();
333
- /** The decoded catalog from the loaded wasm, including family + current schema_version. */
334
- private catalog: DbCatalogState = { kind: 'no-section' };
335
-
336
- // ---- on-disk persistence: dev data + its versions survive restarts, so a
337
- // developer can write rows, evolve a @data type, restart, and watch the @migrate
338
- // run. Delete the file to reset the dev database. JSON with base64 buffers.
339
- private persistPath: string | null = null;
340
-
341
- /** (Re)load the catalog capability metadata from a server wasm. The module
342
- * loader calls this on every (re)compile so writes stamp the live version. */
343
- setCatalog(wasm: Buffer): void {
344
- this.catalog = parseCatalog(wasm);
345
- }
346
-
347
- private currentSchemaVersion(coll: DevCollectionHandle): number {
348
- if (this.catalog.kind !== 'present') return coll.schemaVersion;
349
- return this.catalog.collections.get(coll.name)?.schemaVersion ?? coll.schemaVersion;
350
- }
351
-
352
- private stampVersion(coll: DevCollectionHandle, sk: string): void {
353
- this.versions.set(sk, this.currentSchemaVersion(coll)); // stamp the value type's current version
354
- }
355
-
356
- private recordIdemStart(
357
- coll: DevCollectionHandle,
358
- key: Buffer,
359
- op: string,
360
- idem: Buffer | null,
361
- requestHash: string,
362
- ): { fresh: true } | { fresh: false; status: number; outcome?: RecordOutcome } {
363
- if (idem === null) return { fresh: true };
364
- const ik = idemKey(coll, key, op, idem);
365
- const row = this.recordIdem.get(ik);
366
- if (row === undefined) {
367
- this.recordIdem.set(ik, { requestHash, outcome: null });
368
- return { fresh: true };
369
- }
370
- if (row.requestHash !== requestHash) return { fresh: false, status: CONFLICT };
371
- if (row.outcome === null) return { fresh: false, status: UNAVAILABLE };
372
- return { fresh: false, status: 0, outcome: row.outcome };
373
- }
374
-
375
- private recordIdemFinish(
376
- coll: DevCollectionHandle,
377
- key: Buffer,
378
- op: string,
379
- idem: Buffer | null,
380
- requestHash: string,
381
- outcome: RecordOutcome,
382
- ): void {
383
- if (idem === null) return;
384
- const ik = idemKey(coll, key, op, idem);
385
- const row = this.recordIdem.get(ik);
386
- if (row === undefined || row.requestHash !== requestHash) return;
387
- if (row.outcome === null) row.outcome = outcome;
388
- }
389
-
390
- private replayRecordOutcome(db: DbDevState, outcome: RecordOutcome): number {
391
- switch (outcome.kind) {
392
- case 'unit':
393
- return 0;
394
- case 'value':
395
- db.lastResult = outcome.value;
396
- db.lastResultVersion = outcome.schemaVersion;
397
- return outcome.value.length;
398
- case 'absent':
399
- case 'not_found':
400
- return ABSENT;
401
- case 'already_exists':
402
- return ALREADY_EXISTS;
403
- case 'conflict':
404
- return CONFLICT;
405
- }
406
- }
407
-
408
- /** Point the dev DB at an on-disk file and load any existing snapshot. Call once at
409
- * dev-server startup. Deleting the file (or the whole `.toil/` dir) resets dev data. */
410
- configurePersistence(filePath: string): void {
411
- this.persistPath = filePath;
412
- this.load();
413
- }
414
-
415
- /** Write the current store to disk (no-op if persistence is not configured). The
416
- * module loader calls this after each dispatch so a crash never loses a write. */
417
- persist(): void {
418
- if (this.persistPath === null) return;
419
- const snap: DbSnapshot = {
420
- store: {},
421
- recordIdem: {},
422
- uniqueIdem: {},
423
- views: {},
424
- members: {},
425
- counters: {},
426
- counterIdem: {},
427
- events: {},
428
- eventDedup: {},
429
- deriveCheckpoints: {},
430
- capacity: {},
431
- };
432
- for (const [k, v] of this.store)
433
- snap.store[k] = { v: v.toString('base64'), sv: this.versions.get(k) ?? 0 };
434
- for (const [k, row] of this.recordIdem)
435
- snap.recordIdem![k] = {
436
- requestHash: row.requestHash,
437
- state: row.outcome === null ? 'pending' : 'done',
438
- outcome: row.outcome === null ? undefined : snapshotOutcome(row.outcome),
439
- };
440
- for (const [k, v] of this.uniqueIdem) snap.uniqueIdem![k] = v;
441
- for (const [k, v] of this.views)
442
- snap.views[k] = { v: v.toString('base64'), sv: this.versions.get(k) ?? 0 };
443
- for (const [k, m] of this.members) {
444
- const o: Record<string, { v: string; sv: number }> = {};
445
- const mv = this.memberVersions.get(k);
446
- for (const [mk, mvb] of m) o[mk] = { v: mvb.toString('base64'), sv: mv?.get(mk) ?? 0 };
447
- snap.members[k] = o;
448
- }
449
- for (const [k, v] of this.counters) snap.counters[k] = v.toString();
450
- for (const [k, v] of this.counterIdem) snap.counterIdem![k] = v.toString();
451
- for (const [k, log] of this.events) {
452
- const ver = this.eventVersions.get(k) ?? [];
453
- snap.events[k] = log.map((b, i) => ({ v: b.toString('base64'), sv: ver[i] ?? 0 }));
454
- }
455
- for (const [k, s] of this.eventDedup) snap.eventDedup[k] = [...s];
456
- for (const [k, v] of this.deriveCheckpoints) snap.deriveCheckpoints![k] = v;
457
- for (const [k, l] of this.capacity)
458
- snap.capacity[k] = {
459
- total: l.total.toString(),
460
- nextId: l.nextId.toString(),
461
- reservations: [...l.reservations].map(([id, r]) => [
462
- id.toString(),
463
- { amount: r.amount.toString(), expiresMs: r.expiresMs, confirmed: r.confirmed },
464
- ]),
465
- };
466
- try {
467
- fs.mkdirSync(path.dirname(this.persistPath), { recursive: true });
468
- // Write to a temp file then rename: atomic on POSIX, so a crash mid-write
469
- // leaves the previous good snapshot intact instead of a truncated file
470
- // that would fail to parse and drop the whole dev database on next load.
471
- const tmp = `${this.persistPath}.${process.pid}.tmp`;
472
- fs.writeFileSync(tmp, JSON.stringify(snap));
473
- fs.renameSync(tmp, this.persistPath);
474
- } catch {
475
- // dev-only best effort; a write failure must never crash a request.
476
- }
477
- }
478
-
479
- private load(): void {
480
- if (this.persistPath === null) return;
481
- let snap: DbSnapshot;
482
- try {
483
- snap = JSON.parse(fs.readFileSync(this.persistPath, 'utf8')) as DbSnapshot;
484
- } catch {
485
- return; // no snapshot yet (or unreadable) - start empty
486
- }
487
- this.clear();
488
- for (const [k, e] of Object.entries(snap.store ?? {})) {
489
- this.store.set(k, Buffer.from(e.v, 'base64'));
490
- this.versions.set(k, e.sv);
491
- }
492
- for (const [k, row] of Object.entries(snap.recordIdem ?? {})) {
493
- this.recordIdem.set(k, {
494
- requestHash: row.requestHash,
495
- outcome:
496
- row.state === 'done' && row.outcome !== undefined
497
- ? loadOutcome(row.outcome)
498
- : null,
499
- });
500
- }
501
- for (const [k, v] of Object.entries(snap.uniqueIdem ?? {})) this.uniqueIdem.set(k, v);
502
- for (const [k, e] of Object.entries(snap.views ?? {})) {
503
- this.views.set(k, Buffer.from(e.v, 'base64'));
504
- this.versions.set(k, e.sv);
505
- }
506
- for (const [k, m] of Object.entries(snap.members ?? {})) {
507
- const map = new Map<string, Buffer>();
508
- const ver = new Map<string, number>();
509
- for (const [mk, e] of Object.entries(m)) {
510
- map.set(mk, Buffer.from(e.v, 'base64'));
511
- ver.set(mk, e.sv);
512
- }
513
- this.members.set(k, map);
514
- this.memberVersions.set(k, ver);
515
- }
516
- for (const [k, v] of Object.entries(snap.counters ?? {})) this.counters.set(k, BigInt(v));
517
- for (const [k, v] of Object.entries(snap.counterIdem ?? {}))
518
- this.counterIdem.set(k, BigInt(v));
519
- for (const [k, log] of Object.entries(snap.events ?? {})) {
520
- this.events.set(
521
- k,
522
- log.map((e) => Buffer.from(e.v, 'base64')),
523
- );
524
- this.eventVersions.set(
525
- k,
526
- log.map((e) => e.sv),
527
- );
528
- }
529
- for (const [k, ids] of Object.entries(snap.eventDedup ?? {}))
530
- this.eventDedup.set(k, new Set(ids));
531
- for (const [k, v] of Object.entries(snap.deriveCheckpoints ?? {}))
532
- this.deriveCheckpoints.set(k, v);
533
- for (const [k, l] of Object.entries(snap.capacity ?? {})) {
534
- const res = new Map<bigint, Reservation>();
535
- for (const [id, r] of l.reservations)
536
- res.set(BigInt(id), {
537
- amount: BigInt(r.amount),
538
- expiresMs: r.expiresMs,
539
- confirmed: r.confirmed,
540
- });
541
- this.capacity.set(k, {
542
- total: BigInt(l.total),
543
- nextId: BigInt(l.nextId),
544
- reservations: res,
545
- });
546
- }
547
- }
548
-
549
- private clear(): void {
550
- this.store.clear();
551
- this.recordIdem.clear();
552
- this.uniqueIdem.clear();
553
- this.versions.clear();
554
- this.views.clear();
555
- this.members.clear();
556
- this.memberVersions.clear();
557
- this.counters.clear();
558
- this.counterIdem.clear();
559
- this.eventVersions.clear();
560
- this.events.clear();
561
- this.eventDedup.clear();
562
- this.deriveCheckpoints.clear();
563
- this.capacity.clear();
564
- }
565
-
566
- private capLedger(sk: string): CapLedger {
567
- let l = this.capacity.get(sk);
568
- if (l === undefined) {
569
- l = { total: 0n, reservations: new Map(), nextId: 1n };
570
- this.capacity.set(sk, l);
571
- }
572
- return l;
573
- }
574
-
575
- /** Drop UN-confirmed reservations whose TTL elapsed (a confirmed sale never expires). */
576
- private capPrune(l: CapLedger, nowMs: number): void {
577
- for (const [id, r] of l.reservations)
578
- if (!r.confirmed && r.expiresMs <= nowMs) l.reservations.delete(id);
579
- }
580
-
581
- /** Units reserved against the ceiling: held (un-expired) + confirmed (call capPrune first). */
582
- private capReserved(l: CapLedger): bigint {
583
- let sum = 0n;
584
- for (const r of l.reservations.values()) sum += r.amount;
585
- return sum;
586
- }
587
-
588
- // --- record family (resolve / get / get_many / exists / create / patch /
589
- // delete / get_delete) ---
590
-
591
- resolveCollection(
592
- ref: MemoryRef,
593
- db: DbDevState,
594
- namePtr: number,
595
- nameLen: number,
596
- outHandlePtr: number,
597
- ): number {
598
- if (nameLen < 0 || nameLen > MAX_NAME) throw new Error('data: collection name too long');
599
- const name = readCopy(ref, namePtr, nameLen).toString('utf8');
600
- let coll: DevCollectionHandle;
601
- switch (this.catalog.kind) {
602
- case 'present': {
603
- const found = this.catalog.collections.get(name);
604
- if (found === undefined) return SCHEMA_UNAVAILABLE;
605
- coll = { ...found };
606
- break;
607
- }
608
- case 'malformed':
609
- return SCHEMA_UNAVAILABLE;
610
- case 'no-section':
611
- coll = {
612
- name,
613
- family: CollectionFamily.Record,
614
- schemaVersion: 0,
615
- replication: 0,
616
- placement: 0,
617
- fillMaxWaitMs: DEFAULT_FILL_WAIT_MS,
618
- fillAllowStale: true,
619
- };
620
- break;
621
- }
622
- const handle = db.handles.length;
623
- db.handles.push(coll);
624
- const m = mem(ref);
625
- if (outHandlePtr < 0 || outHandlePtr + 4 > m.length)
626
- throw new Error('data: resolve out-handle out of bounds');
627
- m.writeUInt32LE(handle, outHandlePtr);
628
- return 0;
629
- }
630
-
631
- get(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number {
632
- const coll = collForOp(db, handle, DbOp.Get, CollectionFamily.Record);
633
- if (typeof coll === 'number') return coll;
634
- if (keyLen > MAX_KEY) throw new Error('data: key too long');
635
- const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
636
- const v = this.store.get(sk);
637
- if (v === undefined) return ABSENT;
638
- db.lastResult = v;
639
- db.lastResultVersion = this.versions.get(sk) ?? 0; // surface the row's stored version
640
- return v.length;
641
- }
642
-
643
- // Bounded multi-get. Keys blob: u32 count + per key (u32 len + bytes).
644
- // Result (stashed): u32 count + per item u8 present (+ u32 len + bytes),
645
- // in request order. Mirrors the edge `op_get_many` framing byte-for-byte.
646
- getMany(
647
- ref: MemoryRef,
648
- db: DbDevState,
649
- handle: number,
650
- keysPtr: number,
651
- keysLen: number,
652
- ): number {
653
- const coll = collForOp(
654
- db,
655
- handle,
656
- DbOp.GetMany,
657
- CollectionFamily.Record,
658
- CollectionFamily.View,
659
- );
660
- if (typeof coll === 'number') return coll;
661
- if (keysLen > MAX_VALUE) throw new Error('data: keys blob too large');
662
- const table = coll.family === CollectionFamily.View ? this.views : this.store;
663
- // Keys blob: u32 count, then per key a u32-length-prefixed blob. The shared
664
- // DataReader is bounds-safe (empty past end), so a malformed/truncated blob
665
- // can't over-read; cap each key at MAX_KEY like the edge's prepare_key.
666
- const r = new DataReader(readCopy(ref, keysPtr, keysLen));
667
- const count = r.readU32();
668
- if (count > 1024) return TOO_MANY_KEYS; // anti-OOM cap, mirrors the edge
669
- // Result: u32 count, then per item present(u8) + (when present) the row's
670
- // stored schema_version (u32, per-item @migrate dispatch) + value (u32 len +
671
- // bytes). Byte-identical to the edge op_get_many framing.
672
- const w = new DataWriter();
673
- w.writeU32(count);
674
- for (let i = 0; i < count; i++) {
675
- const key = r.readBytes();
676
- if (key.length > MAX_KEY) throw new Error('data: key too long');
677
- const sk = storeKey(coll.name, Buffer.from(key));
678
- const v = table.get(sk);
679
- if (v === undefined) {
680
- w.writeU8(0);
681
- } else {
682
- w.writeU8(1)
683
- .writeU32(this.versions.get(sk) ?? 0)
684
- .writeBytes(v);
685
- }
686
- }
687
- db.lastResult = Buffer.from(w.toBytes());
688
- return db.lastResult.length;
689
- }
690
-
691
- exists(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number {
692
- const coll = collForOp(db, handle, DbOp.Exists, CollectionFamily.Record);
693
- if (typeof coll === 'number') return coll;
694
- return this.store.has(storeKey(coll.name, readKey(ref, keyPtr, keyLen))) ? 1 : 0;
695
- }
696
-
697
- /** Each `@unique` field's raw value in an encoded record, per the collection's
698
- * field layout. `[]` when there is no layout or no `@unique` field. The
699
- * compiler restricts `@unique` to top-level scalar/blob records, so the walker
700
- * only skips fixed scalars and length-prefixed blobs. */
701
- private uniqueValuesOf(coll: DevCollectionHandle, value: Buffer): { index: number; val: Buffer }[] {
702
- const fields = coll.fields;
703
- if (!fields || !fields.some((f) => f.unique)) return [];
704
- const r = new DataReader(value);
705
- r.readU32(); // skip the @data message-boundary id that encode() writes before the fields
706
- const out: { index: number; val: Buffer }[] = [];
707
- for (let i = 0; i < fields.length; i++) {
708
- const f = fields[i];
709
- if (f.isArray) break;
710
- if (f.typeName === 'string') {
711
- const s = r.readString();
712
- if (f.unique) out.push({ index: i, val: Buffer.from(s, 'utf8') });
713
- } else if (f.typeName === 'Uint8Array') {
714
- const b = r.readBytes();
715
- if (f.unique) out.push({ index: i, val: Buffer.from(b) });
716
- } else {
717
- switch (f.typeName) {
718
- case 'bool':
719
- case 'u8':
720
- case 'i8':
721
- r.readU8();
722
- break;
723
- case 'u16':
724
- case 'i16':
725
- r.readU16();
726
- break;
727
- case 'u32':
728
- case 'i32':
729
- case 'f32':
730
- r.readU32();
731
- break;
732
- case 'u64':
733
- case 'i64':
734
- case 'f64':
735
- r.readU64();
736
- break;
737
- default:
738
- return out; // nested @data — compiler forbids @unique on/after it
739
- }
740
- }
741
- }
742
- return out;
743
- }
744
-
745
- /** True when another record in `coll` already holds one of `value`'s `@unique`
746
- * field values (per-tenant uniqueness). `selfSk` is the writing record's store
747
- * key so it never conflicts with itself; empty values are unique-if-present. */
748
- private uniqueConflict(coll: DevCollectionHandle, value: Buffer, selfSk: string): boolean {
749
- const uvals = this.uniqueValuesOf(coll, value).filter((u) => u.val.length > 0);
750
- if (uvals.length === 0) return false;
751
- const prefix = coll.name + '\0';
752
- for (const [otherSk, otherVal] of this.store) {
753
- if (otherSk === selfSk || !otherSk.startsWith(prefix)) continue;
754
- const otherUvals = this.uniqueValuesOf(coll, otherVal);
755
- for (const u of uvals) {
756
- if (otherUvals.some((o) => o.index === u.index && o.val.equals(u.val))) return true;
757
- }
758
- }
759
- return false;
760
- }
761
-
762
- create(
763
- ref: MemoryRef,
764
- db: DbDevState,
765
- handle: number,
766
- keyPtr: number,
767
- keyLen: number,
768
- valPtr: number,
769
- valLen: number,
770
- idemPtr: number,
771
- ): number {
772
- const coll = collForOp(db, handle, DbOp.Create, CollectionFamily.Record);
773
- if (typeof coll === 'number') return coll;
774
- if (keyLen > MAX_KEY || valLen > MAX_VALUE) throw new Error('data: key/value too large');
775
- const key = readKey(ref, keyPtr, keyLen);
776
- const value = readCopy(ref, valPtr, valLen);
777
- const idem = readIdem(ref, idemPtr);
778
- const requestHash = recordRequestHash(RECORD_OP_CREATE, key, value);
779
- const start = this.recordIdemStart(coll, key, 'C', idem, requestHash);
780
- if (!start.fresh)
781
- return start.outcome ? this.replayRecordOutcome(db, start.outcome) : start.status;
782
- const sk = storeKey(coll.name, key);
783
- // @unique: a value already held by another record blocks the create, exactly
784
- // like the edge's op_create claim (surfaced as AlreadyExists -> create false).
785
- const outcome: RecordOutcome =
786
- this.store.has(sk) || this.uniqueConflict(coll, value, sk)
787
- ? { kind: 'already_exists' }
788
- : { kind: 'unit' };
789
- if (outcome.kind === 'unit') {
790
- this.store.set(sk, value);
791
- this.stampVersion(coll, sk); // stamp the value type's current schema version
792
- this.recordWrite(db, coll);
793
- }
794
- this.recordIdemFinish(coll, key, 'C', idem, requestHash, outcome);
795
- return this.replayRecordOutcome(db, outcome);
796
- }
797
-
798
- patch(
799
- ref: MemoryRef,
800
- db: DbDevState,
801
- handle: number,
802
- keyPtr: number,
803
- keyLen: number,
804
- patchPtr: number,
805
- patchLen: number,
806
- idemPtr: number,
807
- ): number {
808
- const coll = collForOp(db, handle, DbOp.Patch, CollectionFamily.Record);
809
- if (typeof coll === 'number') return coll;
810
- if (keyLen > MAX_KEY || patchLen > MAX_VALUE) throw new Error('data: key/patch too large');
811
- const key = readKey(ref, keyPtr, keyLen);
812
- const v = readCopy(ref, patchPtr, patchLen);
813
- const idem = readIdem(ref, idemPtr);
814
- const requestHash = recordRequestHash(RECORD_OP_PATCH, key, v);
815
- const start = this.recordIdemStart(coll, key, 'P', idem, requestHash);
816
- if (!start.fresh)
817
- return start.outcome ? this.replayRecordOutcome(db, start.outcome) : start.status;
818
- const sk = storeKey(coll.name, key);
819
- const outcome: RecordOutcome = this.store.has(sk)
820
- ? this.uniqueConflict(coll, v, sk)
821
- ? { kind: 'already_exists' } // patch would collide a @unique field
822
- : { kind: 'value', value: v, schemaVersion: this.currentSchemaVersion(coll) }
823
- : { kind: 'not_found' };
824
- if (outcome.kind === 'value') {
825
- this.store.set(sk, v);
826
- this.stampVersion(coll, sk); // a patch rewrites the row at the current version
827
- this.recordWrite(db, coll);
828
- }
829
- this.recordIdemFinish(coll, key, 'P', idem, requestHash, outcome);
830
- return this.replayRecordOutcome(db, outcome);
831
- }
832
-
833
- // Create-or-overwrite in ONE op: a blind last-writer-wins put. Returns
834
- // UPSERT_CREATED (0) / UPSERT_UPDATED (1), or UPSERT_CONFLICT (2) when a
835
- // @unique value is held by ANOTHER record (nothing written). A blind put is
836
- // naturally idempotent (re-applying the same value is a no-op beyond the
837
- // Created/Updated tag), so `idem` needs no dedup ledger; it is accepted for
838
- // ABI parity and ignored, mirroring how the edge merely threads it into the
839
- // oplog env.
840
- upsert(
841
- ref: MemoryRef,
842
- db: DbDevState,
843
- handle: number,
844
- keyPtr: number,
845
- keyLen: number,
846
- valPtr: number,
847
- valLen: number,
848
- _idemPtr: number,
849
- ): number {
850
- const coll = collForOp(db, handle, DbOp.Upsert, CollectionFamily.Record);
851
- if (typeof coll === 'number') return coll;
852
- if (keyLen > MAX_KEY || valLen > MAX_VALUE) throw new Error('data: key/value too large');
853
- const key = readKey(ref, keyPtr, keyLen);
854
- const value = readCopy(ref, valPtr, valLen);
855
- const sk = storeKey(coll.name, key);
856
- // @unique: a value already held by ANOTHER record blocks the upsert (the
857
- // one outcome a blind put cannot auto-resolve), exactly like the edge op.
858
- if (this.uniqueConflict(coll, value, sk)) return UpsertResult.Conflict;
859
- const existed = this.store.has(sk);
860
- this.store.set(sk, value);
861
- this.stampVersion(coll, sk); // stamp the value type's current schema version
862
- this.recordWrite(db, coll);
863
- return existed ? UpsertResult.Updated : UpsertResult.Created;
864
- }
865
-
866
- delete(
867
- ref: MemoryRef,
868
- db: DbDevState,
869
- handle: number,
870
- keyPtr: number,
871
- keyLen: number,
872
- idemPtr: number,
873
- ): number {
874
- const coll = collForOp(db, handle, DbOp.Delete, CollectionFamily.Record);
875
- if (typeof coll === 'number') return coll;
876
- const key = readKey(ref, keyPtr, keyLen);
877
- const idem = readIdem(ref, idemPtr);
878
- const requestHash = recordRequestHash(RECORD_OP_DELETE, key, Buffer.alloc(0));
879
- const start = this.recordIdemStart(coll, key, 'D', idem, requestHash);
880
- if (!start.fresh)
881
- return start.outcome ? this.replayRecordOutcome(db, start.outcome) : start.status;
882
- const sk = storeKey(coll.name, key);
883
- this.store.delete(sk);
884
- this.versions.delete(sk);
885
- const outcome: RecordOutcome = { kind: 'unit' };
886
- this.recordIdemFinish(coll, key, 'D', idem, requestHash, outcome);
887
- return this.replayRecordOutcome(db, outcome);
888
- }
889
-
890
- // Atomic fetch-and-delete (consume-once); deletes only on a real read.
891
- getDelete(
892
- ref: MemoryRef,
893
- db: DbDevState,
894
- handle: number,
895
- keyPtr: number,
896
- keyLen: number,
897
- idemPtr: number,
898
- ): number {
899
- const coll = collForOp(db, handle, DbOp.GetDelete, CollectionFamily.Record);
900
- if (typeof coll === 'number') return coll;
901
- const key = readKey(ref, keyPtr, keyLen);
902
- const idem = readIdem(ref, idemPtr);
903
- const requestHash = recordRequestHash(RECORD_OP_GET_DELETE, key, Buffer.alloc(0));
904
- const start = this.recordIdemStart(coll, key, 'G', idem, requestHash);
905
- if (!start.fresh)
906
- return start.outcome ? this.replayRecordOutcome(db, start.outcome) : start.status;
907
- const sk = storeKey(coll.name, key);
908
- const v = this.store.get(sk);
909
- const outcome: RecordOutcome =
910
- v === undefined
911
- ? { kind: 'absent' }
912
- : {
913
- kind: 'value',
914
- value: v,
915
- schemaVersion: this.versions.get(sk) ?? 0,
916
- };
917
- if (outcome.kind === 'value') {
918
- this.store.delete(sk);
919
- this.versions.delete(sk);
920
- }
921
- this.recordIdemFinish(coll, key, 'G', idem, requestHash, outcome);
922
- return this.replayRecordOutcome(db, outcome);
923
- }
924
-
925
- // --- unique family (lookup / claim / release) ---
926
-
927
- uniqueLookup(
928
- ref: MemoryRef,
929
- db: DbDevState,
930
- handle: number,
931
- keyPtr: number,
932
- keyLen: number,
933
- ): number {
934
- const coll = collForOp(db, handle, DbOp.UniqueLookup, CollectionFamily.Unique);
935
- if (typeof coll === 'number') return coll;
936
- const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
937
- const v = this.store.get(sk);
938
- if (v === undefined) return ABSENT;
939
- db.lastResult = v;
940
- db.lastResultVersion = this.versions.get(sk) ?? 0;
941
- return v.length;
942
- }
943
-
944
- // Tag: 0 Claimed, 1 AlreadyClaimed (owner stashed), 2 AlreadyOwnedByCaller.
945
- uniqueClaim(
946
- ref: MemoryRef,
947
- db: DbDevState,
948
- handle: number,
949
- keyPtr: number,
950
- keyLen: number,
951
- valPtr: number,
952
- valLen: number,
953
- idemPtr: number,
954
- ): number {
955
- const coll = collForOp(db, handle, DbOp.UniqueClaim, CollectionFamily.Unique);
956
- if (typeof coll === 'number') return coll;
957
- if (keyLen > MAX_KEY || valLen > MAX_VALUE) throw new Error('data: key/value too large');
958
- const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
959
- const owner = readCopy(ref, valPtr, valLen);
960
- const idem = readIdem(ref, idemPtr)?.toString('hex') ?? '';
961
- const existing = this.store.get(sk);
962
- if (existing === undefined) {
963
- this.store.set(sk, owner);
964
- this.uniqueIdem.set(sk, idem);
965
- this.stampVersion(coll, sk);
966
- return 0; // Claimed
967
- }
968
- if (existing.equals(owner)) {
969
- return (this.uniqueIdem.get(sk) ?? '') === idem ? 2 : CONFLICT;
970
- }
971
- db.lastResult = existing;
972
- return 1; // AlreadyClaimed (current owner stashed)
973
- }
974
-
975
- uniqueRelease(
976
- ref: MemoryRef,
977
- db: DbDevState,
978
- handle: number,
979
- keyPtr: number,
980
- keyLen: number,
981
- valPtr: number,
982
- valLen: number,
983
- ): number {
984
- const coll = collForOp(db, handle, DbOp.UniqueRelease, CollectionFamily.Unique);
985
- if (typeof coll === 'number') return coll;
986
- const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
987
- const existing = this.store.get(sk);
988
- if (existing === undefined) return 0; // idempotent
989
- if (!existing.equals(readCopy(ref, valPtr, valLen))) return CONFLICT; // not the owner
990
- this.store.delete(sk);
991
- this.uniqueIdem.delete(sk);
992
- this.versions.delete(sk);
993
- return 0;
994
- }
995
-
996
- // --- membership family (contains / add / remove / list) ---
997
-
998
- membershipContains(
999
- ref: MemoryRef,
1000
- db: DbDevState,
1001
- handle: number,
1002
- setPtr: number,
1003
- setLen: number,
1004
- memberPtr: number,
1005
- memberLen: number,
1006
- ): number {
1007
- const coll = collForOp(db, handle, DbOp.MembershipContains, CollectionFamily.Membership);
1008
- if (typeof coll === 'number') return coll;
1009
- const set = this.members.get(storeKey(coll.name, readKey(ref, setPtr, setLen)));
1010
- if (set === undefined) return 0;
1011
- return set.has(readCopy(ref, memberPtr, memberLen).toString('latin1')) ? 1 : 0;
1012
- }
1013
-
1014
- membershipAdd(
1015
- ref: MemoryRef,
1016
- db: DbDevState,
1017
- handle: number,
1018
- setPtr: number,
1019
- setLen: number,
1020
- memberPtr: number,
1021
- memberLen: number,
1022
- ): number {
1023
- const coll = collForOp(db, handle, DbOp.MembershipAdd, CollectionFamily.Membership);
1024
- if (typeof coll === 'number') return coll;
1025
- if (setLen > MAX_KEY || memberLen > MAX_VALUE)
1026
- throw new Error('data: set/member too large');
1027
- const sk = storeKey(coll.name, readKey(ref, setPtr, setLen));
1028
- const member = readCopy(ref, memberPtr, memberLen);
1029
- let set = this.members.get(sk);
1030
- if (set === undefined) {
1031
- set = new Map();
1032
- this.members.set(sk, set);
1033
- }
1034
- const ml = member.toString('latin1');
1035
- set.set(ml, member);
1036
- let mv = this.memberVersions.get(sk);
1037
- if (mv === undefined) {
1038
- mv = new Map();
1039
- this.memberVersions.set(sk, mv);
1040
- }
1041
- mv.set(ml, this.currentSchemaVersion(coll));
1042
- return 0;
1043
- }
1044
-
1045
- membershipRemove(
1046
- ref: MemoryRef,
1047
- db: DbDevState,
1048
- handle: number,
1049
- setPtr: number,
1050
- setLen: number,
1051
- memberPtr: number,
1052
- memberLen: number,
1053
- ): number {
1054
- const coll = collForOp(db, handle, DbOp.MembershipRemove, CollectionFamily.Membership);
1055
- if (typeof coll === 'number') return coll;
1056
- const sk = storeKey(coll.name, readKey(ref, setPtr, setLen));
1057
- const ml = readCopy(ref, memberPtr, memberLen).toString('latin1');
1058
- this.members.get(sk)?.delete(ml);
1059
- this.memberVersions.get(sk)?.delete(ml);
1060
- return 0;
1061
- }
1062
-
1063
- // Frame the members (sorted by bytes, matching the edge BTreeMap) as
1064
- // `u32 count` + per member `u32 len + bytes`; stash + return the length.
1065
- membershipList(
1066
- ref: MemoryRef,
1067
- db: DbDevState,
1068
- handle: number,
1069
- setPtr: number,
1070
- setLen: number,
1071
- limit: number,
1072
- ): number {
1073
- const coll = collForOp(db, handle, DbOp.MembershipList, CollectionFamily.Membership);
1074
- if (typeof coll === 'number') return coll;
1075
- const sk = storeKey(coll.name, readKey(ref, setPtr, setLen));
1076
- const set = this.members.get(sk);
1077
- const mv = this.memberVersions.get(sk);
1078
- const n = Math.max(0, Math.min(limit, 0xffff));
1079
- const members =
1080
- set === undefined ? [] : Array.from(set.values()).sort(Buffer.compare).slice(0, n);
1081
- // u32 count, then per member its stored schema_version (u32) + bytes (u32
1082
- // len + bytes). Same framing as the edge op_membership_list.
1083
- const w = new DataWriter();
1084
- w.writeU32(members.length);
1085
- for (const m of members) {
1086
- w.writeU32(mv?.get(m.toString('latin1')) ?? 0).writeBytes(m);
1087
- }
1088
- db.lastResult = Buffer.from(w.toBytes());
1089
- return db.lastResult.length;
1090
- }
1091
-
1092
- // --- view family (get / publish) ---
1093
-
1094
- viewGet(
1095
- ref: MemoryRef,
1096
- db: DbDevState,
1097
- handle: number,
1098
- keyPtr: number,
1099
- keyLen: number,
1100
- ): number {
1101
- const coll = collForOp(db, handle, DbOp.ViewGet, CollectionFamily.View);
1102
- if (typeof coll === 'number') return coll;
1103
- const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
1104
- const v = this.views.get(sk);
1105
- if (v === undefined) return ABSENT;
1106
- db.lastResult = v;
1107
- db.lastResultVersion = this.versions.get(sk) ?? 0;
1108
- return v.length;
1109
- }
1110
-
1111
- // Publish overwrites (the host assigns the version; dev keeps the latest).
1112
- viewPublish(
1113
- ref: MemoryRef,
1114
- db: DbDevState,
1115
- handle: number,
1116
- keyPtr: number,
1117
- keyLen: number,
1118
- valPtr: number,
1119
- valLen: number,
1120
- ): number {
1121
- const coll = collForOp(db, handle, DbOp.ViewPublish, CollectionFamily.View);
1122
- if (typeof coll === 'number') return coll;
1123
- if (keyLen > MAX_KEY || valLen > MAX_VALUE) throw new Error('data: key/view too large');
1124
- const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
1125
- this.views.set(sk, readCopy(ref, valPtr, valLen));
1126
- this.stampVersion(coll, sk);
1127
- return 0;
1128
- }
1129
-
1130
- // --- counter family (get / add) ---
1131
-
1132
- // Stash the i64 sum as 8 LE bytes; the guest pulls + loads it. A counter
1133
- // with no deltas reads as 0 (never absent).
1134
- counterGet(
1135
- ref: MemoryRef,
1136
- db: DbDevState,
1137
- handle: number,
1138
- keyPtr: number,
1139
- keyLen: number,
1140
- ): number {
1141
- const coll = collForOp(db, handle, DbOp.CounterGet, CollectionFamily.Counter);
1142
- if (typeof coll === 'number') return coll;
1143
- const sum = this.counters.get(storeKey(coll.name, readKey(ref, keyPtr, keyLen))) ?? 0n;
1144
- const out = Buffer.alloc(8);
1145
- out.writeBigInt64LE(sum);
1146
- db.lastResult = out;
1147
- return out.length;
1148
- }
1149
-
1150
- // `delta` is the wasm i64 (a BigInt across the boundary); `BigInt()`
1151
- // normalizes the test's plain-number form too. Saturates like the edge.
1152
- /** Note a successful write to a SOURCE collection, so the runtime can re-run
1153
- * the `@derive` materializers that depend on it after this dispatch. A
1154
- * derive's OWN writes run under FunctionKind=Derive and must never
1155
- * re-trigger a derive (which would loop), so they are never recorded. */
1156
- private recordWrite(db: DbDevState, coll: DevCollectionHandle): void {
1157
- if (db.functionKind === DbFunctionKind.Derive) return;
1158
- db.writtenCollections.add(coll.name);
1159
- }
1160
-
1161
- counterAdd(
1162
- ref: MemoryRef,
1163
- db: DbDevState,
1164
- handle: number,
1165
- keyPtr: number,
1166
- keyLen: number,
1167
- delta: number | bigint,
1168
- idemPtr: number,
1169
- ): number {
1170
- const coll = collForOp(db, handle, DbOp.CounterAdd, CollectionFamily.Counter);
1171
- if (typeof coll === 'number') return coll;
1172
- const key = readKey(ref, keyPtr, keyLen);
1173
- const idem = readIdem(ref, idemPtr);
1174
- const d = BigInt(delta);
1175
- if (idem !== null) {
1176
- const ik = idemKey(coll, key, 'A', idem);
1177
- const seen = this.counterIdem.get(ik);
1178
- if (seen !== undefined) return seen === d ? 0 : CONFLICT;
1179
- this.counterIdem.set(ik, d);
1180
- }
1181
- const sk = storeKey(coll.name, key);
1182
- this.counters.set(sk, satI64((this.counters.get(sk) ?? 0n) + d));
1183
- this.recordWrite(db, coll);
1184
- return 0;
1185
- }
1186
-
1187
- // --- events family (append / append_once / enqueue / latest) ---
1188
-
1189
- append(
1190
- ref: MemoryRef,
1191
- db: DbDevState,
1192
- handle: number,
1193
- keyPtr: number,
1194
- keyLen: number,
1195
- evPtr: number,
1196
- evLen: number,
1197
- idemPtr: number,
1198
- ): number {
1199
- const coll = collForOp(db, handle, DbOp.Append, CollectionFamily.Events);
1200
- if (typeof coll === 'number') return coll;
1201
- if (keyLen > MAX_KEY || evLen > MAX_VALUE) throw new Error('data: key/event too large');
1202
- const key = readKey(ref, keyPtr, keyLen);
1203
- const sk = storeKey(coll.name, key);
1204
- const idem = readIdem(ref, idemPtr);
1205
- if (idem !== null) {
1206
- let seen = this.eventDedup.get(sk);
1207
- if (seen === undefined) {
1208
- seen = new Set();
1209
- this.eventDedup.set(sk, seen);
1210
- }
1211
- const eventId = idem.toString('latin1');
1212
- if (seen.has(eventId)) return 0;
1213
- seen.add(eventId);
1214
- }
1215
- const log = this.events.get(sk);
1216
- const ev = readCopy(ref, evPtr, evLen);
1217
- const sv = this.currentSchemaVersion(coll);
1218
- if (log === undefined) {
1219
- this.events.set(sk, [ev]);
1220
- this.eventVersions.set(sk, [sv]);
1221
- } else {
1222
- log.push(ev);
1223
- (this.eventVersions.get(sk) ?? this.eventVersions.set(sk, []).get(sk)!).push(sv);
1224
- }
1225
- this.recordWrite(db, coll);
1226
- return 0;
1227
- }
1228
-
1229
- // Idempotent append: dedup on eventId. 1 appended, 0 duplicate. Mirrors the
1230
- // edge's (key, event_id) dedup marker (just an in-memory set in dev).
1231
- appendOnce(
1232
- ref: MemoryRef,
1233
- db: DbDevState,
1234
- handle: number,
1235
- keyPtr: number,
1236
- keyLen: number,
1237
- evidPtr: number,
1238
- evidLen: number,
1239
- evPtr: number,
1240
- evLen: number,
1241
- ): number {
1242
- const coll = collForOp(db, handle, DbOp.AppendOnce, CollectionFamily.Events);
1243
- if (typeof coll === 'number') return coll;
1244
- if (keyLen > MAX_KEY || evLen > MAX_VALUE) throw new Error('data: key/event too large');
1245
- const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
1246
- const evid = readCopy(ref, evidPtr, evidLen).toString('latin1');
1247
- let seen = this.eventDedup.get(sk);
1248
- if (seen === undefined) {
1249
- seen = new Set();
1250
- this.eventDedup.set(sk, seen);
1251
- }
1252
- if (seen.has(evid)) return 0; // already appended under this id
1253
- const ev = readCopy(ref, evPtr, evLen);
1254
- const sv = this.currentSchemaVersion(coll);
1255
- const log = this.events.get(sk);
1256
- if (log === undefined) {
1257
- this.events.set(sk, [ev]);
1258
- this.eventVersions.set(sk, [sv]);
1259
- } else {
1260
- log.push(ev);
1261
- (this.eventVersions.get(sk) ?? this.eventVersions.set(sk, []).get(sk)!).push(sv);
1262
- }
1263
- seen.add(evid);
1264
- this.recordWrite(db, coll);
1265
- return 1;
1266
- }
1267
-
1268
- // Version-checked replace of an EXISTING record's value. Returns 0 on apply,
1269
- // ABSENT (-2) if the record is absent. A single dev process has no concurrent
1270
- // writer, so the optimistic-concurrency check always succeeds here.
1271
- enqueue(
1272
- ref: MemoryRef,
1273
- db: DbDevState,
1274
- handle: number,
1275
- keyPtr: number,
1276
- keyLen: number,
1277
- valPtr: number,
1278
- valLen: number,
1279
- idemPtr: number,
1280
- ): number {
1281
- const coll = collForOp(db, handle, DbOp.Enqueue, CollectionFamily.Record);
1282
- if (typeof coll === 'number') return coll;
1283
- if (keyLen > MAX_KEY || valLen > MAX_VALUE) throw new Error('data: key/value too large');
1284
- const key = readKey(ref, keyPtr, keyLen);
1285
- const value = readCopy(ref, valPtr, valLen);
1286
- const idem = readIdem(ref, idemPtr);
1287
- const requestHash = recordRequestHash(RECORD_OP_ENQUEUE, key, value);
1288
- const start = this.recordIdemStart(coll, key, 'E', idem, requestHash);
1289
- if (!start.fresh)
1290
- return start.outcome ? this.replayRecordOutcome(db, start.outcome) : start.status;
1291
- const sk = storeKey(coll.name, key);
1292
- const outcome: RecordOutcome = this.store.has(sk)
1293
- ? { kind: 'unit' }
1294
- : { kind: 'not_found' };
1295
- if (outcome.kind === 'unit') {
1296
- this.store.set(sk, value);
1297
- this.stampVersion(coll, sk);
1298
- }
1299
- this.recordIdemFinish(coll, key, 'E', idem, requestHash, outcome);
1300
- return this.replayRecordOutcome(db, outcome);
1301
- }
1302
-
1303
- // Frame the newest-`limit` events as `u32 count` then per event a
1304
- // length-prefixed blob (`u32 len + bytes`), newest first; stash + return
1305
- // the blob length. Matches the edge `op_latest` / `toildb::Writer` framing.
1306
- latest(
1307
- ref: MemoryRef,
1308
- db: DbDevState,
1309
- handle: number,
1310
- keyPtr: number,
1311
- keyLen: number,
1312
- limit: number,
1313
- ): number {
1314
- const coll = collForOp(db, handle, DbOp.Latest, CollectionFamily.Events);
1315
- if (typeof coll === 'number') return coll;
1316
- const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
1317
- const log = this.events.get(sk) ?? [];
1318
- const vers = this.eventVersions.get(sk) ?? [];
1319
- const n = Math.max(0, Math.min(limit, 0xffff));
1320
- const start = Math.max(0, log.length - n);
1321
- const newest = log.slice(start).reverse();
1322
- const newestVers = vers.slice(start).reverse();
1323
- // u32 count, then per event its stored schema_version (u32) + bytes (u32 len
1324
- // + bytes), newest first. Same framing as the edge op_latest.
1325
- const w = new DataWriter();
1326
- w.writeU32(newest.length);
1327
- for (let i = 0; i < newest.length; i++) {
1328
- w.writeU32(newestVers[i] ?? 0).writeBytes(newest[i]);
1329
- }
1330
- db.lastResult = Buffer.from(w.toBytes());
1331
- return db.lastResult.length;
1332
- }
1333
-
1334
- // `events.since(handle, keyPtr, keyLen, limit)`: the DERIVE-path incremental read. Returns the next
1335
- // batch of events for the key PAST this derive's checkpoint, OLDEST first, and advances the cursor. The
1336
- // advance is STAGED in `db.pendingCheckpoints` (this run only) and committed to the durable
1337
- // `deriveCheckpoints` after `derive_run` returns (see `commitDeriveCheckpoints`), so a trapped fold
1338
- // re-reads the batch next run instead of skipping it. Same wire frame as `latest`; empty once caught up.
1339
- since(
1340
- ref: MemoryRef,
1341
- db: DbDevState,
1342
- handle: number,
1343
- keyPtr: number,
1344
- keyLen: number,
1345
- limit: number,
1346
- ): number {
1347
- const coll = collForOp(db, handle, DbOp.EventsSince, CollectionFamily.Events);
1348
- if (typeof coll === 'number') return coll;
1349
- const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
1350
- const ckKey = `${db.deriveId}${sk}`;
1351
- const log = this.events.get(sk) ?? [];
1352
- const vers = this.eventVersions.get(sk) ?? [];
1353
- // Resume from this run's staged cursor if `since` was already called for this key, else the durable
1354
- // checkpoint, else the start.
1355
- const cursor = db.pendingCheckpoints.get(ckKey) ?? this.deriveCheckpoints.get(ckKey) ?? 0;
1356
- const n = Math.max(0, Math.min(limit, 0xffff));
1357
- const end = Math.min(log.length, cursor + n);
1358
- const batch = log.slice(cursor, end); // oldest-first
1359
- const batchVers = vers.slice(cursor, end);
1360
- db.pendingCheckpoints.set(ckKey, end); // staged; committed only on derive success
1361
- const w = new DataWriter();
1362
- w.writeU32(batch.length);
1363
- for (let i = 0; i < batch.length; i++) {
1364
- w.writeU32(batchVers[i] ?? 0).writeBytes(batch[i]);
1365
- }
1366
- db.lastResult = Buffer.from(w.toBytes());
1367
- return db.lastResult.length;
1368
- }
1369
-
1370
- /** Commit the `events.since` cursor advances a derive run staged in `db.pendingCheckpoints` into the
1371
- * durable map. Call ONLY after the fold's `derive_run` returned (its `view.publish`es have landed in
1372
- * this.views), so a trapped fold leaves the durable cursor untouched and the next run re-reads the
1373
- * batch (publish-then-persist, mirroring the edge `commit_derive_checkpoints`). No-op if none staged. */
1374
- commitDeriveCheckpoints(db: DbDevState): void {
1375
- if (db.pendingCheckpoints.size === 0) return;
1376
- for (const [k, v] of db.pendingCheckpoints) this.deriveCheckpoints.set(k, v);
1377
- db.pendingCheckpoints.clear();
1378
- }
1379
-
1380
- // --- capacity family (escrow: set_total / available / reserve / confirm / cancel) ---
1381
-
1382
- // Set the ceiling (restock / reduce). Job/derive only (kind-gated upstream).
1383
- // A ceiling is never negative.
1384
- capacitySetTotal(
1385
- ref: MemoryRef,
1386
- db: DbDevState,
1387
- handle: number,
1388
- keyPtr: number,
1389
- keyLen: number,
1390
- total: number | bigint,
1391
- ): number {
1392
- const coll = collForOp(db, handle, DbOp.CapacitySetTotal, CollectionFamily.Capacity);
1393
- if (typeof coll === 'number') return coll;
1394
- const l = this.capLedger(storeKey(coll.name, readKey(ref, keyPtr, keyLen)));
1395
- const t = BigInt(total);
1396
- l.total = satI64(t < 0n ? 0n : t);
1397
- return 0;
1398
- }
1399
-
1400
- // Stash the i64 available (total - reserved [held + confirmed], floored at 0).
1401
- capacityAvailable(
1402
- ref: MemoryRef,
1403
- db: DbDevState,
1404
- handle: number,
1405
- keyPtr: number,
1406
- keyLen: number,
1407
- ): number {
1408
- const coll = collForOp(db, handle, DbOp.CapacityAvailable, CollectionFamily.Capacity);
1409
- if (typeof coll === 'number') return coll;
1410
- const l = this.capLedger(storeKey(coll.name, readKey(ref, keyPtr, keyLen)));
1411
- this.capPrune(l, Date.now());
1412
- const avail = l.total - this.capReserved(l);
1413
- const out = Buffer.alloc(8);
1414
- out.writeBigInt64LE(avail < 0n ? 0n : avail);
1415
- db.lastResult = out;
1416
- return out.length;
1417
- }
1418
-
1419
- // Hold `amount` for `ttlMs`: stash the u64 reservation id (8 bytes) on
1420
- // success. A non-positive amount is a typed error (CODEC_ERR), matching the
1421
- // edge's BadAmount; insufficient available OR too many live reservations is
1422
- // ABSENT (-2) (the guest maps that to reservation 0 = no oversell). The TTL
1423
- // is clamped to the edge's 24h ceiling. `now` is the HOST clock.
1424
- capacityReserve(
1425
- ref: MemoryRef,
1426
- db: DbDevState,
1427
- handle: number,
1428
- keyPtr: number,
1429
- keyLen: number,
1430
- amount: number | bigint,
1431
- ttlMs: number | bigint,
1432
- idemPtr: number,
1433
- ): number {
1434
- const coll = collForOp(db, handle, DbOp.CapacityReserve, CollectionFamily.Capacity);
1435
- if (typeof coll === 'number') return coll;
1436
- const want = BigInt(amount);
1437
- if (want <= 0n) return CODEC_ERR; // BadAmount (edge: -1006)
1438
- const key = readKey(ref, keyPtr, keyLen);
1439
- const idem = readIdem(ref, idemPtr);
1440
- const ttl = Math.min(Math.max(0, Number(ttlMs)), MAX_RESERVATION_TTL_MS);
1441
- const requestedId = idem === null ? null : reservationIdFromIdem(coll, key, idem);
1442
- const l = this.capLedger(storeKey(coll.name, key));
1443
- const now = Date.now();
1444
- this.capPrune(l, now);
1445
- if (requestedId !== null) {
1446
- const existing = l.reservations.get(requestedId);
1447
- if (existing !== undefined) {
1448
- if (existing.amount !== want) return CONFLICT;
1449
- const out = Buffer.alloc(8);
1450
- out.writeBigUInt64LE(requestedId);
1451
- db.lastResult = out;
1452
- return out.length;
1453
- }
1454
- }
1455
- if (l.total - this.capReserved(l) < want || l.reservations.size >= MAX_RESERVATIONS)
1456
- return ABSENT; // never oversell; bound the reservation count
1457
- const id = requestedId ?? l.nextId++;
1458
- l.reservations.set(id, { amount: want, expiresMs: now + ttl, confirmed: false });
1459
- const out = Buffer.alloc(8);
1460
- out.writeBigUInt64LE(id);
1461
- db.lastResult = out;
1462
- return out.length;
1463
- }
1464
-
1465
- // Finalize a reservation into a permanent consume. IDEMPOTENT: the
1466
- // reservation is flagged confirmed (and kept), so a retry of a settled id
1467
- // still returns 1; 0 only when the id is unknown / expired-and-pruned.
1468
- capacityConfirm(
1469
- ref: MemoryRef,
1470
- db: DbDevState,
1471
- handle: number,
1472
- keyPtr: number,
1473
- keyLen: number,
1474
- reservationId: number | bigint,
1475
- ): number {
1476
- const coll = collForOp(db, handle, DbOp.CapacityConfirm, CollectionFamily.Capacity);
1477
- if (typeof coll === 'number') return coll;
1478
- const l = this.capLedger(storeKey(coll.name, readKey(ref, keyPtr, keyLen)));
1479
- this.capPrune(l, Date.now());
1480
- const r = l.reservations.get(BigInt(reservationId));
1481
- if (r === undefined) return 0;
1482
- r.confirmed = true;
1483
- return 1;
1484
- }
1485
-
1486
- // Release a HELD reservation back to available. A confirmed sale cannot be
1487
- // cancelled (returns 0), nor an unknown id.
1488
- capacityCancel(
1489
- ref: MemoryRef,
1490
- db: DbDevState,
1491
- handle: number,
1492
- keyPtr: number,
1493
- keyLen: number,
1494
- reservationId: number | bigint,
1495
- ): number {
1496
- const coll = collForOp(db, handle, DbOp.CapacityCancel, CollectionFamily.Capacity);
1497
- if (typeof coll === 'number') return coll;
1498
- const l = this.capLedger(storeKey(coll.name, readKey(ref, keyPtr, keyLen)));
1499
- this.capPrune(l, Date.now());
1500
- const id = BigInt(reservationId);
1501
- const r = l.reservations.get(id);
1502
- if (r === undefined || r.confirmed) return 0;
1503
- l.reservations.delete(id);
1504
- return 1;
1505
- }
1506
-
1507
- // --- result pull + per-row metadata ---
1508
-
1509
- // Drain the last stashed variable-length result into the caller buffer.
1510
- takeResult(ref: MemoryRef, db: DbDevState, outPtr: number, outCap: number): number {
1511
- const v = db.lastResult;
1512
- if (v === null) return 0;
1513
- if (v.length > outCap) return TOO_SMALL; // keep the stash for retry
1514
- const m = mem(ref);
1515
- if (outPtr < 0 || outPtr + v.length > m.length)
1516
- throw new Error('data: take_result out of bounds');
1517
- v.copy(m, outPtr);
1518
- db.lastResult = null;
1519
- return v.length;
1520
- }
1521
-
1522
- // `data.result_schema_version() -> i64`: the schema_version the last
1523
- // value-returning read's row was written under, so the guest's woven decoder
1524
- // runs the right @migrate. With on-disk persistence + catalog version
1525
- // stamping, dev DOES surface historical versions: a row written under an old
1526
- // @data layout reports that old version after the type evolves, so the dev
1527
- // server exercises real cross-version decode. -1 means the last op returned
1528
- // no value. An i64 result returns a BigInt in Node's WASM ABI.
1529
- resultSchemaVersion(db: DbDevState): bigint {
1530
- return BigInt(db.lastResultVersion);
1531
- }
1532
-
1533
- // --- test hooks ---
1534
-
1535
- /** Test-only: clear the stores + catalog + persistence between unit tests. */
1536
- resetForTests(): void {
1537
- this.clear();
1538
- this.catalog = { kind: 'no-section' };
1539
- this.persistPath = null;
1540
- }
1541
-
1542
- /** Test-only: seed the catalog directly. Number values default to record-family entries. */
1543
- setCatalogForTests(entries: Record<string, CatalogSeedEntry>): void {
1544
- const collections = new Map<string, DevCollectionHandle>();
1545
- for (const [name, entry] of Object.entries(entries)) {
1546
- const schemaVersion = typeof entry === 'number' ? entry : (entry.schemaVersion ?? 0);
1547
- const family =
1548
- typeof entry === 'number'
1549
- ? CollectionFamily.Record
1550
- : (entry.family ?? CollectionFamily.Record);
1551
- const replication = typeof entry === 'number' ? 0 : (entry.replication ?? 0);
1552
- const placement = typeof entry === 'number' ? 0 : (entry.placement ?? 0);
1553
- const fillMaxWaitMs =
1554
- typeof entry === 'number'
1555
- ? DEFAULT_FILL_WAIT_MS
1556
- : (entry.fillMaxWaitMs ?? DEFAULT_FILL_WAIT_MS);
1557
- const fillAllowStale =
1558
- typeof entry === 'number' ? true : (entry.fillAllowStale ?? true);
1559
- if (!isCollectionFamily(family)) {
1560
- this.catalog = { kind: 'malformed' };
1561
- return;
1562
- }
1563
- if (fillMaxWaitMs > MAX_FILL_WAIT_MS) {
1564
- this.catalog = { kind: 'malformed' };
1565
- return;
1566
- }
1567
- collections.set(name, {
1568
- name,
1569
- family,
1570
- schemaVersion: schemaVersion >>> 0,
1571
- replication,
1572
- placement,
1573
- fillMaxWaitMs,
1574
- fillAllowStale,
1575
- });
1576
- }
1577
- this.catalog = { kind: 'present', collections };
1578
- }
1579
- }
1580
-
1581
- /** The single process-lifetime dev database (one store per dev/self-host process). */
1582
- export const devDb = new DevDatabase();
1583
-
1584
- /** (Re)load the collection -> current schema_version map from a server wasm. */
1585
- export function setDbCatalog(wasm: Buffer): void {
1586
- devDb.setCatalog(wasm);
1587
- }
1588
-
1589
- /** Point the dev DB at an on-disk file and load any existing snapshot. */
1590
- export function configureDbPersistence(filePath: string): void {
1591
- devDb.configurePersistence(filePath);
1592
- }
1593
-
1594
- /** Write the current store to disk (no-op if persistence is not configured). */
1595
- export function persistDb(): void {
1596
- devDb.persist();
1597
- }
1598
-
1599
- /** Commit the `events.since` checkpoints a derive run staged in `db.pendingCheckpoints` (call after a
1600
- * successful `derive_run`; a trapped fold must NOT reach this). No-op if the run staged none. */
1601
- export function commitDeriveCheckpoints(db: DbDevState): void {
1602
- devDb.commitDeriveCheckpoints(db);
1603
- }
1604
-
1605
- /**
1606
- * Build the `data.*` host imports for one instance, delegating to the shared
1607
- * {@link devDb}. `db` is the per-request state (handles + result stash); bind a
1608
- * fresh one per dispatch.
1609
- */
1610
- export function buildDatabaseImports(
1611
- ref: MemoryRef,
1612
- db: DbDevState,
1613
- ): Record<string, (...args: number[]) => number | bigint> {
1614
- return {
1615
- 'data.resolve_collection': (
1616
- namePtr: number,
1617
- nameLen: number,
1618
- outHandlePtr: number,
1619
- ): number => devDb.resolveCollection(ref, db, namePtr, nameLen, outHandlePtr),
1620
-
1621
- 'data.get': (handle: number, keyPtr: number, keyLen: number): number =>
1622
- devDb.get(ref, db, handle, keyPtr, keyLen),
1623
-
1624
- 'data.get_many': (handle: number, keysPtr: number, keysLen: number): number =>
1625
- devDb.getMany(ref, db, handle, keysPtr, keysLen),
1626
-
1627
- 'data.exists': (handle: number, keyPtr: number, keyLen: number): number =>
1628
- devDb.exists(ref, db, handle, keyPtr, keyLen),
1629
-
1630
- 'data.create': (
1631
- handle: number,
1632
- keyPtr: number,
1633
- keyLen: number,
1634
- valPtr: number,
1635
- valLen: number,
1636
- idemPtr: number,
1637
- ): number => devDb.create(ref, db, handle, keyPtr, keyLen, valPtr, valLen, idemPtr),
1638
-
1639
- 'data.patch': (
1640
- handle: number,
1641
- keyPtr: number,
1642
- keyLen: number,
1643
- patchPtr: number,
1644
- patchLen: number,
1645
- idemPtr: number,
1646
- ): number => devDb.patch(ref, db, handle, keyPtr, keyLen, patchPtr, patchLen, idemPtr),
1647
-
1648
- 'data.upsert': (
1649
- handle: number,
1650
- keyPtr: number,
1651
- keyLen: number,
1652
- valPtr: number,
1653
- valLen: number,
1654
- idemPtr: number,
1655
- ): number => devDb.upsert(ref, db, handle, keyPtr, keyLen, valPtr, valLen, idemPtr),
1656
-
1657
- 'data.delete': (handle: number, keyPtr: number, keyLen: number, idemPtr: number): number =>
1658
- devDb.delete(ref, db, handle, keyPtr, keyLen, idemPtr),
1659
-
1660
- 'data.get_delete': (
1661
- handle: number,
1662
- keyPtr: number,
1663
- keyLen: number,
1664
- idemPtr: number,
1665
- ): number => devDb.getDelete(ref, db, handle, keyPtr, keyLen, idemPtr),
1666
-
1667
- 'data.unique_lookup': (handle: number, keyPtr: number, keyLen: number): number =>
1668
- devDb.uniqueLookup(ref, db, handle, keyPtr, keyLen),
1669
-
1670
- 'data.unique_claim': (
1671
- handle: number,
1672
- keyPtr: number,
1673
- keyLen: number,
1674
- valPtr: number,
1675
- valLen: number,
1676
- idemPtr: number,
1677
- ): number => devDb.uniqueClaim(ref, db, handle, keyPtr, keyLen, valPtr, valLen, idemPtr),
1678
-
1679
- 'data.unique_release': (
1680
- handle: number,
1681
- keyPtr: number,
1682
- keyLen: number,
1683
- valPtr: number,
1684
- valLen: number,
1685
- _idemPtr: number,
1686
- ): number => devDb.uniqueRelease(ref, db, handle, keyPtr, keyLen, valPtr, valLen),
1687
-
1688
- 'data.membership_contains': (
1689
- handle: number,
1690
- setPtr: number,
1691
- setLen: number,
1692
- memberPtr: number,
1693
- memberLen: number,
1694
- ): number =>
1695
- devDb.membershipContains(ref, db, handle, setPtr, setLen, memberPtr, memberLen),
1696
-
1697
- 'data.membership_add': (
1698
- handle: number,
1699
- setPtr: number,
1700
- setLen: number,
1701
- memberPtr: number,
1702
- memberLen: number,
1703
- _idemPtr: number,
1704
- ): number => devDb.membershipAdd(ref, db, handle, setPtr, setLen, memberPtr, memberLen),
1705
-
1706
- 'data.membership_remove': (
1707
- handle: number,
1708
- setPtr: number,
1709
- setLen: number,
1710
- memberPtr: number,
1711
- memberLen: number,
1712
- _idemPtr: number,
1713
- ): number => devDb.membershipRemove(ref, db, handle, setPtr, setLen, memberPtr, memberLen),
1714
-
1715
- 'data.membership_list': (
1716
- handle: number,
1717
- setPtr: number,
1718
- setLen: number,
1719
- limit: number,
1720
- ): number => devDb.membershipList(ref, db, handle, setPtr, setLen, limit),
1721
-
1722
- 'data.view_get': (handle: number, keyPtr: number, keyLen: number): number =>
1723
- devDb.viewGet(ref, db, handle, keyPtr, keyLen),
1724
-
1725
- 'data.view_publish': (
1726
- handle: number,
1727
- keyPtr: number,
1728
- keyLen: number,
1729
- valPtr: number,
1730
- valLen: number,
1731
- _idemPtr: number,
1732
- ): number => devDb.viewPublish(ref, db, handle, keyPtr, keyLen, valPtr, valLen),
1733
-
1734
- 'data.counter_get': (handle: number, keyPtr: number, keyLen: number): number =>
1735
- devDb.counterGet(ref, db, handle, keyPtr, keyLen),
1736
-
1737
- 'data.counter_add': (
1738
- handle: number,
1739
- keyPtr: number,
1740
- keyLen: number,
1741
- delta: number | bigint,
1742
- idemPtr: number,
1743
- ): number => devDb.counterAdd(ref, db, handle, keyPtr, keyLen, delta, idemPtr),
1744
-
1745
- 'data.append': (
1746
- handle: number,
1747
- keyPtr: number,
1748
- keyLen: number,
1749
- evPtr: number,
1750
- evLen: number,
1751
- idemPtr: number,
1752
- ): number => devDb.append(ref, db, handle, keyPtr, keyLen, evPtr, evLen, idemPtr),
1753
-
1754
- 'data.append_once': (
1755
- handle: number,
1756
- keyPtr: number,
1757
- keyLen: number,
1758
- evidPtr: number,
1759
- evidLen: number,
1760
- evPtr: number,
1761
- evLen: number,
1762
- ): number =>
1763
- devDb.appendOnce(ref, db, handle, keyPtr, keyLen, evidPtr, evidLen, evPtr, evLen),
1764
-
1765
- 'data.enqueue': (
1766
- handle: number,
1767
- keyPtr: number,
1768
- keyLen: number,
1769
- valPtr: number,
1770
- valLen: number,
1771
- idemPtr: number,
1772
- ): number => devDb.enqueue(ref, db, handle, keyPtr, keyLen, valPtr, valLen, idemPtr),
1773
-
1774
- 'data.latest': (handle: number, keyPtr: number, keyLen: number, limit: number): number =>
1775
- devDb.latest(ref, db, handle, keyPtr, keyLen, limit),
1776
- 'data.events_since': (handle: number, keyPtr: number, keyLen: number, limit: number): number =>
1777
- devDb.since(ref, db, handle, keyPtr, keyLen, limit),
1778
-
1779
- 'data.capacity_set_total': (
1780
- handle: number,
1781
- keyPtr: number,
1782
- keyLen: number,
1783
- total: number | bigint,
1784
- _idemPtr: number,
1785
- ): number => devDb.capacitySetTotal(ref, db, handle, keyPtr, keyLen, total),
1786
-
1787
- 'data.capacity_available': (handle: number, keyPtr: number, keyLen: number): number =>
1788
- devDb.capacityAvailable(ref, db, handle, keyPtr, keyLen),
1789
-
1790
- 'data.capacity_reserve': (
1791
- handle: number,
1792
- keyPtr: number,
1793
- keyLen: number,
1794
- amount: number | bigint,
1795
- ttlMs: number | bigint,
1796
- idemPtr: number,
1797
- ): number => devDb.capacityReserve(ref, db, handle, keyPtr, keyLen, amount, ttlMs, idemPtr),
1798
-
1799
- 'data.capacity_confirm': (
1800
- handle: number,
1801
- keyPtr: number,
1802
- keyLen: number,
1803
- reservationId: number | bigint,
1804
- _idemPtr: number,
1805
- ): number => devDb.capacityConfirm(ref, db, handle, keyPtr, keyLen, reservationId),
1806
-
1807
- 'data.capacity_cancel': (
1808
- handle: number,
1809
- keyPtr: number,
1810
- keyLen: number,
1811
- reservationId: number | bigint,
1812
- _idemPtr: number,
1813
- ): number => devDb.capacityCancel(ref, db, handle, keyPtr, keyLen, reservationId),
1814
-
1815
- 'data.take_result': (outPtr: number, outCap: number): number =>
1816
- devDb.takeResult(ref, db, outPtr, outCap),
1817
-
1818
- 'data.result_schema_version': (): bigint => devDb.resultSchemaVersion(db),
1819
-
1820
- // `data.write_allowed() -> i32`: 1 if the current call may issue the
1821
- // record patch used by rewrite-on-read migration convergence.
1822
- 'data.write_allowed': (): number => (kindAllows(db.functionKind, DbOp.Patch) ? 1 : 0),
1823
- };
1824
- }
1825
-
1826
- /** Test-only: clear the stores + catalog + persistence between unit tests. */
1827
- export function __resetDbForTests(): void {
1828
- devDb.resetForTests();
1829
- }
1830
-
1831
- /** Test-only: seed the catalog directly. Number values default to record-family entries. */
1832
- export function __setDbCatalogForTests(entries: Record<string, CatalogSeedEntry>): void {
1833
- devDb.setCatalogForTests(entries);
1834
- }