toiljs 0.0.59 → 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.
- package/.github/workflows/ci.yml +31 -0
- package/CHANGELOG.md +15 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +311 -118
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/index.d.ts +1 -1
- package/build/client/index.js +1 -1
- package/build/client/routing/mount.js +12 -1
- package/build/client/ssr/markers.d.ts +1 -0
- package/build/client/ssr/markers.js +3 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +21 -0
- package/build/compiler/config.js +35 -0
- package/build/compiler/docs.d.ts +2 -1
- package/build/compiler/docs.js +33 -304
- package/build/compiler/index.d.ts +13 -0
- package/build/compiler/index.js +113 -21
- package/build/compiler/template-build.d.ts +21 -1
- package/build/compiler/template-build.js +110 -26
- package/build/compiler/toil-docs.generated.d.ts +1 -0
- package/build/compiler/toil-docs.generated.js +20 -0
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/daemon/catalog.d.ts +26 -0
- package/build/devserver/daemon/catalog.js +48 -0
- package/build/devserver/daemon/cron.d.ts +4 -0
- package/build/devserver/daemon/cron.js +50 -0
- package/build/devserver/daemon/host.d.ts +37 -0
- package/build/devserver/daemon/host.js +94 -0
- package/build/devserver/daemon/index.d.ts +34 -0
- package/build/devserver/daemon/index.js +241 -0
- package/build/devserver/db/catalog.d.ts +2 -0
- package/build/devserver/db/catalog.js +80 -0
- package/build/devserver/db/database.d.ts +80 -0
- package/build/devserver/db/database.js +1032 -0
- package/build/devserver/db/index.d.ts +3 -0
- package/build/devserver/db/index.js +3 -0
- package/build/devserver/db/routeKinds.d.ts +8 -0
- package/build/devserver/db/routeKinds.js +139 -0
- package/build/devserver/db/types.d.ts +121 -0
- package/build/devserver/db/types.js +52 -0
- package/build/devserver/email/index.js +1 -1
- package/build/devserver/index.d.ts +19 -24
- package/build/devserver/index.js +11 -165
- package/build/devserver/mstore/store.d.ts +18 -0
- package/build/devserver/mstore/store.js +82 -0
- package/build/devserver/{host.d.ts → runtime/host.d.ts} +7 -1
- package/build/devserver/{host.js → runtime/host.js} +51 -7
- package/build/devserver/{module.d.ts → runtime/module.d.ts} +2 -1
- package/build/devserver/{module.js → runtime/module.js} +34 -1
- package/build/devserver/server.d.ts +23 -0
- package/build/devserver/server.js +223 -0
- package/build/devserver/ssr.d.ts +25 -0
- package/build/devserver/ssr.js +114 -0
- package/build/devserver/wasm/sections.d.ts +2 -0
- package/build/devserver/wasm/sections.js +42 -0
- package/build/devserver/wasm/surface.d.ts +18 -0
- package/build/devserver/wasm/surface.js +41 -0
- package/docs/README.md +4 -4
- package/docs/auth-todo.md +6 -6
- package/docs/caching.md +5 -5
- package/docs/cli.md +15 -0
- package/docs/client.md +40 -0
- package/docs/crypto.md +4 -4
- package/docs/data.md +6 -6
- package/docs/email.md +28 -28
- package/docs/environment.md +10 -10
- package/docs/index.md +26 -0
- package/docs/ratelimit.md +10 -10
- package/docs/routing.md +2 -2
- package/docs/server.md +61 -0
- package/docs/ssr.md +561 -113
- package/docs/styling.md +22 -0
- package/docs/time.md +3 -3
- package/eslint.config.js +10 -1
- package/examples/basic/client/components/Header.tsx +3 -0
- package/examples/basic/client/routes/features/actions.tsx +0 -2
- package/examples/basic/client/routes/hello.tsx +89 -19
- package/examples/basic/client/styles/main.css +48 -0
- package/examples/basic/server/SsrHelloRender.ts +97 -0
- package/examples/basic/server/main.ts +5 -0
- package/examples/basic/server/migrations/GuestEntry.migration.ts +39 -0
- package/examples/basic/server/streams/Echo.ts +49 -0
- package/package.json +12 -10
- package/scripts/gen-toil-docs.mjs +96 -0
- package/server/runtime/time.ts +3 -3
- package/src/cli/create.ts +40 -3
- package/src/cli/db.ts +158 -0
- package/src/cli/diagnostics.ts +19 -0
- package/src/cli/doctor.ts +20 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/update.ts +58 -0
- package/src/client/index.ts +1 -1
- package/src/client/routing/mount.tsx +18 -2
- package/src/client/ssr/markers.tsx +22 -0
- package/src/compiler/config.ts +88 -2
- package/src/compiler/docs.ts +47 -308
- package/src/compiler/index.ts +236 -32
- package/src/compiler/ssr-codegen.ts +1 -1
- package/src/compiler/template-build.ts +247 -46
- package/src/compiler/toil-docs.generated.ts +26 -0
- package/src/devserver/daemon/catalog.ts +120 -0
- package/src/devserver/daemon/cron.ts +87 -0
- package/src/devserver/daemon/host.ts +224 -0
- package/src/devserver/daemon/index.ts +349 -0
- package/src/devserver/db/catalog.ts +108 -0
- package/src/devserver/db/database.ts +1633 -0
- package/src/devserver/db/index.ts +18 -0
- package/src/devserver/db/routeKinds.ts +147 -0
- package/src/devserver/db/types.ts +139 -0
- package/src/devserver/email/index.ts +1 -1
- package/src/devserver/index.ts +31 -287
- package/src/devserver/mstore/store.ts +121 -0
- package/src/devserver/{host.ts → runtime/host.ts} +98 -7
- package/src/devserver/{module.ts → runtime/module.ts} +47 -1
- package/src/devserver/server.ts +393 -0
- package/src/devserver/ssr.ts +166 -0
- package/src/devserver/wasm/sections.ts +59 -0
- package/src/devserver/wasm/surface.ts +88 -0
- package/test/daemon-build.test.ts +198 -0
- package/test/daemon-catalog.test.ts +265 -0
- package/test/daemon-emulation.test.ts +216 -0
- package/test/db.test.ts +0 -0
- package/test/devserver-database.test.ts +510 -14
- package/test/devserver-pqauth.test.ts +1 -1
- package/test/devserver-secrets.test.ts +5 -1
- package/test/doctor.test.ts +13 -0
- package/test/email-preview.test.ts +6 -1
- package/test/example-guestbook.test.ts +43 -1
- package/test/fixtures/daemon-app.ts +56 -0
- package/test/global-setup.ts +17 -0
- package/test/pqauth-e2e.test.ts +1 -1
- package/test/ssr-render.test.ts +94 -27
- package/test/ssr-template.test.tsx +44 -1
- package/vitest.config.ts +3 -0
- package/build/devserver/database.d.ts +0 -8
- package/build/devserver/database.js +0 -418
- package/src/devserver/database.ts +0 -618
- /package/build/devserver/{dotenv.d.ts → config/dotenv.d.ts} +0 -0
- /package/build/devserver/{dotenv.js → config/dotenv.js} +0 -0
- /package/build/devserver/{env.d.ts → config/env.d.ts} +0 -0
- /package/build/devserver/{env.js → config/env.js} +0 -0
- /package/build/devserver/{ratelimit.d.ts → config/ratelimit.d.ts} +0 -0
- /package/build/devserver/{ratelimit.js → config/ratelimit.js} +0 -0
- /package/build/devserver/{cache.d.ts → http/cache.d.ts} +0 -0
- /package/build/devserver/{cache.js → http/cache.js} +0 -0
- /package/build/devserver/{envelope.d.ts → http/envelope.d.ts} +0 -0
- /package/build/devserver/{envelope.js → http/envelope.js} +0 -0
- /package/build/devserver/{proxy.d.ts → http/proxy.d.ts} +0 -0
- /package/build/devserver/{proxy.js → http/proxy.js} +0 -0
- /package/build/devserver/{crypto.d.ts → runtime/crypto.d.ts} +0 -0
- /package/build/devserver/{crypto.js → runtime/crypto.js} +0 -0
- /package/src/devserver/{dotenv.ts → config/dotenv.ts} +0 -0
- /package/src/devserver/{env.ts → config/env.ts} +0 -0
- /package/src/devserver/{ratelimit.ts → config/ratelimit.ts} +0 -0
- /package/src/devserver/{cache.ts → http/cache.ts} +0 -0
- /package/src/devserver/{envelope.ts → http/envelope.ts} +0 -0
- /package/src/devserver/{proxy.ts → http/proxy.ts} +0 -0
- /package/src/devserver/{crypto.ts → runtime/crypto.ts} +0 -0
|
@@ -1,9 +1,38 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
|
|
3
|
-
import {
|
|
4
|
-
import type { MemoryRef } from '../src/devserver/host.js';
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
import {
|
|
8
|
+
CollectionFamily,
|
|
9
|
+
DbFunctionKind,
|
|
10
|
+
__resetDbForTests,
|
|
11
|
+
__setDbCatalogForTests,
|
|
12
|
+
buildDatabaseImports,
|
|
13
|
+
configureDbPersistence,
|
|
14
|
+
freshDbState,
|
|
15
|
+
persistDb,
|
|
16
|
+
setDbCatalog,
|
|
17
|
+
} from '../src/devserver/db/index.js';
|
|
18
|
+
import { parseRouteKinds, routeKindForRequest } from '../src/devserver/db/routeKinds.js';
|
|
19
|
+
import type { MemoryRef } from '../src/devserver/runtime/host.js';
|
|
20
|
+
|
|
21
|
+
const DEFAULT_CATALOG = {
|
|
22
|
+
'App/users': { family: CollectionFamily.Record },
|
|
23
|
+
'App/posts': { family: CollectionFamily.Record },
|
|
24
|
+
'App/docs': { family: CollectionFamily.Record },
|
|
25
|
+
'App/challenges': { family: CollectionFamily.Record },
|
|
26
|
+
'App/pages': { family: CollectionFamily.View },
|
|
27
|
+
'App/feed': { family: CollectionFamily.Events },
|
|
28
|
+
'App/likes': { family: CollectionFamily.Counter },
|
|
29
|
+
'App/rooms': { family: CollectionFamily.Membership },
|
|
30
|
+
'App/usernames': { family: CollectionFamily.Unique },
|
|
31
|
+
'App/seats': { family: CollectionFamily.Capacity },
|
|
32
|
+
'App/tickets': { family: CollectionFamily.Capacity },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function setupRaw() {
|
|
7
36
|
const memory = new WebAssembly.Memory({ initial: 1 });
|
|
8
37
|
const ref: MemoryRef = { memory };
|
|
9
38
|
const db = freshDbState();
|
|
@@ -12,6 +41,11 @@ function setup() {
|
|
|
12
41
|
return { ref, db, imports, buf };
|
|
13
42
|
}
|
|
14
43
|
|
|
44
|
+
function setup() {
|
|
45
|
+
__setDbCatalogForTests(DEFAULT_CATALOG);
|
|
46
|
+
return setupRaw();
|
|
47
|
+
}
|
|
48
|
+
|
|
15
49
|
/** Write bytes at `offset`, returning the `[ptr, len]` pair the imports expect. */
|
|
16
50
|
function put(buf: Buffer, offset: number, data: string): [number, number] {
|
|
17
51
|
const b = Buffer.from(data);
|
|
@@ -19,17 +53,213 @@ function put(buf: Buffer, offset: number, data: string): [number, number] {
|
|
|
19
53
|
return [offset, b.length];
|
|
20
54
|
}
|
|
21
55
|
|
|
22
|
-
function resolve(
|
|
56
|
+
function resolve(
|
|
57
|
+
imports: Record<string, (...a: number[]) => number>,
|
|
58
|
+
buf: Buffer,
|
|
59
|
+
name: string,
|
|
60
|
+
): number {
|
|
23
61
|
const [p, l] = put(buf, 0, name);
|
|
24
62
|
expect(imports['data.resolve_collection'](p, l, 16)).toBe(0);
|
|
25
63
|
return buf.readUInt32LE(16);
|
|
26
64
|
}
|
|
27
65
|
|
|
66
|
+
function wasmWithSection(name: string, payload: Uint8Array): Buffer {
|
|
67
|
+
const nameBytes = Buffer.from(name);
|
|
68
|
+
const sectionPayload = Buffer.concat([
|
|
69
|
+
Buffer.from([nameBytes.length]),
|
|
70
|
+
nameBytes,
|
|
71
|
+
Buffer.from(payload),
|
|
72
|
+
]);
|
|
73
|
+
return Buffer.concat([
|
|
74
|
+
Buffer.from([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x00, sectionPayload.length]),
|
|
75
|
+
sectionPayload,
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function routeKindsSection(routes: readonly (readonly [number, number, string])[]): Buffer {
|
|
80
|
+
const chunks: Buffer[] = [Buffer.alloc(4)];
|
|
81
|
+
chunks[0].writeUInt16LE(1, 0);
|
|
82
|
+
chunks[0].writeUInt16LE(routes.length, 2);
|
|
83
|
+
for (const [method, kind, pattern] of routes) {
|
|
84
|
+
const p = Buffer.from(pattern, 'utf8');
|
|
85
|
+
const header = Buffer.alloc(6);
|
|
86
|
+
header.writeUInt8(method, 0);
|
|
87
|
+
header.writeUInt8(kind, 1);
|
|
88
|
+
header.writeUInt32LE(p.length, 2);
|
|
89
|
+
chunks.push(header, p);
|
|
90
|
+
}
|
|
91
|
+
return Buffer.concat(chunks);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function catalogSectionV2(fillMaxWaitMs: number, fillAllowStale: number): Buffer {
|
|
95
|
+
const chunks: Buffer[] = [];
|
|
96
|
+
const u8 = (v: number) => chunks.push(Buffer.from([v & 0xff]));
|
|
97
|
+
const u16 = (v: number) => {
|
|
98
|
+
const b = Buffer.alloc(2);
|
|
99
|
+
b.writeUInt16LE(v);
|
|
100
|
+
chunks.push(b);
|
|
101
|
+
};
|
|
102
|
+
const u32 = (v: number) => {
|
|
103
|
+
const b = Buffer.alloc(4);
|
|
104
|
+
b.writeUInt32LE(v >>> 0);
|
|
105
|
+
chunks.push(b);
|
|
106
|
+
};
|
|
107
|
+
const str = (s: string) => {
|
|
108
|
+
const b = Buffer.from(s, 'utf8');
|
|
109
|
+
u32(b.length);
|
|
110
|
+
chunks.push(b);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
u16(2); // catalog version
|
|
114
|
+
u16(1); // databases
|
|
115
|
+
str('App');
|
|
116
|
+
u16(1); // collections
|
|
117
|
+
str('users');
|
|
118
|
+
u8(CollectionFamily.Record);
|
|
119
|
+
str('UserId');
|
|
120
|
+
str('User');
|
|
121
|
+
u32(0xaaaa);
|
|
122
|
+
u32(0x1234);
|
|
123
|
+
u32(0); // generation
|
|
124
|
+
u8(5); // replication = localOnly, accepted by the backend ABI
|
|
125
|
+
u8(0); // placement = hashKey
|
|
126
|
+
u32(fillMaxWaitMs);
|
|
127
|
+
u8(fillAllowStale);
|
|
128
|
+
u16(0); // fields
|
|
129
|
+
u16(0); // migrations
|
|
130
|
+
return Buffer.concat(chunks);
|
|
131
|
+
}
|
|
132
|
+
|
|
28
133
|
afterEach(() => {
|
|
29
134
|
__resetDbForTests();
|
|
30
135
|
});
|
|
31
136
|
|
|
32
137
|
describe('toildb dev emulator (record family)', () => {
|
|
138
|
+
it('parses route-kind metadata and only tightens matching mutating routes', () => {
|
|
139
|
+
const wasm = wasmWithSection(
|
|
140
|
+
'toildb.route_kinds',
|
|
141
|
+
routeKindsSection([
|
|
142
|
+
[1, 0, '/api/search'],
|
|
143
|
+
[1, 0, '/api/items/:id'],
|
|
144
|
+
]),
|
|
145
|
+
);
|
|
146
|
+
const routes = parseRouteKinds(wasm);
|
|
147
|
+
|
|
148
|
+
expect(routeKindForRequest(routes, 'POST', '/api/search?q=x')).toBe(DbFunctionKind.Query);
|
|
149
|
+
expect(routeKindForRequest(routes, 'POST', '/api/items/42')).toBe(DbFunctionKind.Query);
|
|
150
|
+
expect(routeKindForRequest(routes, 'GET', '/api/search')).toBeNull();
|
|
151
|
+
expect(routeKindForRequest(routes, 'POST', '/api/items')).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('ignores malformed route-kind metadata like the edge method clamp fallback', () => {
|
|
155
|
+
expect(
|
|
156
|
+
parseRouteKinds(wasmWithSection('toildb.route_kinds', Buffer.from([1, 0, 1, 0, 1]))),
|
|
157
|
+
).toEqual([]);
|
|
158
|
+
expect(
|
|
159
|
+
parseRouteKinds(
|
|
160
|
+
wasmWithSection('toildb.route_kinds', routeKindsSection([[1, 0, 'relative']])),
|
|
161
|
+
),
|
|
162
|
+
).toEqual([]);
|
|
163
|
+
expect(
|
|
164
|
+
parseRouteKinds(
|
|
165
|
+
wasmWithSection('toildb.route_kinds', routeKindsSection([[1, 0, '/snowman-☃']])),
|
|
166
|
+
),
|
|
167
|
+
).toEqual([]);
|
|
168
|
+
|
|
169
|
+
const invalidUtf8 = Buffer.concat([
|
|
170
|
+
Buffer.from([1, 0, 1, 0, 1, 0, 2, 0, 0, 0]),
|
|
171
|
+
Buffer.from([0xc3, 0x28]),
|
|
172
|
+
]);
|
|
173
|
+
expect(parseRouteKinds(wasmWithSection('toildb.route_kinds', invalidUtf8))).toEqual([]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('rejects unknown catalog collections instead of minting arbitrary handles', () => {
|
|
177
|
+
const { imports, buf } = setup();
|
|
178
|
+
const [p, l] = put(buf, 0, 'App/missing');
|
|
179
|
+
buf.writeUInt32LE(0xdeadbeef, 16);
|
|
180
|
+
expect(imports['data.resolve_collection'](p, l, 16)).toBe(-1070);
|
|
181
|
+
expect(buf.readUInt32LE(16)).toBe(0xdeadbeef);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('rejects a present but malformed catalog section', () => {
|
|
185
|
+
setDbCatalog(wasmWithSection('toildb.catalog', Buffer.from([0xff, 0x00, 0xde, 0xad])));
|
|
186
|
+
const { imports, buf } = setupRaw();
|
|
187
|
+
const [p, l] = put(buf, 0, 'App/users');
|
|
188
|
+
|
|
189
|
+
expect(imports['data.resolve_collection'](p, l, 16)).toBe(-1070);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('parses catalog v2 fill policy and backend replication bytes', () => {
|
|
193
|
+
setDbCatalog(wasmWithSection('toildb.catalog', catalogSectionV2(7, 0)));
|
|
194
|
+
const { imports, buf, db } = setupRaw();
|
|
195
|
+
const h = resolve(imports, buf, 'App/users');
|
|
196
|
+
|
|
197
|
+
expect(db.handles[h]).toMatchObject({
|
|
198
|
+
name: 'App/users',
|
|
199
|
+
family: CollectionFamily.Record,
|
|
200
|
+
schemaVersion: 0x1234,
|
|
201
|
+
replication: 5,
|
|
202
|
+
placement: 0,
|
|
203
|
+
fillMaxWaitMs: 7,
|
|
204
|
+
fillAllowStale: false,
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('rejects malformed catalog v2 fill policy', () => {
|
|
209
|
+
setDbCatalog(wasmWithSection('toildb.catalog', catalogSectionV2(7, 2)));
|
|
210
|
+
const { imports, buf } = setupRaw();
|
|
211
|
+
const [p, l] = put(buf, 0, 'App/users');
|
|
212
|
+
|
|
213
|
+
expect(imports['data.resolve_collection'](p, l, 16)).toBe(-1070);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('rejects handles used with the wrong collection family', () => {
|
|
217
|
+
const { imports, buf } = setup();
|
|
218
|
+
const users = resolve(imports, buf, 'App/users');
|
|
219
|
+
const likes = resolve(imports, buf, 'App/likes');
|
|
220
|
+
const [kPtr, kLen] = put(buf, 32, 'k');
|
|
221
|
+
const [vPtr, vLen] = put(buf, 48, 'v');
|
|
222
|
+
|
|
223
|
+
expect(imports['data.counter_add'](users, kPtr, kLen, 1, 0)).toBe(-1010);
|
|
224
|
+
expect(imports['data.create'](likes, kPtr, kLen, vPtr, vLen, 0)).toBe(-1010);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('query-kind dispatch denies writes and write_allowed is false', () => {
|
|
228
|
+
const { imports, buf, db } = setup();
|
|
229
|
+
db.functionKind = DbFunctionKind.Query;
|
|
230
|
+
const h = resolve(imports, buf, 'App/users');
|
|
231
|
+
const [kPtr, kLen] = put(buf, 32, 'u1');
|
|
232
|
+
const [vPtr, vLen] = put(buf, 48, 'hello');
|
|
233
|
+
|
|
234
|
+
expect(imports['data.write_allowed']()).toBe(0);
|
|
235
|
+
expect(imports['data.get'](h, kPtr, kLen)).toBe(-2);
|
|
236
|
+
expect(imports['data.create'](h, kPtr, kLen, vPtr, vLen, 0)).toBe(-1011);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('query/action dispatch deny scan-class imports', () => {
|
|
240
|
+
const { imports, buf, db } = setup();
|
|
241
|
+
const feed = resolve(imports, buf, 'App/feed');
|
|
242
|
+
const rooms = resolve(imports, buf, 'App/rooms');
|
|
243
|
+
const [kPtr, kLen] = put(buf, 32, 'room1');
|
|
244
|
+
const [vPtr, vLen] = put(buf, 48, 'ev1');
|
|
245
|
+
const [mPtr, mLen] = put(buf, 64, 'alice');
|
|
246
|
+
|
|
247
|
+
expect(imports['data.append'](feed, kPtr, kLen, vPtr, vLen, 0)).toBe(0);
|
|
248
|
+
expect(imports['data.membership_add'](rooms, kPtr, kLen, mPtr, mLen, 0)).toBe(0);
|
|
249
|
+
|
|
250
|
+
db.functionKind = DbFunctionKind.Query;
|
|
251
|
+
expect(imports['data.latest'](feed, kPtr, kLen, 10)).toBe(-1011);
|
|
252
|
+
expect(imports['data.membership_list'](rooms, kPtr, kLen, 10)).toBe(-1011);
|
|
253
|
+
|
|
254
|
+
db.functionKind = DbFunctionKind.Action;
|
|
255
|
+
expect(imports['data.latest'](feed, kPtr, kLen, 10)).toBe(-1011);
|
|
256
|
+
expect(imports['data.membership_list'](rooms, kPtr, kLen, 10)).toBe(-1011);
|
|
257
|
+
|
|
258
|
+
db.functionKind = DbFunctionKind.Job;
|
|
259
|
+
expect(imports['data.latest'](feed, kPtr, kLen, 10)).toBeGreaterThan(0);
|
|
260
|
+
expect(imports['data.membership_list'](rooms, kPtr, kLen, 10)).toBeGreaterThan(0);
|
|
261
|
+
});
|
|
262
|
+
|
|
33
263
|
it('resolve + create + get + take_result round-trips', () => {
|
|
34
264
|
const { imports, buf } = setup();
|
|
35
265
|
const h = resolve(imports, buf, 'App/users');
|
|
@@ -37,7 +267,7 @@ describe('toildb dev emulator (record family)', () => {
|
|
|
37
267
|
const [vPtr, vLen] = put(buf, 48, 'hello');
|
|
38
268
|
|
|
39
269
|
expect(imports['data.create'](h, kPtr, kLen, vPtr, vLen, 0)).toBe(0);
|
|
40
|
-
expect(imports['data.create'](h, kPtr, kLen, vPtr, vLen, 0)).toBe(-
|
|
270
|
+
expect(imports['data.create'](h, kPtr, kLen, vPtr, vLen, 0)).toBe(-1003); // AlreadyExists
|
|
41
271
|
expect(imports['data.exists'](h, kPtr, kLen)).toBe(1);
|
|
42
272
|
|
|
43
273
|
expect(imports['data.get'](h, kPtr, kLen)).toBe(5);
|
|
@@ -59,12 +289,33 @@ describe('toildb dev emulator (record family)', () => {
|
|
|
59
289
|
expect(buf.toString('utf8', 128, 134)).toBe('world!');
|
|
60
290
|
});
|
|
61
291
|
|
|
292
|
+
it('record patch idempotency replays and rejects request mismatch', () => {
|
|
293
|
+
const { imports, buf } = setup();
|
|
294
|
+
const h = resolve(imports, buf, 'App/users');
|
|
295
|
+
const [kPtr, kLen] = put(buf, 32, 'u1');
|
|
296
|
+
const [vPtr, vLen] = put(buf, 48, 'hello');
|
|
297
|
+
const [idemPtr] = put(buf, 64, '1234567890abcdef');
|
|
298
|
+
imports['data.create'](h, kPtr, kLen, vPtr, vLen, 0);
|
|
299
|
+
|
|
300
|
+
const [pPtr, pLen] = put(buf, 96, 'world!');
|
|
301
|
+
expect(imports['data.patch'](h, kPtr, kLen, pPtr, pLen, idemPtr)).toBe(6);
|
|
302
|
+
expect(imports['data.patch'](h, kPtr, kLen, pPtr, pLen, idemPtr)).toBe(6);
|
|
303
|
+
expect(imports['data.take_result'](128, 64)).toBe(6);
|
|
304
|
+
expect(buf.toString('utf8', 128, 134)).toBe('world!');
|
|
305
|
+
|
|
306
|
+
const [otherPtr, otherLen] = put(buf, 160, 'again!');
|
|
307
|
+
expect(imports['data.patch'](h, kPtr, kLen, otherPtr, otherLen, idemPtr)).toBe(-1004);
|
|
308
|
+
expect(imports['data.get'](h, kPtr, kLen)).toBe(6);
|
|
309
|
+
expect(imports['data.take_result'](192, 64)).toBe(6);
|
|
310
|
+
expect(buf.toString('utf8', 192, 198)).toBe('world!');
|
|
311
|
+
});
|
|
312
|
+
|
|
62
313
|
it('patch on a missing key is NotFound', () => {
|
|
63
314
|
const { imports, buf } = setup();
|
|
64
315
|
const h = resolve(imports, buf, 'App/users');
|
|
65
316
|
const [kPtr, kLen] = put(buf, 32, 'ghost');
|
|
66
317
|
const [pPtr, pLen] = put(buf, 48, 'x');
|
|
67
|
-
expect(imports['data.patch'](h, kPtr, kLen, pPtr, pLen, 0)).toBe(-
|
|
318
|
+
expect(imports['data.patch'](h, kPtr, kLen, pPtr, pLen, 0)).toBe(-2); // NotFound -> ABSENT
|
|
68
319
|
});
|
|
69
320
|
|
|
70
321
|
it('consume-once get_delete deletes exactly once', () => {
|
|
@@ -81,6 +332,24 @@ describe('toildb dev emulator (record family)', () => {
|
|
|
81
332
|
expect(imports['data.get_delete'](h, kPtr, kLen, 0)).toBe(-2);
|
|
82
333
|
});
|
|
83
334
|
|
|
335
|
+
it('record get_delete idempotency replays the consumed value', () => {
|
|
336
|
+
const { imports, buf } = setup();
|
|
337
|
+
const h = resolve(imports, buf, 'App/challenges');
|
|
338
|
+
const [kPtr, kLen] = put(buf, 32, 'chal');
|
|
339
|
+
const [vPtr, vLen] = put(buf, 48, 'nonce');
|
|
340
|
+
const [idemPtr] = put(buf, 80, 'consume-idem-001');
|
|
341
|
+
imports['data.create'](h, kPtr, kLen, vPtr, vLen, 0);
|
|
342
|
+
|
|
343
|
+
expect(imports['data.get_delete'](h, kPtr, kLen, idemPtr)).toBe(5);
|
|
344
|
+
expect(imports['data.take_result'](96, 64)).toBe(5);
|
|
345
|
+
expect(buf.toString('utf8', 96, 101)).toBe('nonce');
|
|
346
|
+
|
|
347
|
+
expect(imports['data.get_delete'](h, kPtr, kLen, idemPtr)).toBe(5);
|
|
348
|
+
expect(imports['data.take_result'](128, 64)).toBe(5);
|
|
349
|
+
expect(buf.toString('utf8', 128, 133)).toBe('nonce');
|
|
350
|
+
expect(imports['data.get'](h, kPtr, kLen)).toBe(-2);
|
|
351
|
+
});
|
|
352
|
+
|
|
84
353
|
it('absent / invalid handle / buffer-too-small return the edge codes', () => {
|
|
85
354
|
const { imports, buf } = setup();
|
|
86
355
|
const h = resolve(imports, buf, 'App/users');
|
|
@@ -116,6 +385,19 @@ describe('toildb dev emulator (record family)', () => {
|
|
|
116
385
|
expect(buf.toString('utf8', 200, 206)).toBe('user_1');
|
|
117
386
|
});
|
|
118
387
|
|
|
388
|
+
it('unique claim: same owner must replay only with the same idempotency key', () => {
|
|
389
|
+
const { imports, buf } = setup();
|
|
390
|
+
const h = resolve(imports, buf, 'App/usernames');
|
|
391
|
+
const [kPtr, kLen] = put(buf, 32, 'grace');
|
|
392
|
+
const [u1Ptr, u1Len] = put(buf, 48, 'user_1');
|
|
393
|
+
const [idem1Ptr] = put(buf, 80, 'idem-claim-0001');
|
|
394
|
+
const [idem2Ptr] = put(buf, 112, 'idem-claim-0002');
|
|
395
|
+
|
|
396
|
+
expect(imports['data.unique_claim'](h, kPtr, kLen, u1Ptr, u1Len, idem1Ptr)).toBe(0);
|
|
397
|
+
expect(imports['data.unique_claim'](h, kPtr, kLen, u1Ptr, u1Len, idem1Ptr)).toBe(2);
|
|
398
|
+
expect(imports['data.unique_claim'](h, kPtr, kLen, u1Ptr, u1Len, idem2Ptr)).toBe(-1004);
|
|
399
|
+
});
|
|
400
|
+
|
|
119
401
|
it('unique release: only the owner may release', () => {
|
|
120
402
|
const { imports, buf } = setup();
|
|
121
403
|
const h = resolve(imports, buf, 'App/usernames');
|
|
@@ -124,7 +406,7 @@ describe('toildb dev emulator (record family)', () => {
|
|
|
124
406
|
const [u2Ptr, u2Len] = put(buf, 64, 'user_2');
|
|
125
407
|
imports['data.unique_claim'](h, kPtr, kLen, u1Ptr, u1Len, 0);
|
|
126
408
|
|
|
127
|
-
expect(imports['data.unique_release'](h, kPtr, kLen, u2Ptr, u2Len, 0)).toBe(-
|
|
409
|
+
expect(imports['data.unique_release'](h, kPtr, kLen, u2Ptr, u2Len, 0)).toBe(-1004); // not owner
|
|
128
410
|
expect(imports['data.unique_release'](h, kPtr, kLen, u1Ptr, u1Len, 0)).toBe(0); // owner releases
|
|
129
411
|
expect(imports['data.unique_lookup'](h, kPtr, kLen)).toBe(-2); // gone
|
|
130
412
|
});
|
|
@@ -162,6 +444,7 @@ describe('toildb dev emulator (record family)', () => {
|
|
|
162
444
|
got.push(null);
|
|
163
445
|
continue;
|
|
164
446
|
}
|
|
447
|
+
p += 4; // per-item schema_version (0 in dev)
|
|
165
448
|
const len = buf.readUInt32LE(p);
|
|
166
449
|
p += 4;
|
|
167
450
|
got.push(buf.toString('utf8', p, p + len));
|
|
@@ -194,6 +477,7 @@ describe('toildb dev emulator (record family)', () => {
|
|
|
194
477
|
expect(count).toBe(2);
|
|
195
478
|
const out: string[] = [];
|
|
196
479
|
for (let i = 0; i < count; i++) {
|
|
480
|
+
off += 4; // per-item schema_version (0 in dev)
|
|
197
481
|
const len = buf.readUInt32LE(off);
|
|
198
482
|
off += 4;
|
|
199
483
|
out.push(buf.toString('utf8', off, off + len));
|
|
@@ -250,6 +534,14 @@ describe('toildb dev emulator (record family)', () => {
|
|
|
250
534
|
imports['data.take_result'](72, 8);
|
|
251
535
|
expect(buf.readBigInt64LE(72)).toBe(3n);
|
|
252
536
|
|
|
537
|
+
const [idemPtr] = put(buf, 96, 'counter-idem-001');
|
|
538
|
+
expect(imports['data.counter_add'](h, kPtr, kLen, 7, idemPtr)).toBe(0);
|
|
539
|
+
expect(imports['data.counter_add'](h, kPtr, kLen, 7, idemPtr)).toBe(0);
|
|
540
|
+
expect(imports['data.counter_add'](h, kPtr, kLen, 8, idemPtr)).toBe(-1004);
|
|
541
|
+
expect(imports['data.counter_get'](h, kPtr, kLen)).toBe(8);
|
|
542
|
+
imports['data.take_result'](104, 8);
|
|
543
|
+
expect(buf.readBigInt64LE(104)).toBe(10n);
|
|
544
|
+
|
|
253
545
|
// invalid handle still rejected
|
|
254
546
|
expect(imports['data.counter_add'](999, kPtr, kLen, 1, 0)).toBe(-1001);
|
|
255
547
|
});
|
|
@@ -274,6 +566,7 @@ describe('toildb dev emulator (record family)', () => {
|
|
|
274
566
|
expect(count).toBe(2);
|
|
275
567
|
const out: string[] = [];
|
|
276
568
|
for (let i = 0; i < count; i++) {
|
|
569
|
+
off += 4; // per-item schema_version (0 in dev)
|
|
277
570
|
const len = buf.readUInt32LE(off);
|
|
278
571
|
off += 4;
|
|
279
572
|
out.push(buf.toString('utf8', off, off + len));
|
|
@@ -301,6 +594,39 @@ describe('toildb dev emulator (record family)', () => {
|
|
|
301
594
|
// the same logical key in another collection is absent.
|
|
302
595
|
expect(imports['data.get'](posts, kPtr, kLen)).toBe(-2);
|
|
303
596
|
});
|
|
597
|
+
|
|
598
|
+
it('append_once dedups on eventId; enqueue replaces an existing record', () => {
|
|
599
|
+
const { imports, buf } = setup();
|
|
600
|
+
const feed = resolve(imports, buf, 'App/feed');
|
|
601
|
+
const [kPtr, kLen] = put(buf, 32, 'room1');
|
|
602
|
+
const [idPtr, idLen] = put(buf, 64, 'evt-1');
|
|
603
|
+
const [evPtr, evLen] = put(buf, 96, 'hello');
|
|
604
|
+
// first appendOnce appends (1); the same id is a no-op (0); a new id appends (1).
|
|
605
|
+
expect(imports['data.append_once'](feed, kPtr, kLen, idPtr, idLen, evPtr, evLen)).toBe(1);
|
|
606
|
+
expect(imports['data.append_once'](feed, kPtr, kLen, idPtr, idLen, evPtr, evLen)).toBe(0);
|
|
607
|
+
const [id2P, id2L] = put(buf, 128, 'evt-2');
|
|
608
|
+
expect(imports['data.append_once'](feed, kPtr, kLen, id2P, id2L, evPtr, evLen)).toBe(1);
|
|
609
|
+
const [appendIdemPtr] = put(buf, 144, 'append-idem-0001');
|
|
610
|
+
expect(imports['data.append'](feed, kPtr, kLen, evPtr, evLen, appendIdemPtr)).toBe(0);
|
|
611
|
+
expect(imports['data.append'](feed, kPtr, kLen, evPtr, evLen, appendIdemPtr)).toBe(0);
|
|
612
|
+
// latest frames exactly 2 events (the duplicate did not double-append).
|
|
613
|
+
const total = imports['data.latest'](feed, kPtr, kLen, 10);
|
|
614
|
+
expect(total).toBeGreaterThan(0);
|
|
615
|
+
imports['data.take_result'](512, total);
|
|
616
|
+
expect(buf.readUInt32LE(512)).toBe(3);
|
|
617
|
+
|
|
618
|
+
// enqueue: absent -> ABSENT (-2); after create -> replaces (0); get sees the new value.
|
|
619
|
+
const docs = resolve(imports, buf, 'App/docs');
|
|
620
|
+
const [dkP, dkL] = put(buf, 160, 'doc1');
|
|
621
|
+
const [v1P, v1L] = put(buf, 192, 'AAAA');
|
|
622
|
+
expect(imports['data.enqueue'](docs, dkP, dkL, v1P, v1L, 0)).toBe(-2);
|
|
623
|
+
expect(imports['data.create'](docs, dkP, dkL, v1P, v1L, 0)).toBe(0);
|
|
624
|
+
const [v2P, v2L] = put(buf, 224, 'BBBB');
|
|
625
|
+
expect(imports['data.enqueue'](docs, dkP, dkL, v2P, v2L, 0)).toBe(0);
|
|
626
|
+
expect(imports['data.get'](docs, dkP, dkL)).toBe(4);
|
|
627
|
+
imports['data.take_result'](256, 4);
|
|
628
|
+
expect(buf.toString('utf8', 256, 260)).toBe('BBBB');
|
|
629
|
+
});
|
|
304
630
|
});
|
|
305
631
|
|
|
306
632
|
type Imports = Record<string, (...args: number[]) => number>;
|
|
@@ -332,9 +658,19 @@ describe('toildb dev emulator (capacity family)', () => {
|
|
|
332
658
|
expect(id > 0n).toBe(true);
|
|
333
659
|
expect(avail(imports, buf, h, kPtr, kLen)).toBe(7n);
|
|
334
660
|
|
|
661
|
+
const [idemPtr] = put(buf, 64, 'reserve-idem-001');
|
|
662
|
+
expect(imports['data.capacity_reserve'](h, kPtr, kLen, 2, 60000, idemPtr)).toBe(8);
|
|
663
|
+
expect(imports['data.take_result'](520, 16)).toBe(8);
|
|
664
|
+
const idemId = buf.readBigUInt64LE(520);
|
|
665
|
+
expect(imports['data.capacity_reserve'](h, kPtr, kLen, 2, 60000, idemPtr)).toBe(8);
|
|
666
|
+
expect(imports['data.take_result'](528, 16)).toBe(8);
|
|
667
|
+
expect(buf.readBigUInt64LE(528)).toBe(idemId);
|
|
668
|
+
expect(imports['data.capacity_reserve'](h, kPtr, kLen, 1, 60000, idemPtr)).toBe(-1004);
|
|
669
|
+
expect(avail(imports, buf, h, kPtr, kLen)).toBe(5n);
|
|
670
|
+
|
|
335
671
|
// cancel returns the hold -> back to 10; a double-cancel is a no-op.
|
|
336
672
|
expect(imports['data.capacity_cancel'](h, kPtr, kLen, Number(id), 0)).toBe(1);
|
|
337
|
-
expect(avail(imports, buf, h, kPtr, kLen)).toBe(
|
|
673
|
+
expect(avail(imports, buf, h, kPtr, kLen)).toBe(8n);
|
|
338
674
|
expect(imports['data.capacity_cancel'](h, kPtr, kLen, Number(id), 0)).toBe(0);
|
|
339
675
|
|
|
340
676
|
// reserve 4 then confirm -> a permanent consume; available holds at 6.
|
|
@@ -342,10 +678,10 @@ describe('toildb dev emulator (capacity family)', () => {
|
|
|
342
678
|
imports['data.take_result'](512, 16);
|
|
343
679
|
const id2 = Number(buf.readBigUInt64LE(512));
|
|
344
680
|
expect(imports['data.capacity_confirm'](h, kPtr, kLen, id2, 0)).toBe(1);
|
|
345
|
-
expect(avail(imports, buf, h, kPtr, kLen)).toBe(
|
|
346
|
-
// a confirmed
|
|
681
|
+
expect(avail(imports, buf, h, kPtr, kLen)).toBe(4n);
|
|
682
|
+
// a confirmed sale cannot be cancelled (0); re-confirm is idempotent (1).
|
|
347
683
|
expect(imports['data.capacity_cancel'](h, kPtr, kLen, id2, 0)).toBe(0);
|
|
348
|
-
expect(imports['data.capacity_confirm'](h, kPtr, kLen, id2, 0)).toBe(
|
|
684
|
+
expect(imports['data.capacity_confirm'](h, kPtr, kLen, id2, 0)).toBe(1);
|
|
349
685
|
});
|
|
350
686
|
|
|
351
687
|
it('never oversells (a reserve beyond available is refused)', () => {
|
|
@@ -357,8 +693,168 @@ describe('toildb dev emulator (capacity family)', () => {
|
|
|
357
693
|
// a hold for all 5 succeeds; a further hold for 1 is refused (-2 -> guest 0).
|
|
358
694
|
expect(imports['data.capacity_reserve'](h, kPtr, kLen, 5, 60000, 0)).toBe(8);
|
|
359
695
|
expect(imports['data.capacity_reserve'](h, kPtr, kLen, 1, 60000, 0)).toBe(-2);
|
|
360
|
-
// a non-positive amount is
|
|
361
|
-
expect(imports['data.capacity_reserve'](h, kPtr, kLen, 0, 60000, 0)).toBe(-
|
|
696
|
+
// a non-positive amount is a typed error (BadAmount), invalid handle rejected.
|
|
697
|
+
expect(imports['data.capacity_reserve'](h, kPtr, kLen, 0, 60000, 0)).toBe(-1006);
|
|
362
698
|
expect(imports['data.capacity_available'](999, kPtr, kLen)).toBe(-1001);
|
|
363
699
|
});
|
|
700
|
+
|
|
701
|
+
it('does not cache an idempotent insufficient reserve forever', () => {
|
|
702
|
+
const { imports, buf } = setup();
|
|
703
|
+
const h = resolve(imports, buf, 'App/tickets');
|
|
704
|
+
const [kPtr, kLen] = put(buf, 32, 'late-restock');
|
|
705
|
+
const [idemPtr] = put(buf, 64, 'reserve-idem-001');
|
|
706
|
+
|
|
707
|
+
expect(imports['data.capacity_reserve'](h, kPtr, kLen, 1, 60000, idemPtr)).toBe(-2);
|
|
708
|
+
expect(imports['data.capacity_set_total'](h, kPtr, kLen, 1, 0)).toBe(0);
|
|
709
|
+
expect(imports['data.capacity_reserve'](h, kPtr, kLen, 1, 60000, idemPtr)).toBe(8);
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
describe('toildb dev emulator (migration + persistence)', () => {
|
|
714
|
+
const rsv = (imports: Imports): bigint =>
|
|
715
|
+
(imports['data.result_schema_version'] as () => bigint)();
|
|
716
|
+
|
|
717
|
+
it('stamps writes with the catalog schema_version and surfaces it on read', () => {
|
|
718
|
+
const { imports, buf } = setup();
|
|
719
|
+
__setDbCatalogForTests({ 'App/users': 0x1234 });
|
|
720
|
+
const h = resolve(imports, buf, 'App/users');
|
|
721
|
+
const [kPtr, kLen] = put(buf, 32, 'u1');
|
|
722
|
+
const [vPtr, vLen] = put(buf, 64, 'data');
|
|
723
|
+
imports['data.create'](h, kPtr, kLen, vPtr, vLen, 0);
|
|
724
|
+
expect(imports['data.get'](h, kPtr, kLen)).toBe(4);
|
|
725
|
+
expect(rsv(imports)).toBe(0x1234n); // the woven decoder dispatches on this
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('an evolved @data type leaves old rows stamped with the OLD version', () => {
|
|
729
|
+
const { imports, buf } = setup();
|
|
730
|
+
__setDbCatalogForTests({ 'App/users': 100 }); // version A
|
|
731
|
+
const h = resolve(imports, buf, 'App/users');
|
|
732
|
+
const [kPtr, kLen] = put(buf, 32, 'u1');
|
|
733
|
+
const [vPtr, vLen] = put(buf, 64, 'old');
|
|
734
|
+
imports['data.create'](h, kPtr, kLen, vPtr, vLen, 0);
|
|
735
|
+
// the @data type evolves + the wasm rebuilds -> the catalog version changes.
|
|
736
|
+
__setDbCatalogForTests({ 'App/users': 200 }); // version B
|
|
737
|
+
// a read of the existing row still reports the OLD version -> guest migrates it.
|
|
738
|
+
imports['data.get'](h, kPtr, kLen);
|
|
739
|
+
expect(rsv(imports)).toBe(100n);
|
|
740
|
+
// a NEW write stamps the current version.
|
|
741
|
+
const [k2, kl2] = put(buf, 96, 'u2');
|
|
742
|
+
imports['data.create'](h, k2, kl2, vPtr, vLen, 0);
|
|
743
|
+
imports['data.get'](h, k2, kl2);
|
|
744
|
+
expect(rsv(imports)).toBe(200n);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('persists data + versions to disk and reloads them', () => {
|
|
748
|
+
const dir = mkdtempSync(join(tmpdir(), 'toildb-'));
|
|
749
|
+
const file = join(dir, 'devdata.json');
|
|
750
|
+
try {
|
|
751
|
+
const a = setup();
|
|
752
|
+
__setDbCatalogForTests({ 'App/users': 777 });
|
|
753
|
+
configureDbPersistence(file);
|
|
754
|
+
const h = resolve(a.imports, a.buf, 'App/users');
|
|
755
|
+
const [kPtr, kLen] = put(a.buf, 32, 'u1');
|
|
756
|
+
const [vPtr, vLen] = put(a.buf, 64, 'persisted');
|
|
757
|
+
a.imports['data.create'](h, kPtr, kLen, vPtr, vLen, 0);
|
|
758
|
+
persistDb();
|
|
759
|
+
|
|
760
|
+
// simulate a restart: wipe memory + catalog, then reload from disk.
|
|
761
|
+
__resetDbForTests();
|
|
762
|
+
configureDbPersistence(file);
|
|
763
|
+
const b = setup();
|
|
764
|
+
const h2 = resolve(b.imports, b.buf, 'App/users');
|
|
765
|
+
const [k2, kl2] = put(b.buf, 32, 'u1');
|
|
766
|
+
expect(b.imports['data.get'](h2, k2, kl2)).toBe(9); // "persisted" survived restart
|
|
767
|
+
expect(rsv(b.imports)).toBe(777n); // and so did its schema_version stamp
|
|
768
|
+
} finally {
|
|
769
|
+
rmSync(dir, { recursive: true, force: true });
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it('persists counter idempotency across devserver restart', () => {
|
|
774
|
+
const dir = mkdtempSync(join(tmpdir(), 'toildb-'));
|
|
775
|
+
const file = join(dir, 'devdata.json');
|
|
776
|
+
try {
|
|
777
|
+
const a = setup();
|
|
778
|
+
configureDbPersistence(file);
|
|
779
|
+
const h = resolve(a.imports, a.buf, 'App/likes');
|
|
780
|
+
const [kPtr, kLen] = put(a.buf, 32, 'post-1');
|
|
781
|
+
const [idemPtr] = put(a.buf, 64, 'counter-idem-001');
|
|
782
|
+
expect(a.imports['data.counter_add'](h, kPtr, kLen, 7, idemPtr)).toBe(0);
|
|
783
|
+
persistDb();
|
|
784
|
+
|
|
785
|
+
__resetDbForTests();
|
|
786
|
+
configureDbPersistence(file);
|
|
787
|
+
const b = setup();
|
|
788
|
+
const h2 = resolve(b.imports, b.buf, 'App/likes');
|
|
789
|
+
const [k2, kl2] = put(b.buf, 32, 'post-1');
|
|
790
|
+
const [idem2] = put(b.buf, 64, 'counter-idem-001');
|
|
791
|
+
expect(b.imports['data.counter_add'](h2, k2, kl2, 7, idem2)).toBe(0);
|
|
792
|
+
expect(b.imports['data.counter_add'](h2, k2, kl2, 8, idem2)).toBe(-1004);
|
|
793
|
+
expect(b.imports['data.counter_get'](h2, k2, kl2)).toBe(8);
|
|
794
|
+
expect(b.imports['data.take_result'](96, 8)).toBe(8);
|
|
795
|
+
expect(b.buf.readBigInt64LE(96)).toBe(7n);
|
|
796
|
+
} finally {
|
|
797
|
+
rmSync(dir, { recursive: true, force: true });
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
it('persists unique claim idempotency across devserver restart', () => {
|
|
802
|
+
const dir = mkdtempSync(join(tmpdir(), 'toildb-'));
|
|
803
|
+
const file = join(dir, 'devdata.json');
|
|
804
|
+
try {
|
|
805
|
+
const a = setup();
|
|
806
|
+
configureDbPersistence(file);
|
|
807
|
+
const h = resolve(a.imports, a.buf, 'App/usernames');
|
|
808
|
+
const [kPtr, kLen] = put(a.buf, 32, 'hopper');
|
|
809
|
+
const [u1Ptr, u1Len] = put(a.buf, 48, 'user_1');
|
|
810
|
+
const [idemPtr] = put(a.buf, 80, 'idem-claim-0001');
|
|
811
|
+
expect(a.imports['data.unique_claim'](h, kPtr, kLen, u1Ptr, u1Len, idemPtr)).toBe(0);
|
|
812
|
+
persistDb();
|
|
813
|
+
|
|
814
|
+
__resetDbForTests();
|
|
815
|
+
configureDbPersistence(file);
|
|
816
|
+
const b = setup();
|
|
817
|
+
const h2 = resolve(b.imports, b.buf, 'App/usernames');
|
|
818
|
+
const [k2, kl2] = put(b.buf, 32, 'hopper');
|
|
819
|
+
const [u2, u2l] = put(b.buf, 48, 'user_1');
|
|
820
|
+
const [sameIdem] = put(b.buf, 80, 'idem-claim-0001');
|
|
821
|
+
const [otherIdem] = put(b.buf, 112, 'idem-claim-0002');
|
|
822
|
+
expect(b.imports['data.unique_claim'](h2, k2, kl2, u2, u2l, sameIdem)).toBe(2);
|
|
823
|
+
expect(b.imports['data.unique_claim'](h2, k2, kl2, u2, u2l, otherIdem)).toBe(-1004);
|
|
824
|
+
} finally {
|
|
825
|
+
rmSync(dir, { recursive: true, force: true });
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it('replays idempotent capacity reservations across devserver restart', () => {
|
|
830
|
+
const dir = mkdtempSync(join(tmpdir(), 'toildb-'));
|
|
831
|
+
const file = join(dir, 'devdata.json');
|
|
832
|
+
try {
|
|
833
|
+
const a = setup();
|
|
834
|
+
configureDbPersistence(file);
|
|
835
|
+
const h = resolve(a.imports, a.buf, 'App/seats');
|
|
836
|
+
const [kPtr, kLen] = put(a.buf, 32, 'showB');
|
|
837
|
+
const [idemPtr] = put(a.buf, 64, 'reserve-idem-001');
|
|
838
|
+
expect(a.imports['data.capacity_set_total'](h, kPtr, kLen, 3, 0)).toBe(0);
|
|
839
|
+
expect(a.imports['data.capacity_reserve'](h, kPtr, kLen, 2, 60000, idemPtr)).toBe(8);
|
|
840
|
+
expect(a.imports['data.take_result'](96, 16)).toBe(8);
|
|
841
|
+
const firstId = a.buf.readBigUInt64LE(96);
|
|
842
|
+
persistDb();
|
|
843
|
+
|
|
844
|
+
__resetDbForTests();
|
|
845
|
+
configureDbPersistence(file);
|
|
846
|
+
const b = setup();
|
|
847
|
+
const h2 = resolve(b.imports, b.buf, 'App/seats');
|
|
848
|
+
const [k2, kl2] = put(b.buf, 32, 'showB');
|
|
849
|
+
const [idem2] = put(b.buf, 64, 'reserve-idem-001');
|
|
850
|
+
expect(b.imports['data.capacity_reserve'](h2, k2, kl2, 2, 60000, idem2)).toBe(8);
|
|
851
|
+
expect(b.imports['data.take_result'](96, 16)).toBe(8);
|
|
852
|
+
expect(b.buf.readBigUInt64LE(96)).toBe(firstId);
|
|
853
|
+
expect(b.imports['data.capacity_available'](h2, k2, kl2)).toBe(8);
|
|
854
|
+
expect(b.imports['data.take_result'](112, 16)).toBe(8);
|
|
855
|
+
expect(b.buf.readBigInt64LE(112)).toBe(1n);
|
|
856
|
+
} finally {
|
|
857
|
+
rmSync(dir, { recursive: true, force: true });
|
|
858
|
+
}
|
|
859
|
+
});
|
|
364
860
|
});
|
|
@@ -10,7 +10,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
10
10
|
|
|
11
11
|
import { ml_kem768 } from '@dacely/noble-post-quantum/ml-kem.js';
|
|
12
12
|
|
|
13
|
-
import { buildCryptoImports, freshCryptoState } from '../src/devserver/crypto.js';
|
|
13
|
+
import { buildCryptoImports, freshCryptoState } from '../src/devserver/runtime/crypto.js';
|
|
14
14
|
|
|
15
15
|
type Ref = { memory: WebAssembly.Memory | null };
|
|
16
16
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
buildHostImports,
|
|
5
|
+
freshDispatchState,
|
|
6
|
+
type MemoryRef,
|
|
7
|
+
} from '../src/devserver/runtime/host.js';
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* The dev host warns (once) when the guest reads a framework auth secret that is unset, since the
|