toiljs 0.0.60 → 0.0.61

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 (119) hide show
  1. package/.github/workflows/ci.yml +31 -0
  2. package/CHANGELOG.md +5 -0
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +2 -2
  5. package/build/client/.tsbuildinfo +1 -1
  6. package/build/client/index.d.ts +1 -1
  7. package/build/client/index.js +1 -1
  8. package/build/client/routing/mount.js +12 -1
  9. package/build/client/ssr/markers.d.ts +1 -0
  10. package/build/client/ssr/markers.js +3 -0
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/config.d.ts +21 -0
  13. package/build/compiler/config.js +35 -0
  14. package/build/compiler/docs.d.ts +2 -1
  15. package/build/compiler/docs.js +33 -304
  16. package/build/compiler/index.d.ts +13 -0
  17. package/build/compiler/index.js +113 -21
  18. package/build/compiler/template-build.d.ts +21 -1
  19. package/build/compiler/template-build.js +110 -26
  20. package/build/compiler/toil-docs.generated.d.ts +1 -0
  21. package/build/compiler/toil-docs.generated.js +20 -0
  22. package/build/devserver/.tsbuildinfo +1 -1
  23. package/build/devserver/daemon/catalog.d.ts +26 -0
  24. package/build/devserver/daemon/catalog.js +48 -0
  25. package/build/devserver/daemon/cron.d.ts +4 -0
  26. package/build/devserver/daemon/cron.js +50 -0
  27. package/build/devserver/daemon/host.d.ts +37 -0
  28. package/build/devserver/daemon/host.js +94 -0
  29. package/build/devserver/daemon/index.d.ts +34 -0
  30. package/build/devserver/daemon/index.js +241 -0
  31. package/build/devserver/db/catalog.d.ts +2 -1
  32. package/build/devserver/db/catalog.js +44 -44
  33. package/build/devserver/db/database.d.ts +27 -11
  34. package/build/devserver/db/database.js +539 -169
  35. package/build/devserver/db/index.d.ts +1 -1
  36. package/build/devserver/db/index.js +1 -1
  37. package/build/devserver/db/routeKinds.d.ts +8 -0
  38. package/build/devserver/db/routeKinds.js +139 -0
  39. package/build/devserver/db/types.d.ts +64 -1
  40. package/build/devserver/db/types.js +33 -1
  41. package/build/devserver/index.d.ts +10 -0
  42. package/build/devserver/index.js +7 -0
  43. package/build/devserver/mstore/store.d.ts +18 -0
  44. package/build/devserver/mstore/store.js +82 -0
  45. package/build/devserver/runtime/host.d.ts +6 -0
  46. package/build/devserver/runtime/host.js +45 -1
  47. package/build/devserver/runtime/module.d.ts +1 -0
  48. package/build/devserver/runtime/module.js +27 -1
  49. package/build/devserver/server.d.ts +6 -0
  50. package/build/devserver/server.js +59 -0
  51. package/build/devserver/ssr.d.ts +25 -0
  52. package/build/devserver/ssr.js +114 -0
  53. package/build/devserver/wasm/sections.d.ts +2 -0
  54. package/build/devserver/wasm/sections.js +42 -0
  55. package/build/devserver/wasm/surface.d.ts +18 -0
  56. package/build/devserver/wasm/surface.js +41 -0
  57. package/docs/README.md +4 -4
  58. package/docs/auth-todo.md +6 -6
  59. package/docs/caching.md +5 -5
  60. package/docs/cli.md +15 -0
  61. package/docs/client.md +40 -0
  62. package/docs/crypto.md +4 -4
  63. package/docs/data.md +6 -6
  64. package/docs/email.md +28 -28
  65. package/docs/environment.md +10 -10
  66. package/docs/index.md +26 -0
  67. package/docs/ratelimit.md +10 -10
  68. package/docs/routing.md +2 -2
  69. package/docs/server.md +61 -0
  70. package/docs/ssr.md +561 -113
  71. package/docs/styling.md +22 -0
  72. package/docs/time.md +1 -1
  73. package/eslint.config.js +10 -1
  74. package/examples/basic/client/components/Header.tsx +3 -0
  75. package/examples/basic/client/routes/features/actions.tsx +0 -2
  76. package/examples/basic/client/routes/hello.tsx +89 -19
  77. package/examples/basic/client/styles/main.css +48 -0
  78. package/examples/basic/server/SsrHelloRender.ts +97 -0
  79. package/examples/basic/server/main.ts +5 -0
  80. package/examples/basic/server/streams/Echo.ts +49 -0
  81. package/package.json +12 -10
  82. package/scripts/gen-toil-docs.mjs +96 -0
  83. package/src/cli/create.ts +2 -2
  84. package/src/client/index.ts +1 -1
  85. package/src/client/routing/mount.tsx +18 -2
  86. package/src/client/ssr/markers.tsx +22 -0
  87. package/src/compiler/config.ts +88 -2
  88. package/src/compiler/docs.ts +47 -308
  89. package/src/compiler/index.ts +236 -32
  90. package/src/compiler/ssr-codegen.ts +1 -1
  91. package/src/compiler/template-build.ts +247 -46
  92. package/src/compiler/toil-docs.generated.ts +26 -0
  93. package/src/devserver/daemon/catalog.ts +120 -0
  94. package/src/devserver/daemon/cron.ts +87 -0
  95. package/src/devserver/daemon/host.ts +224 -0
  96. package/src/devserver/daemon/index.ts +349 -0
  97. package/src/devserver/db/catalog.ts +61 -53
  98. package/src/devserver/db/database.ts +613 -149
  99. package/src/devserver/db/index.ts +1 -1
  100. package/src/devserver/db/routeKinds.ts +147 -0
  101. package/src/devserver/db/types.ts +65 -2
  102. package/src/devserver/index.ts +12 -0
  103. package/src/devserver/mstore/store.ts +121 -0
  104. package/src/devserver/runtime/host.ts +92 -1
  105. package/src/devserver/runtime/module.ts +35 -1
  106. package/src/devserver/server.ts +101 -0
  107. package/src/devserver/ssr.ts +166 -0
  108. package/src/devserver/wasm/sections.ts +59 -0
  109. package/src/devserver/wasm/surface.ts +88 -0
  110. package/test/daemon-build.test.ts +198 -0
  111. package/test/daemon-catalog.test.ts +265 -0
  112. package/test/daemon-emulation.test.ts +216 -0
  113. package/test/devserver-database.test.ts +396 -5
  114. package/test/email-preview.test.ts +6 -1
  115. package/test/fixtures/daemon-app.ts +56 -0
  116. package/test/global-setup.ts +17 -0
  117. package/test/ssr-render.test.ts +94 -27
  118. package/test/ssr-template.test.tsx +44 -1
  119. package/vitest.config.ts +3 -0
@@ -0,0 +1,94 @@
1
+ import { buildDatabaseImports, freshDbState } from '../db/index.js';
2
+ import { buildCryptoImports, freshCryptoState } from '../runtime/crypto.js';
3
+ import { buildEnvImports, readBytes, writeBytesOut, } from '../runtime/host.js';
4
+ export var AbiError;
5
+ (function (AbiError) {
6
+ AbiError[AbiError["MstoreNotFound"] = 769] = "MstoreNotFound";
7
+ AbiError[AbiError["MstoreNotANumber"] = 774] = "MstoreNotANumber";
8
+ AbiError[AbiError["MstoreScanBusy"] = 775] = "MstoreScanBusy";
9
+ AbiError[AbiError["MstoreConflict"] = 772] = "MstoreConflict";
10
+ AbiError[AbiError["DaemonScheduleRejected"] = 1027] = "DaemonScheduleRejected";
11
+ AbiError[AbiError["DaemonCallFailed"] = 1029] = "DaemonCallFailed";
12
+ })(AbiError || (AbiError = {}));
13
+ export function encodeAbiError(code) {
14
+ return -(0x10000 + code);
15
+ }
16
+ export function buildMstoreImports(ref, store) {
17
+ const key = (p, l) => readBytes(ref, p, l).toString('utf8');
18
+ return {
19
+ 'mstore.get': (keyPtr, keyLen, outPtr, outCap) => {
20
+ const v = store.get(key(keyPtr, keyLen));
21
+ if (v === null)
22
+ return encodeAbiError(769);
23
+ return writeBytesOut(ref, v, outPtr, outCap);
24
+ },
25
+ 'mstore.set': (keyPtr, keyLen, valPtr, valLen, ttlSecs) => {
26
+ store.set(key(keyPtr, keyLen), readBytes(ref, valPtr, valLen), ttlSecs);
27
+ return 0;
28
+ },
29
+ 'mstore.delete': (keyPtr, keyLen) => store.delete(key(keyPtr, keyLen)) ? 0 : encodeAbiError(769),
30
+ 'mstore.incr': (keyPtr, keyLen, delta, ttlSecs) => {
31
+ const d = typeof delta === 'bigint' ? delta : BigInt(delta);
32
+ const next = store.incr(key(keyPtr, keyLen), d, ttlSecs);
33
+ return next === null ? BigInt(encodeAbiError(774)) : next;
34
+ },
35
+ 'mstore.cas': (keyPtr, keyLen, expectPtr, expectLen, newPtr, newLen, ttlSecs) => {
36
+ const expect = expectLen === 0 ? null : readBytes(ref, expectPtr, expectLen);
37
+ const ok = store.cas(key(keyPtr, keyLen), expect, readBytes(ref, newPtr, newLen), ttlSecs);
38
+ return ok ? 0 : encodeAbiError(772);
39
+ },
40
+ 'mstore.expire': (keyPtr, keyLen, ttlSecs) => store.expire(key(keyPtr, keyLen), ttlSecs)
41
+ ? 0
42
+ : encodeAbiError(769),
43
+ 'mstore.scan': (prefixPtr, prefixLen, cursor, outPtr, outCap) => {
44
+ const cur = typeof cursor === 'bigint' ? cursor : BigInt(cursor);
45
+ const res = store.scan(key(prefixPtr, prefixLen), cur);
46
+ if (res === null)
47
+ return BigInt(encodeAbiError(775));
48
+ let total = 4;
49
+ for (const k of res.keys)
50
+ total += 4 + Buffer.byteLength(k, 'utf8');
51
+ const frame = Buffer.alloc(total);
52
+ let o = frame.writeUInt32LE(res.keys.length, 0);
53
+ for (const k of res.keys) {
54
+ const kb = Buffer.from(k, 'utf8');
55
+ o = frame.writeUInt32LE(kb.length, o);
56
+ kb.copy(frame, o);
57
+ o += kb.length;
58
+ }
59
+ const len = writeBytesOut(ref, frame, outPtr, outCap);
60
+ if (len < 0)
61
+ return BigInt(len);
62
+ return (res.next << 32n) | BigInt(len);
63
+ },
64
+ };
65
+ }
66
+ export function buildDaemonNamespace(rt) {
67
+ return {
68
+ 'daemon.is_leader': () => (rt.isLeader() ? 1 : 0),
69
+ 'daemon.current_epoch': () => rt.epoch(),
70
+ 'daemon.yield': () => 0,
71
+ 'daemon.sleep_ms': (_ms) => 0,
72
+ 'daemon.task_count': () => rt.taskCount(),
73
+ 'daemon.next_fire_ms': (taskId) => {
74
+ const at = rt.nextFireMs(taskId);
75
+ return at === null ? BigInt(encodeAbiError(1027)) : BigInt(at);
76
+ },
77
+ 'daemon.http_call': (_reqPtr, _reqLen, _outPtr, _outCap) => BigInt(encodeAbiError(1029)),
78
+ 'daemon.remote_call': (_svcId, _reqPtr, _reqLen, _outPtr, _outCap) => BigInt(encodeAbiError(1029)),
79
+ };
80
+ }
81
+ export function freshDaemonState() {
82
+ return { crypto: freshCryptoState(), db: freshDbState() };
83
+ }
84
+ export function buildDaemonImports(ref, state, rt, store) {
85
+ return {
86
+ env: {
87
+ ...buildEnvImports(ref, state),
88
+ ...buildCryptoImports(ref, state.crypto),
89
+ ...buildDatabaseImports(ref, state.db),
90
+ ...buildDaemonNamespace(rt),
91
+ ...buildMstoreImports(ref, store),
92
+ },
93
+ };
94
+ }
@@ -0,0 +1,34 @@
1
+ import { type ScheduledTask } from './catalog.js';
2
+ import { type DaemonRuntime, type ResolvedDaemonConfig } from './host.js';
3
+ export declare function daemonEmulationEnabled(nodeMode: string): boolean;
4
+ export declare class DaemonHost implements DaemonRuntime {
5
+ private readonly coldWasmPath;
6
+ private readonly cfg;
7
+ private readonly nodeMode;
8
+ private readonly log;
9
+ private module;
10
+ private instance;
11
+ private exports;
12
+ private state;
13
+ private catalog;
14
+ private loadedMtimeMs;
15
+ private running;
16
+ private epochValue;
17
+ private timers;
18
+ private nextFire;
19
+ private ticking;
20
+ constructor(coldWasmPath: string, cfg: ResolvedDaemonConfig, nodeMode: string, log?: (s: string) => void);
21
+ get active(): boolean;
22
+ get tasks(): readonly ScheduledTask[];
23
+ isLeader(): boolean;
24
+ epoch(): bigint;
25
+ taskCount(): number;
26
+ nextFireMs(taskId: number): number | null;
27
+ refresh(): boolean;
28
+ private start;
29
+ private stop;
30
+ private registerTask;
31
+ private armCron;
32
+ private runTick;
33
+ close(): void;
34
+ }
@@ -0,0 +1,241 @@
1
+ import fs from 'node:fs';
2
+ import pc from 'picocolors';
3
+ import { devMemoryStore } from '../mstore/store.js';
4
+ import { parseSurface } from '../wasm/surface.js';
5
+ import { parseDaemonCatalog, } from './catalog.js';
6
+ import { cronMatches, cronNeverFires, nextCronFireMs } from './cron.js';
7
+ import { buildDaemonImports, freshDaemonState, } from './host.js';
8
+ function decodeAbiError(ret) {
9
+ if (ret >= 0n)
10
+ return 'ok';
11
+ if (ret === -1n)
12
+ return 'STATUS_TOO_SMALL';
13
+ if (ret === -2n)
14
+ return 'STATUS_ABSENT';
15
+ if (ret <= -0x10000n)
16
+ return '0x' + ((-ret - 0x10000n) & 0xffffn).toString(16).padStart(4, '0');
17
+ if (ret <= -1000n)
18
+ return 'DB(TDL ' + String(-ret - 1000n) + ')';
19
+ return String(ret);
20
+ }
21
+ export function daemonEmulationEnabled(nodeMode) {
22
+ return nodeMode === 'daemon' || nodeMode === 'all';
23
+ }
24
+ export class DaemonHost {
25
+ coldWasmPath;
26
+ cfg;
27
+ nodeMode;
28
+ log;
29
+ module = null;
30
+ instance = null;
31
+ exports = null;
32
+ state = freshDaemonState();
33
+ catalog = null;
34
+ loadedMtimeMs = -1;
35
+ running = false;
36
+ epochValue = 0n;
37
+ timers = new Map();
38
+ nextFire = new Map();
39
+ ticking = new Set();
40
+ constructor(coldWasmPath, cfg, nodeMode, log = (s) => process.stdout.write(s)) {
41
+ this.coldWasmPath = coldWasmPath;
42
+ this.cfg = cfg;
43
+ this.nodeMode = nodeMode;
44
+ this.log = log;
45
+ }
46
+ get active() {
47
+ return this.running;
48
+ }
49
+ get tasks() {
50
+ return this.catalog?.tasks ?? [];
51
+ }
52
+ isLeader() {
53
+ return true;
54
+ }
55
+ epoch() {
56
+ return this.epochValue;
57
+ }
58
+ taskCount() {
59
+ return this.catalog?.tasks.length ?? 0;
60
+ }
61
+ nextFireMs(taskId) {
62
+ return this.nextFire.get(taskId) ?? null;
63
+ }
64
+ refresh() {
65
+ if (!daemonEmulationEnabled(this.nodeMode))
66
+ return false;
67
+ let mtimeMs;
68
+ try {
69
+ mtimeMs = fs.statSync(this.coldWasmPath).mtimeMs;
70
+ }
71
+ catch {
72
+ if (this.running)
73
+ this.stop();
74
+ this.module = null;
75
+ this.loadedMtimeMs = -1;
76
+ return false;
77
+ }
78
+ if (this.module !== null && mtimeMs === this.loadedMtimeMs)
79
+ return false;
80
+ const bytes = fs.readFileSync(this.coldWasmPath);
81
+ const surface = parseSurface(bytes);
82
+ if (surface === 'invalid') {
83
+ this.log(pc.red(' ✗ cold artifact toil.surface is corrupt; daemon not started') + '\n');
84
+ if (this.running)
85
+ this.stop();
86
+ this.loadedMtimeMs = mtimeMs;
87
+ return false;
88
+ }
89
+ if (surface !== 'absent' && surface.targetMode !== 'cold')
90
+ this.log(pc.yellow(' ! ') +
91
+ pc.dim('cold slot holds a hot-mode artifact; ignoring daemon emulator') +
92
+ '\n');
93
+ const catalog = parseDaemonCatalog(bytes);
94
+ const declaresDaemon = (surface === 'absent' ? false : surface.flags.daemon) || (catalog?.hasDaemon ?? false);
95
+ if (this.running)
96
+ this.stop();
97
+ if (!declaresDaemon || catalog === null || !catalog.hasDaemon) {
98
+ this.module = null;
99
+ this.catalog = null;
100
+ this.loadedMtimeMs = mtimeMs;
101
+ return false;
102
+ }
103
+ this.module = new WebAssembly.Module(bytes);
104
+ this.catalog = catalog;
105
+ this.loadedMtimeMs = mtimeMs;
106
+ this.epochValue += 1n;
107
+ this.start();
108
+ return true;
109
+ }
110
+ start() {
111
+ if (this.module === null || this.catalog === null)
112
+ return;
113
+ const ref = { memory: null };
114
+ this.state = freshDaemonState();
115
+ const imports = buildDaemonImports(ref, this.state, this, devMemoryStore);
116
+ const provided = new Set(Object.keys(imports.env));
117
+ const missing = WebAssembly.Module.imports(this.module)
118
+ .filter((i) => i.kind === 'function' && (i.module !== 'env' || !provided.has(i.name)))
119
+ .map((i) => `${i.module}.${i.name}`);
120
+ if (missing.length > 0) {
121
+ this.log(pc.red(` ✗ cold daemon wasm imports unsupported host functions: ${missing.join(', ')}`) + '\n');
122
+ this.module = null;
123
+ return;
124
+ }
125
+ this.instance = new WebAssembly.Instance(this.module, imports);
126
+ this.exports = this.instance.exports;
127
+ ref.memory = this.exports.memory;
128
+ try {
129
+ if (typeof this.exports.init === 'function')
130
+ this.exports.init();
131
+ const rc = this.exports.daemon_start();
132
+ if (rc !== 0) {
133
+ this.log(pc.red(` ✗ daemon_start() returned ${String(rc)}; daemon not running`) + '\n');
134
+ this.instance = null;
135
+ this.exports = null;
136
+ return;
137
+ }
138
+ }
139
+ catch (e) {
140
+ this.log(pc.red(` ✗ daemon_start() trapped: ${String(e)}`) + '\n');
141
+ this.instance = null;
142
+ this.exports = null;
143
+ return;
144
+ }
145
+ this.running = true;
146
+ const limited = this.catalog.tasks.slice(0, this.cfg.maxTasks);
147
+ for (const task of limited)
148
+ this.registerTask(task);
149
+ this.log(pc.green(' ⏱ ') +
150
+ pc.dim(`daemon started (epoch ${String(this.epochValue)}, ${String(limited.length)} task${limited.length === 1 ? '' : 's'})`) +
151
+ '\n');
152
+ }
153
+ stop() {
154
+ for (const t of this.timers.values())
155
+ clearTimeout(t);
156
+ this.timers.clear();
157
+ this.nextFire.clear();
158
+ const stop = this.exports?.daemon_stop;
159
+ if (typeof stop === 'function') {
160
+ try {
161
+ stop();
162
+ }
163
+ catch {
164
+ }
165
+ }
166
+ this.instance = null;
167
+ this.exports = null;
168
+ this.running = false;
169
+ }
170
+ registerTask(task) {
171
+ if (task.schedule.kind === 'interval') {
172
+ const ms = Math.max(1000, task.schedule.ms || this.cfg.defaultIntervalMs);
173
+ this.nextFire.set(task.taskIndex, Date.now() + ms);
174
+ const handle = setInterval(() => {
175
+ this.nextFire.set(task.taskIndex, Date.now() + ms);
176
+ this.runTick(task);
177
+ }, ms);
178
+ handle.unref?.();
179
+ this.timers.set(task.taskIndex, handle);
180
+ }
181
+ else {
182
+ const masks = task.schedule.masks;
183
+ if (cronNeverFires(masks)) {
184
+ this.log(pc.yellow(' ! ') +
185
+ pc.dim(`@scheduled ${task.name} has an unsatisfiable cron mask; skipping (DAEMON_SCHEDULE_REJECTED)`) +
186
+ '\n');
187
+ return;
188
+ }
189
+ this.armCron(task, masks);
190
+ }
191
+ }
192
+ armCron(task, masks) {
193
+ const next = nextCronFireMs(masks, Date.now());
194
+ if (next === null) {
195
+ this.nextFire.delete(task.taskIndex);
196
+ return;
197
+ }
198
+ this.nextFire.set(task.taskIndex, next);
199
+ const delay = Math.max(0, next - Date.now());
200
+ const handle = setTimeout(() => {
201
+ if (cronMatches(masks, new Date()))
202
+ this.runTick(task);
203
+ if (this.running)
204
+ this.armCron(task, masks);
205
+ }, delay);
206
+ handle.unref?.();
207
+ this.timers.set(task.taskIndex, handle);
208
+ }
209
+ runTick(task) {
210
+ if (!this.running || this.exports === null)
211
+ return;
212
+ if (this.ticking.has(task.taskIndex)) {
213
+ this.log(pc.dim(` ⏱ @scheduled ${task.name} overran its interval; skipping a tick`) + '\n');
214
+ return;
215
+ }
216
+ if (!this.isLeader())
217
+ return;
218
+ this.ticking.add(task.taskIndex);
219
+ const startedAt = Date.now();
220
+ try {
221
+ const ret = this.exports.scheduled_tick(task.taskIndex);
222
+ if (ret < 0n)
223
+ this.log(pc.yellow(` ⏱ @scheduled ${task.name} returned error ${decodeAbiError(ret)}`) + '\n');
224
+ }
225
+ catch (e) {
226
+ this.log(pc.red(` ✗ @scheduled ${task.name} trapped: ${String(e)}`) + '\n');
227
+ }
228
+ finally {
229
+ const took = Date.now() - startedAt;
230
+ if (took > this.cfg.tickBudgetMs)
231
+ this.log(pc.yellow(` ⏱ @scheduled ${task.name} took ${String(took)}ms (> tickBudgetMs ${String(this.cfg.tickBudgetMs)})`) + '\n');
232
+ this.ticking.delete(task.taskIndex);
233
+ }
234
+ }
235
+ close() {
236
+ if (this.running)
237
+ this.stop();
238
+ this.module = null;
239
+ this.catalog = null;
240
+ }
241
+ }
@@ -1 +1,2 @@
1
- export declare function parseCatalog(wasm: Buffer): Map<string, number>;
1
+ import { type DbCatalogState } from './types.js';
2
+ export declare function parseCatalog(wasm: Buffer): DbCatalogState;
@@ -1,68 +1,51 @@
1
1
  import { DataReader } from 'toiljs/io';
2
- function leb(buf, pos) {
3
- let result = 0;
4
- let shift = 0;
5
- let p = pos;
6
- for (;;) {
7
- if (p >= buf.length)
8
- throw new RangeError('leb128 past end of buffer');
9
- const b = buf[p++];
10
- result |= (b & 0x7f) << shift;
11
- if ((b & 0x80) === 0)
12
- break;
13
- shift += 7;
14
- if (shift > 35)
15
- throw new RangeError('leb128 too long');
16
- }
17
- return [result >>> 0, p];
2
+ import { customSection } from '../wasm/sections.js';
3
+ import { DEFAULT_FILL_WAIT_MS, isCollectionFamily, MAX_FILL_WAIT_MS, } from './types.js';
4
+ function validReplication(value) {
5
+ return value >= 0 && value <= 5;
18
6
  }
19
- function customSection(wasm, want) {
20
- let pos = 8;
21
- while (pos < wasm.length) {
22
- const id = wasm[pos++];
23
- let size;
24
- [size, pos] = leb(wasm, pos);
25
- const end = pos + size;
26
- if (end > wasm.length || end < pos)
27
- return null;
28
- if (id === 0) {
29
- let nameLen;
30
- let namePos;
31
- [nameLen, namePos] = leb(wasm, pos);
32
- if (namePos + nameLen <= end && wasm.toString('latin1', namePos, namePos + nameLen) === want)
33
- return wasm.subarray(namePos + nameLen, end);
34
- }
35
- pos = end;
36
- }
37
- return null;
7
+ function validPlacement(value) {
8
+ return value === 0 || value === 1;
38
9
  }
39
10
  export function parseCatalog(wasm) {
40
- const out = new Map();
11
+ const collections = new Map();
41
12
  let sec;
42
13
  try {
43
14
  sec = customSection(wasm, 'toildb.catalog');
44
15
  }
45
16
  catch {
46
- return out;
17
+ return { kind: 'no-section' };
47
18
  }
48
19
  if (sec === null)
49
- return out;
20
+ return { kind: 'no-section' };
50
21
  const r = new DataReader(sec);
51
- r.readU16();
22
+ const version = r.readU16();
23
+ if (!r.ok || (version !== 1 && version !== 2))
24
+ return { kind: 'malformed' };
52
25
  const ndb = r.readU16();
53
26
  for (let d = 0; d < ndb && r.ok; d++) {
54
27
  const db = r.readString();
55
28
  const nc = r.readU16();
56
29
  for (let c = 0; c < nc && r.ok; c++) {
57
30
  const name = r.readString();
58
- r.readU8();
31
+ const family = r.readU8();
59
32
  r.readString();
60
33
  r.readString();
61
34
  r.readU32();
62
35
  const schemaVersion = r.readU32();
63
36
  r.readU32();
64
- r.readU8();
65
- r.readU8();
37
+ const replication = r.readU8();
38
+ const placement = r.readU8();
39
+ let fillMaxWaitMs = DEFAULT_FILL_WAIT_MS;
40
+ let fillAllowStale = true;
41
+ if (version >= 2) {
42
+ fillMaxWaitMs = r.readU32();
43
+ const fillAllowStaleByte = r.readU8();
44
+ if (fillMaxWaitMs > MAX_FILL_WAIT_MS ||
45
+ (fillAllowStaleByte !== 0 && fillAllowStaleByte !== 1))
46
+ return { kind: 'malformed' };
47
+ fillAllowStale = fillAllowStaleByte === 1;
48
+ }
66
49
  const nFields = r.readU16();
67
50
  for (let f = 0; f < nFields; f++) {
68
51
  r.readString();
@@ -72,9 +55,26 @@ export function parseCatalog(wasm) {
72
55
  const nMig = r.readU16();
73
56
  for (let m = 0; m < nMig; m++)
74
57
  r.readU32();
58
+ if (!isCollectionFamily(family) ||
59
+ !validReplication(replication) ||
60
+ !validPlacement(placement))
61
+ return { kind: 'malformed' };
62
+ const key = db + '/' + name;
63
+ if (collections.has(key))
64
+ return { kind: 'malformed' };
75
65
  if (r.ok)
76
- out.set(db + '/' + name, schemaVersion >>> 0);
66
+ collections.set(key, {
67
+ name: key,
68
+ family: family,
69
+ schemaVersion: schemaVersion >>> 0,
70
+ replication,
71
+ placement,
72
+ fillMaxWaitMs,
73
+ fillAllowStale,
74
+ });
77
75
  }
78
76
  }
79
- return out;
77
+ if (!r.ok || r.remaining() !== 0)
78
+ return { kind: 'malformed' };
79
+ return { kind: 'present', collections };
80
80
  }
@@ -1,10 +1,21 @@
1
1
  import type { MemoryRef } from '../runtime/host.js';
2
2
  import { type DbDevState } from './types.js';
3
+ type CatalogSeedEntry = number | {
4
+ family?: number;
5
+ schemaVersion?: number;
6
+ replication?: number;
7
+ placement?: number;
8
+ fillMaxWaitMs?: number;
9
+ fillAllowStale?: boolean;
10
+ };
3
11
  export declare class DevDatabase {
4
12
  private readonly store;
13
+ private readonly recordIdem;
14
+ private readonly uniqueIdem;
5
15
  private readonly views;
6
16
  private readonly members;
7
17
  private readonly counters;
18
+ private readonly counterIdem;
8
19
  private readonly events;
9
20
  private readonly eventDedup;
10
21
  private readonly capacity;
@@ -14,7 +25,11 @@ export declare class DevDatabase {
14
25
  private catalog;
15
26
  private persistPath;
16
27
  setCatalog(wasm: Buffer): void;
28
+ private currentSchemaVersion;
17
29
  private stampVersion;
30
+ private recordIdemStart;
31
+ private recordIdemFinish;
32
+ private replayRecordOutcome;
18
33
  configurePersistence(filePath: string): void;
19
34
  persist(): void;
20
35
  private load;
@@ -26,12 +41,12 @@ export declare class DevDatabase {
26
41
  get(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number;
27
42
  getMany(ref: MemoryRef, db: DbDevState, handle: number, keysPtr: number, keysLen: number): number;
28
43
  exists(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number;
29
- create(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, valPtr: number, valLen: number): number;
30
- patch(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, patchPtr: number, patchLen: number): number;
31
- delete(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number;
32
- getDelete(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number;
44
+ create(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, valPtr: number, valLen: number, idemPtr: number): number;
45
+ patch(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, patchPtr: number, patchLen: number, idemPtr: number): number;
46
+ delete(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, idemPtr: number): number;
47
+ getDelete(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, idemPtr: number): number;
33
48
  uniqueLookup(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number;
34
- uniqueClaim(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, valPtr: number, valLen: number): number;
49
+ uniqueClaim(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, valPtr: number, valLen: number, idemPtr: number): number;
35
50
  uniqueRelease(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, valPtr: number, valLen: number): number;
36
51
  membershipContains(ref: MemoryRef, db: DbDevState, handle: number, setPtr: number, setLen: number, memberPtr: number, memberLen: number): number;
37
52
  membershipAdd(ref: MemoryRef, db: DbDevState, handle: number, setPtr: number, setLen: number, memberPtr: number, memberLen: number): number;
@@ -40,20 +55,20 @@ export declare class DevDatabase {
40
55
  viewGet(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number;
41
56
  viewPublish(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, valPtr: number, valLen: number): number;
42
57
  counterGet(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number;
43
- counterAdd(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, delta: number | bigint): number;
44
- append(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, evPtr: number, evLen: number): number;
58
+ counterAdd(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, delta: number | bigint, idemPtr: number): number;
59
+ append(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, evPtr: number, evLen: number, idemPtr: number): number;
45
60
  appendOnce(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, evidPtr: number, evidLen: number, evPtr: number, evLen: number): number;
46
- enqueue(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, valPtr: number, valLen: number): number;
61
+ enqueue(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, valPtr: number, valLen: number, idemPtr: number): number;
47
62
  latest(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, limit: number): number;
48
63
  capacitySetTotal(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, total: number | bigint): number;
49
64
  capacityAvailable(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number;
50
- capacityReserve(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, amount: number | bigint, ttlMs: number | bigint): number;
65
+ capacityReserve(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, amount: number | bigint, ttlMs: number | bigint, idemPtr: number): number;
51
66
  capacityConfirm(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, reservationId: number | bigint): number;
52
67
  capacityCancel(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, reservationId: number | bigint): number;
53
68
  takeResult(ref: MemoryRef, db: DbDevState, outPtr: number, outCap: number): number;
54
69
  resultSchemaVersion(db: DbDevState): bigint;
55
70
  resetForTests(): void;
56
- setCatalogForTests(entries: Record<string, number>): void;
71
+ setCatalogForTests(entries: Record<string, CatalogSeedEntry>): void;
57
72
  }
58
73
  export declare const devDb: DevDatabase;
59
74
  export declare function setDbCatalog(wasm: Buffer): void;
@@ -61,4 +76,5 @@ export declare function configureDbPersistence(filePath: string): void;
61
76
  export declare function persistDb(): void;
62
77
  export declare function buildDatabaseImports(ref: MemoryRef, db: DbDevState): Record<string, (...args: number[]) => number | bigint>;
63
78
  export declare function __resetDbForTests(): void;
64
- export declare function __setDbCatalogForTests(entries: Record<string, number>): void;
79
+ export declare function __setDbCatalogForTests(entries: Record<string, CatalogSeedEntry>): void;
80
+ export {};