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
|
@@ -0,0 +1,1633 @@
|
|
|
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
|
+
Delete,
|
|
190
|
+
GetDelete,
|
|
191
|
+
Enqueue,
|
|
192
|
+
Append,
|
|
193
|
+
AppendOnce,
|
|
194
|
+
Latest,
|
|
195
|
+
CounterGet,
|
|
196
|
+
CounterAdd,
|
|
197
|
+
MembershipContains,
|
|
198
|
+
MembershipAdd,
|
|
199
|
+
MembershipRemove,
|
|
200
|
+
MembershipList,
|
|
201
|
+
UniqueLookup,
|
|
202
|
+
UniqueClaim,
|
|
203
|
+
UniqueRelease,
|
|
204
|
+
CapacityAvailable,
|
|
205
|
+
CapacityReserve,
|
|
206
|
+
CapacityConfirm,
|
|
207
|
+
CapacityCancel,
|
|
208
|
+
ViewGet,
|
|
209
|
+
ViewPublish,
|
|
210
|
+
CapacitySetTotal,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isReadOp(op: DbOp): boolean {
|
|
214
|
+
return (
|
|
215
|
+
op === DbOp.Get ||
|
|
216
|
+
op === DbOp.GetMany ||
|
|
217
|
+
op === DbOp.Exists ||
|
|
218
|
+
op === DbOp.ViewGet ||
|
|
219
|
+
op === DbOp.CounterGet ||
|
|
220
|
+
op === DbOp.MembershipContains ||
|
|
221
|
+
op === DbOp.MembershipList ||
|
|
222
|
+
op === DbOp.UniqueLookup ||
|
|
223
|
+
op === DbOp.Latest ||
|
|
224
|
+
op === DbOp.CapacityAvailable
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isScanOp(op: DbOp): boolean {
|
|
229
|
+
return op === DbOp.Latest || op === DbOp.MembershipList;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function kindAllows(kind: DbFunctionKind, op: DbOp): boolean {
|
|
233
|
+
if (kind === DbFunctionKind.Query) return isReadOp(op) && !isScanOp(op);
|
|
234
|
+
if (kind === DbFunctionKind.Action) {
|
|
235
|
+
return (
|
|
236
|
+
(isReadOp(op) && !isScanOp(op)) ||
|
|
237
|
+
op === DbOp.Create ||
|
|
238
|
+
op === DbOp.Patch ||
|
|
239
|
+
op === DbOp.Delete ||
|
|
240
|
+
op === DbOp.GetDelete ||
|
|
241
|
+
op === DbOp.Enqueue ||
|
|
242
|
+
op === DbOp.Append ||
|
|
243
|
+
op === DbOp.AppendOnce ||
|
|
244
|
+
op === DbOp.CounterAdd ||
|
|
245
|
+
op === DbOp.MembershipAdd ||
|
|
246
|
+
op === DbOp.MembershipRemove ||
|
|
247
|
+
op === DbOp.UniqueClaim ||
|
|
248
|
+
op === DbOp.UniqueRelease ||
|
|
249
|
+
op === DbOp.CapacityReserve ||
|
|
250
|
+
op === DbOp.CapacityConfirm ||
|
|
251
|
+
op === DbOp.CapacityCancel
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if (kind === DbFunctionKind.Derive)
|
|
255
|
+
return (
|
|
256
|
+
isReadOp(op) || op === DbOp.ViewPublish || op === DbOp.Append || op === DbOp.CounterAdd
|
|
257
|
+
);
|
|
258
|
+
if (kind === DbFunctionKind.Job) return true;
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function collForOp(
|
|
263
|
+
db: DbDevState,
|
|
264
|
+
handle: number,
|
|
265
|
+
op: DbOp,
|
|
266
|
+
...families: CollectionFamily[]
|
|
267
|
+
): DevCollectionHandle | number {
|
|
268
|
+
const coll = collOfFamily(db, handle, ...families);
|
|
269
|
+
if (typeof coll === 'number') return coll;
|
|
270
|
+
return kindAllows(db.functionKind, op) ? coll : OP_NOT_ALLOWED_IN_KIND;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
type CatalogSeedEntry =
|
|
274
|
+
| number
|
|
275
|
+
| {
|
|
276
|
+
family?: number;
|
|
277
|
+
schemaVersion?: number;
|
|
278
|
+
replication?: number;
|
|
279
|
+
placement?: number;
|
|
280
|
+
fillMaxWaitMs?: number;
|
|
281
|
+
fillAllowStale?: boolean;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The single-process dev data store: the seven ToilDB families, their per-row
|
|
286
|
+
* schema_versions, the loaded wasm's catalog, and optional on-disk persistence.
|
|
287
|
+
* Process-lifetime, shared across dispatches via the module singleton {@link devDb}.
|
|
288
|
+
*/
|
|
289
|
+
export class DevDatabase {
|
|
290
|
+
/** Process-lifetime store: `"collection\0keyLatin1"` -> value. Shared across dispatches. */
|
|
291
|
+
private readonly store = new Map<string, Buffer>();
|
|
292
|
+
/** Record-family idempotency claims/outcomes: collection+key+op+idem -> row. */
|
|
293
|
+
private readonly recordIdem = new Map<string, RecordIdemRow>();
|
|
294
|
+
/** Unique-claim request idempotency bytes: `"collection\0key"` -> hex idem. */
|
|
295
|
+
private readonly uniqueIdem = new Map<string, string>();
|
|
296
|
+
/** View family: `"collection\0key"` -> the latest published view blob. */
|
|
297
|
+
private readonly views = new Map<string, Buffer>();
|
|
298
|
+
/** Membership family: `"collection\0setKey"` -> (memberLatin1 -> member bytes). */
|
|
299
|
+
private readonly members = new Map<string, Map<string, Buffer>>();
|
|
300
|
+
/** Counter family: `"collection\0key"` -> saturating i64 sum of deltas. */
|
|
301
|
+
private readonly counters = new Map<string, bigint>();
|
|
302
|
+
/** Counter idempotency: collection+key+idem -> original delta. */
|
|
303
|
+
private readonly counterIdem = new Map<string, bigint>();
|
|
304
|
+
/** Events family: `"collection\0key"` -> append-ordered event blobs (oldest first). */
|
|
305
|
+
private readonly events = new Map<string, Buffer[]>();
|
|
306
|
+
/** append_once dedup: `"collection\0key"` -> set of eventIds already appended. */
|
|
307
|
+
private readonly eventDedup = new Map<string, Set<string>>();
|
|
308
|
+
/** Capacity family: `"collection\0key"` -> an escrow ledger (ceiling + reservations). */
|
|
309
|
+
private readonly capacity = new Map<string, CapLedger>();
|
|
310
|
+
/** `"collection\0key"` -> the schema_version the record/view/unique-owner was last
|
|
311
|
+
* written under (single-value families; the edge stores it per StoredValue). */
|
|
312
|
+
private readonly versions = new Map<string, number>();
|
|
313
|
+
/** Per-event schema_version, parallel to `events[sk]` (append order). */
|
|
314
|
+
private readonly eventVersions = new Map<string, number[]>();
|
|
315
|
+
/** Per-member schema_version: `sk` -> (memberLatin1 -> version), parallel to `members`. */
|
|
316
|
+
private readonly memberVersions = new Map<string, Map<string, number>>();
|
|
317
|
+
/** The decoded catalog from the loaded wasm, including family + current schema_version. */
|
|
318
|
+
private catalog: DbCatalogState = { kind: 'no-section' };
|
|
319
|
+
|
|
320
|
+
// ---- on-disk persistence: dev data + its versions survive restarts, so a
|
|
321
|
+
// developer can write rows, evolve a @data type, restart, and watch the @migrate
|
|
322
|
+
// run. Delete the file to reset the dev database. JSON with base64 buffers.
|
|
323
|
+
private persistPath: string | null = null;
|
|
324
|
+
|
|
325
|
+
/** (Re)load the catalog capability metadata from a server wasm. The module
|
|
326
|
+
* loader calls this on every (re)compile so writes stamp the live version. */
|
|
327
|
+
setCatalog(wasm: Buffer): void {
|
|
328
|
+
this.catalog = parseCatalog(wasm);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private currentSchemaVersion(coll: DevCollectionHandle): number {
|
|
332
|
+
if (this.catalog.kind !== 'present') return coll.schemaVersion;
|
|
333
|
+
return this.catalog.collections.get(coll.name)?.schemaVersion ?? coll.schemaVersion;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private stampVersion(coll: DevCollectionHandle, sk: string): void {
|
|
337
|
+
this.versions.set(sk, this.currentSchemaVersion(coll)); // stamp the value type's current version
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private recordIdemStart(
|
|
341
|
+
coll: DevCollectionHandle,
|
|
342
|
+
key: Buffer,
|
|
343
|
+
op: string,
|
|
344
|
+
idem: Buffer | null,
|
|
345
|
+
requestHash: string,
|
|
346
|
+
): { fresh: true } | { fresh: false; status: number; outcome?: RecordOutcome } {
|
|
347
|
+
if (idem === null) return { fresh: true };
|
|
348
|
+
const ik = idemKey(coll, key, op, idem);
|
|
349
|
+
const row = this.recordIdem.get(ik);
|
|
350
|
+
if (row === undefined) {
|
|
351
|
+
this.recordIdem.set(ik, { requestHash, outcome: null });
|
|
352
|
+
return { fresh: true };
|
|
353
|
+
}
|
|
354
|
+
if (row.requestHash !== requestHash) return { fresh: false, status: CONFLICT };
|
|
355
|
+
if (row.outcome === null) return { fresh: false, status: UNAVAILABLE };
|
|
356
|
+
return { fresh: false, status: 0, outcome: row.outcome };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private recordIdemFinish(
|
|
360
|
+
coll: DevCollectionHandle,
|
|
361
|
+
key: Buffer,
|
|
362
|
+
op: string,
|
|
363
|
+
idem: Buffer | null,
|
|
364
|
+
requestHash: string,
|
|
365
|
+
outcome: RecordOutcome,
|
|
366
|
+
): void {
|
|
367
|
+
if (idem === null) return;
|
|
368
|
+
const ik = idemKey(coll, key, op, idem);
|
|
369
|
+
const row = this.recordIdem.get(ik);
|
|
370
|
+
if (row === undefined || row.requestHash !== requestHash) return;
|
|
371
|
+
if (row.outcome === null) row.outcome = outcome;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private replayRecordOutcome(db: DbDevState, outcome: RecordOutcome): number {
|
|
375
|
+
switch (outcome.kind) {
|
|
376
|
+
case 'unit':
|
|
377
|
+
return 0;
|
|
378
|
+
case 'value':
|
|
379
|
+
db.lastResult = outcome.value;
|
|
380
|
+
db.lastResultVersion = outcome.schemaVersion;
|
|
381
|
+
return outcome.value.length;
|
|
382
|
+
case 'absent':
|
|
383
|
+
case 'not_found':
|
|
384
|
+
return ABSENT;
|
|
385
|
+
case 'already_exists':
|
|
386
|
+
return ALREADY_EXISTS;
|
|
387
|
+
case 'conflict':
|
|
388
|
+
return CONFLICT;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Point the dev DB at an on-disk file and load any existing snapshot. Call once at
|
|
393
|
+
* dev-server startup. Deleting the file (or the whole `.toil/` dir) resets dev data. */
|
|
394
|
+
configurePersistence(filePath: string): void {
|
|
395
|
+
this.persistPath = filePath;
|
|
396
|
+
this.load();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Write the current store to disk (no-op if persistence is not configured). The
|
|
400
|
+
* module loader calls this after each dispatch so a crash never loses a write. */
|
|
401
|
+
persist(): void {
|
|
402
|
+
if (this.persistPath === null) return;
|
|
403
|
+
const snap: DbSnapshot = {
|
|
404
|
+
store: {},
|
|
405
|
+
recordIdem: {},
|
|
406
|
+
uniqueIdem: {},
|
|
407
|
+
views: {},
|
|
408
|
+
members: {},
|
|
409
|
+
counters: {},
|
|
410
|
+
counterIdem: {},
|
|
411
|
+
events: {},
|
|
412
|
+
eventDedup: {},
|
|
413
|
+
capacity: {},
|
|
414
|
+
};
|
|
415
|
+
for (const [k, v] of this.store)
|
|
416
|
+
snap.store[k] = { v: v.toString('base64'), sv: this.versions.get(k) ?? 0 };
|
|
417
|
+
for (const [k, row] of this.recordIdem)
|
|
418
|
+
snap.recordIdem![k] = {
|
|
419
|
+
requestHash: row.requestHash,
|
|
420
|
+
state: row.outcome === null ? 'pending' : 'done',
|
|
421
|
+
outcome: row.outcome === null ? undefined : snapshotOutcome(row.outcome),
|
|
422
|
+
};
|
|
423
|
+
for (const [k, v] of this.uniqueIdem) snap.uniqueIdem![k] = v;
|
|
424
|
+
for (const [k, v] of this.views)
|
|
425
|
+
snap.views[k] = { v: v.toString('base64'), sv: this.versions.get(k) ?? 0 };
|
|
426
|
+
for (const [k, m] of this.members) {
|
|
427
|
+
const o: Record<string, { v: string; sv: number }> = {};
|
|
428
|
+
const mv = this.memberVersions.get(k);
|
|
429
|
+
for (const [mk, mvb] of m) o[mk] = { v: mvb.toString('base64'), sv: mv?.get(mk) ?? 0 };
|
|
430
|
+
snap.members[k] = o;
|
|
431
|
+
}
|
|
432
|
+
for (const [k, v] of this.counters) snap.counters[k] = v.toString();
|
|
433
|
+
for (const [k, v] of this.counterIdem) snap.counterIdem![k] = v.toString();
|
|
434
|
+
for (const [k, log] of this.events) {
|
|
435
|
+
const ver = this.eventVersions.get(k) ?? [];
|
|
436
|
+
snap.events[k] = log.map((b, i) => ({ v: b.toString('base64'), sv: ver[i] ?? 0 }));
|
|
437
|
+
}
|
|
438
|
+
for (const [k, s] of this.eventDedup) snap.eventDedup[k] = [...s];
|
|
439
|
+
for (const [k, l] of this.capacity)
|
|
440
|
+
snap.capacity[k] = {
|
|
441
|
+
total: l.total.toString(),
|
|
442
|
+
nextId: l.nextId.toString(),
|
|
443
|
+
reservations: [...l.reservations].map(([id, r]) => [
|
|
444
|
+
id.toString(),
|
|
445
|
+
{ amount: r.amount.toString(), expiresMs: r.expiresMs, confirmed: r.confirmed },
|
|
446
|
+
]),
|
|
447
|
+
};
|
|
448
|
+
try {
|
|
449
|
+
fs.mkdirSync(path.dirname(this.persistPath), { recursive: true });
|
|
450
|
+
// Write to a temp file then rename: atomic on POSIX, so a crash mid-write
|
|
451
|
+
// leaves the previous good snapshot intact instead of a truncated file
|
|
452
|
+
// that would fail to parse and drop the whole dev database on next load.
|
|
453
|
+
const tmp = `${this.persistPath}.${process.pid}.tmp`;
|
|
454
|
+
fs.writeFileSync(tmp, JSON.stringify(snap));
|
|
455
|
+
fs.renameSync(tmp, this.persistPath);
|
|
456
|
+
} catch {
|
|
457
|
+
// dev-only best effort; a write failure must never crash a request.
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private load(): void {
|
|
462
|
+
if (this.persistPath === null) return;
|
|
463
|
+
let snap: DbSnapshot;
|
|
464
|
+
try {
|
|
465
|
+
snap = JSON.parse(fs.readFileSync(this.persistPath, 'utf8')) as DbSnapshot;
|
|
466
|
+
} catch {
|
|
467
|
+
return; // no snapshot yet (or unreadable) - start empty
|
|
468
|
+
}
|
|
469
|
+
this.clear();
|
|
470
|
+
for (const [k, e] of Object.entries(snap.store ?? {})) {
|
|
471
|
+
this.store.set(k, Buffer.from(e.v, 'base64'));
|
|
472
|
+
this.versions.set(k, e.sv);
|
|
473
|
+
}
|
|
474
|
+
for (const [k, row] of Object.entries(snap.recordIdem ?? {})) {
|
|
475
|
+
this.recordIdem.set(k, {
|
|
476
|
+
requestHash: row.requestHash,
|
|
477
|
+
outcome:
|
|
478
|
+
row.state === 'done' && row.outcome !== undefined
|
|
479
|
+
? loadOutcome(row.outcome)
|
|
480
|
+
: null,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
for (const [k, v] of Object.entries(snap.uniqueIdem ?? {})) this.uniqueIdem.set(k, v);
|
|
484
|
+
for (const [k, e] of Object.entries(snap.views ?? {})) {
|
|
485
|
+
this.views.set(k, Buffer.from(e.v, 'base64'));
|
|
486
|
+
this.versions.set(k, e.sv);
|
|
487
|
+
}
|
|
488
|
+
for (const [k, m] of Object.entries(snap.members ?? {})) {
|
|
489
|
+
const map = new Map<string, Buffer>();
|
|
490
|
+
const ver = new Map<string, number>();
|
|
491
|
+
for (const [mk, e] of Object.entries(m)) {
|
|
492
|
+
map.set(mk, Buffer.from(e.v, 'base64'));
|
|
493
|
+
ver.set(mk, e.sv);
|
|
494
|
+
}
|
|
495
|
+
this.members.set(k, map);
|
|
496
|
+
this.memberVersions.set(k, ver);
|
|
497
|
+
}
|
|
498
|
+
for (const [k, v] of Object.entries(snap.counters ?? {})) this.counters.set(k, BigInt(v));
|
|
499
|
+
for (const [k, v] of Object.entries(snap.counterIdem ?? {}))
|
|
500
|
+
this.counterIdem.set(k, BigInt(v));
|
|
501
|
+
for (const [k, log] of Object.entries(snap.events ?? {})) {
|
|
502
|
+
this.events.set(
|
|
503
|
+
k,
|
|
504
|
+
log.map((e) => Buffer.from(e.v, 'base64')),
|
|
505
|
+
);
|
|
506
|
+
this.eventVersions.set(
|
|
507
|
+
k,
|
|
508
|
+
log.map((e) => e.sv),
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
for (const [k, ids] of Object.entries(snap.eventDedup ?? {}))
|
|
512
|
+
this.eventDedup.set(k, new Set(ids));
|
|
513
|
+
for (const [k, l] of Object.entries(snap.capacity ?? {})) {
|
|
514
|
+
const res = new Map<bigint, Reservation>();
|
|
515
|
+
for (const [id, r] of l.reservations)
|
|
516
|
+
res.set(BigInt(id), {
|
|
517
|
+
amount: BigInt(r.amount),
|
|
518
|
+
expiresMs: r.expiresMs,
|
|
519
|
+
confirmed: r.confirmed,
|
|
520
|
+
});
|
|
521
|
+
this.capacity.set(k, {
|
|
522
|
+
total: BigInt(l.total),
|
|
523
|
+
nextId: BigInt(l.nextId),
|
|
524
|
+
reservations: res,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
private clear(): void {
|
|
530
|
+
this.store.clear();
|
|
531
|
+
this.recordIdem.clear();
|
|
532
|
+
this.uniqueIdem.clear();
|
|
533
|
+
this.versions.clear();
|
|
534
|
+
this.views.clear();
|
|
535
|
+
this.members.clear();
|
|
536
|
+
this.memberVersions.clear();
|
|
537
|
+
this.counters.clear();
|
|
538
|
+
this.counterIdem.clear();
|
|
539
|
+
this.eventVersions.clear();
|
|
540
|
+
this.events.clear();
|
|
541
|
+
this.eventDedup.clear();
|
|
542
|
+
this.capacity.clear();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private capLedger(sk: string): CapLedger {
|
|
546
|
+
let l = this.capacity.get(sk);
|
|
547
|
+
if (l === undefined) {
|
|
548
|
+
l = { total: 0n, reservations: new Map(), nextId: 1n };
|
|
549
|
+
this.capacity.set(sk, l);
|
|
550
|
+
}
|
|
551
|
+
return l;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Drop UN-confirmed reservations whose TTL elapsed (a confirmed sale never expires). */
|
|
555
|
+
private capPrune(l: CapLedger, nowMs: number): void {
|
|
556
|
+
for (const [id, r] of l.reservations)
|
|
557
|
+
if (!r.confirmed && r.expiresMs <= nowMs) l.reservations.delete(id);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Units reserved against the ceiling: held (un-expired) + confirmed (call capPrune first). */
|
|
561
|
+
private capReserved(l: CapLedger): bigint {
|
|
562
|
+
let sum = 0n;
|
|
563
|
+
for (const r of l.reservations.values()) sum += r.amount;
|
|
564
|
+
return sum;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// --- record family (resolve / get / get_many / exists / create / patch /
|
|
568
|
+
// delete / get_delete) ---
|
|
569
|
+
|
|
570
|
+
resolveCollection(
|
|
571
|
+
ref: MemoryRef,
|
|
572
|
+
db: DbDevState,
|
|
573
|
+
namePtr: number,
|
|
574
|
+
nameLen: number,
|
|
575
|
+
outHandlePtr: number,
|
|
576
|
+
): number {
|
|
577
|
+
if (nameLen < 0 || nameLen > MAX_NAME) throw new Error('data: collection name too long');
|
|
578
|
+
const name = readCopy(ref, namePtr, nameLen).toString('utf8');
|
|
579
|
+
let coll: DevCollectionHandle;
|
|
580
|
+
switch (this.catalog.kind) {
|
|
581
|
+
case 'present': {
|
|
582
|
+
const found = this.catalog.collections.get(name);
|
|
583
|
+
if (found === undefined) return SCHEMA_UNAVAILABLE;
|
|
584
|
+
coll = { ...found };
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
case 'malformed':
|
|
588
|
+
return SCHEMA_UNAVAILABLE;
|
|
589
|
+
case 'no-section':
|
|
590
|
+
coll = {
|
|
591
|
+
name,
|
|
592
|
+
family: CollectionFamily.Record,
|
|
593
|
+
schemaVersion: 0,
|
|
594
|
+
replication: 0,
|
|
595
|
+
placement: 0,
|
|
596
|
+
fillMaxWaitMs: DEFAULT_FILL_WAIT_MS,
|
|
597
|
+
fillAllowStale: true,
|
|
598
|
+
};
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
const handle = db.handles.length;
|
|
602
|
+
db.handles.push(coll);
|
|
603
|
+
const m = mem(ref);
|
|
604
|
+
if (outHandlePtr < 0 || outHandlePtr + 4 > m.length)
|
|
605
|
+
throw new Error('data: resolve out-handle out of bounds');
|
|
606
|
+
m.writeUInt32LE(handle, outHandlePtr);
|
|
607
|
+
return 0;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
get(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number {
|
|
611
|
+
const coll = collForOp(db, handle, DbOp.Get, CollectionFamily.Record);
|
|
612
|
+
if (typeof coll === 'number') return coll;
|
|
613
|
+
if (keyLen > MAX_KEY) throw new Error('data: key too long');
|
|
614
|
+
const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
|
|
615
|
+
const v = this.store.get(sk);
|
|
616
|
+
if (v === undefined) return ABSENT;
|
|
617
|
+
db.lastResult = v;
|
|
618
|
+
db.lastResultVersion = this.versions.get(sk) ?? 0; // surface the row's stored version
|
|
619
|
+
return v.length;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Bounded multi-get. Keys blob: u32 count + per key (u32 len + bytes).
|
|
623
|
+
// Result (stashed): u32 count + per item u8 present (+ u32 len + bytes),
|
|
624
|
+
// in request order. Mirrors the edge `op_get_many` framing byte-for-byte.
|
|
625
|
+
getMany(
|
|
626
|
+
ref: MemoryRef,
|
|
627
|
+
db: DbDevState,
|
|
628
|
+
handle: number,
|
|
629
|
+
keysPtr: number,
|
|
630
|
+
keysLen: number,
|
|
631
|
+
): number {
|
|
632
|
+
const coll = collForOp(
|
|
633
|
+
db,
|
|
634
|
+
handle,
|
|
635
|
+
DbOp.GetMany,
|
|
636
|
+
CollectionFamily.Record,
|
|
637
|
+
CollectionFamily.View,
|
|
638
|
+
);
|
|
639
|
+
if (typeof coll === 'number') return coll;
|
|
640
|
+
if (keysLen > MAX_VALUE) throw new Error('data: keys blob too large');
|
|
641
|
+
const table = coll.family === CollectionFamily.View ? this.views : this.store;
|
|
642
|
+
// Keys blob: u32 count, then per key a u32-length-prefixed blob. The shared
|
|
643
|
+
// DataReader is bounds-safe (empty past end), so a malformed/truncated blob
|
|
644
|
+
// can't over-read; cap each key at MAX_KEY like the edge's prepare_key.
|
|
645
|
+
const r = new DataReader(readCopy(ref, keysPtr, keysLen));
|
|
646
|
+
const count = r.readU32();
|
|
647
|
+
if (count > 1024) return TOO_MANY_KEYS; // anti-OOM cap, mirrors the edge
|
|
648
|
+
// Result: u32 count, then per item present(u8) + (when present) the row's
|
|
649
|
+
// stored schema_version (u32, per-item @migrate dispatch) + value (u32 len +
|
|
650
|
+
// bytes). Byte-identical to the edge op_get_many framing.
|
|
651
|
+
const w = new DataWriter();
|
|
652
|
+
w.writeU32(count);
|
|
653
|
+
for (let i = 0; i < count; i++) {
|
|
654
|
+
const key = r.readBytes();
|
|
655
|
+
if (key.length > MAX_KEY) throw new Error('data: key too long');
|
|
656
|
+
const sk = storeKey(coll.name, Buffer.from(key));
|
|
657
|
+
const v = table.get(sk);
|
|
658
|
+
if (v === undefined) {
|
|
659
|
+
w.writeU8(0);
|
|
660
|
+
} else {
|
|
661
|
+
w.writeU8(1)
|
|
662
|
+
.writeU32(this.versions.get(sk) ?? 0)
|
|
663
|
+
.writeBytes(v);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
db.lastResult = Buffer.from(w.toBytes());
|
|
667
|
+
return db.lastResult.length;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
exists(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number {
|
|
671
|
+
const coll = collForOp(db, handle, DbOp.Exists, CollectionFamily.Record);
|
|
672
|
+
if (typeof coll === 'number') return coll;
|
|
673
|
+
return this.store.has(storeKey(coll.name, readKey(ref, keyPtr, keyLen))) ? 1 : 0;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
create(
|
|
677
|
+
ref: MemoryRef,
|
|
678
|
+
db: DbDevState,
|
|
679
|
+
handle: number,
|
|
680
|
+
keyPtr: number,
|
|
681
|
+
keyLen: number,
|
|
682
|
+
valPtr: number,
|
|
683
|
+
valLen: number,
|
|
684
|
+
idemPtr: number,
|
|
685
|
+
): number {
|
|
686
|
+
const coll = collForOp(db, handle, DbOp.Create, CollectionFamily.Record);
|
|
687
|
+
if (typeof coll === 'number') return coll;
|
|
688
|
+
if (keyLen > MAX_KEY || valLen > MAX_VALUE) throw new Error('data: key/value too large');
|
|
689
|
+
const key = readKey(ref, keyPtr, keyLen);
|
|
690
|
+
const value = readCopy(ref, valPtr, valLen);
|
|
691
|
+
const idem = readIdem(ref, idemPtr);
|
|
692
|
+
const requestHash = recordRequestHash(RECORD_OP_CREATE, key, value);
|
|
693
|
+
const start = this.recordIdemStart(coll, key, 'C', idem, requestHash);
|
|
694
|
+
if (!start.fresh)
|
|
695
|
+
return start.outcome ? this.replayRecordOutcome(db, start.outcome) : start.status;
|
|
696
|
+
const sk = storeKey(coll.name, key);
|
|
697
|
+
const outcome: RecordOutcome = this.store.has(sk)
|
|
698
|
+
? { kind: 'already_exists' }
|
|
699
|
+
: { kind: 'unit' };
|
|
700
|
+
if (outcome.kind === 'unit') {
|
|
701
|
+
this.store.set(sk, value);
|
|
702
|
+
this.stampVersion(coll, sk); // stamp the value type's current schema version
|
|
703
|
+
}
|
|
704
|
+
this.recordIdemFinish(coll, key, 'C', idem, requestHash, outcome);
|
|
705
|
+
return this.replayRecordOutcome(db, outcome);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
patch(
|
|
709
|
+
ref: MemoryRef,
|
|
710
|
+
db: DbDevState,
|
|
711
|
+
handle: number,
|
|
712
|
+
keyPtr: number,
|
|
713
|
+
keyLen: number,
|
|
714
|
+
patchPtr: number,
|
|
715
|
+
patchLen: number,
|
|
716
|
+
idemPtr: number,
|
|
717
|
+
): number {
|
|
718
|
+
const coll = collForOp(db, handle, DbOp.Patch, CollectionFamily.Record);
|
|
719
|
+
if (typeof coll === 'number') return coll;
|
|
720
|
+
if (keyLen > MAX_KEY || patchLen > MAX_VALUE) throw new Error('data: key/patch too large');
|
|
721
|
+
const key = readKey(ref, keyPtr, keyLen);
|
|
722
|
+
const v = readCopy(ref, patchPtr, patchLen);
|
|
723
|
+
const idem = readIdem(ref, idemPtr);
|
|
724
|
+
const requestHash = recordRequestHash(RECORD_OP_PATCH, key, v);
|
|
725
|
+
const start = this.recordIdemStart(coll, key, 'P', idem, requestHash);
|
|
726
|
+
if (!start.fresh)
|
|
727
|
+
return start.outcome ? this.replayRecordOutcome(db, start.outcome) : start.status;
|
|
728
|
+
const sk = storeKey(coll.name, key);
|
|
729
|
+
const outcome: RecordOutcome = this.store.has(sk)
|
|
730
|
+
? { kind: 'value', value: v, schemaVersion: -1 }
|
|
731
|
+
: { kind: 'not_found' };
|
|
732
|
+
if (outcome.kind === 'value') {
|
|
733
|
+
this.store.set(sk, v);
|
|
734
|
+
this.stampVersion(coll, sk); // a patch rewrites the row at the current version
|
|
735
|
+
}
|
|
736
|
+
this.recordIdemFinish(coll, key, 'P', idem, requestHash, outcome);
|
|
737
|
+
return this.replayRecordOutcome(db, outcome);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
delete(
|
|
741
|
+
ref: MemoryRef,
|
|
742
|
+
db: DbDevState,
|
|
743
|
+
handle: number,
|
|
744
|
+
keyPtr: number,
|
|
745
|
+
keyLen: number,
|
|
746
|
+
idemPtr: number,
|
|
747
|
+
): number {
|
|
748
|
+
const coll = collForOp(db, handle, DbOp.Delete, CollectionFamily.Record);
|
|
749
|
+
if (typeof coll === 'number') return coll;
|
|
750
|
+
const key = readKey(ref, keyPtr, keyLen);
|
|
751
|
+
const idem = readIdem(ref, idemPtr);
|
|
752
|
+
const requestHash = recordRequestHash(RECORD_OP_DELETE, key, Buffer.alloc(0));
|
|
753
|
+
const start = this.recordIdemStart(coll, key, 'D', idem, requestHash);
|
|
754
|
+
if (!start.fresh)
|
|
755
|
+
return start.outcome ? this.replayRecordOutcome(db, start.outcome) : start.status;
|
|
756
|
+
const sk = storeKey(coll.name, key);
|
|
757
|
+
this.store.delete(sk);
|
|
758
|
+
this.versions.delete(sk);
|
|
759
|
+
const outcome: RecordOutcome = { kind: 'unit' };
|
|
760
|
+
this.recordIdemFinish(coll, key, 'D', idem, requestHash, outcome);
|
|
761
|
+
return this.replayRecordOutcome(db, outcome);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Atomic fetch-and-delete (consume-once); deletes only on a real read.
|
|
765
|
+
getDelete(
|
|
766
|
+
ref: MemoryRef,
|
|
767
|
+
db: DbDevState,
|
|
768
|
+
handle: number,
|
|
769
|
+
keyPtr: number,
|
|
770
|
+
keyLen: number,
|
|
771
|
+
idemPtr: number,
|
|
772
|
+
): number {
|
|
773
|
+
const coll = collForOp(db, handle, DbOp.GetDelete, CollectionFamily.Record);
|
|
774
|
+
if (typeof coll === 'number') return coll;
|
|
775
|
+
const key = readKey(ref, keyPtr, keyLen);
|
|
776
|
+
const idem = readIdem(ref, idemPtr);
|
|
777
|
+
const requestHash = recordRequestHash(RECORD_OP_GET_DELETE, key, Buffer.alloc(0));
|
|
778
|
+
const start = this.recordIdemStart(coll, key, 'G', idem, requestHash);
|
|
779
|
+
if (!start.fresh)
|
|
780
|
+
return start.outcome ? this.replayRecordOutcome(db, start.outcome) : start.status;
|
|
781
|
+
const sk = storeKey(coll.name, key);
|
|
782
|
+
const v = this.store.get(sk);
|
|
783
|
+
const outcome: RecordOutcome =
|
|
784
|
+
v === undefined
|
|
785
|
+
? { kind: 'absent' }
|
|
786
|
+
: {
|
|
787
|
+
kind: 'value',
|
|
788
|
+
value: v,
|
|
789
|
+
schemaVersion: this.versions.get(sk) ?? 0,
|
|
790
|
+
};
|
|
791
|
+
if (outcome.kind === 'value') {
|
|
792
|
+
this.store.delete(sk);
|
|
793
|
+
this.versions.delete(sk);
|
|
794
|
+
}
|
|
795
|
+
this.recordIdemFinish(coll, key, 'G', idem, requestHash, outcome);
|
|
796
|
+
return this.replayRecordOutcome(db, outcome);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// --- unique family (lookup / claim / release) ---
|
|
800
|
+
|
|
801
|
+
uniqueLookup(
|
|
802
|
+
ref: MemoryRef,
|
|
803
|
+
db: DbDevState,
|
|
804
|
+
handle: number,
|
|
805
|
+
keyPtr: number,
|
|
806
|
+
keyLen: number,
|
|
807
|
+
): number {
|
|
808
|
+
const coll = collForOp(db, handle, DbOp.UniqueLookup, CollectionFamily.Unique);
|
|
809
|
+
if (typeof coll === 'number') return coll;
|
|
810
|
+
const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
|
|
811
|
+
const v = this.store.get(sk);
|
|
812
|
+
if (v === undefined) return ABSENT;
|
|
813
|
+
db.lastResult = v;
|
|
814
|
+
db.lastResultVersion = this.versions.get(sk) ?? 0;
|
|
815
|
+
return v.length;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Tag: 0 Claimed, 1 AlreadyClaimed (owner stashed), 2 AlreadyOwnedByCaller.
|
|
819
|
+
uniqueClaim(
|
|
820
|
+
ref: MemoryRef,
|
|
821
|
+
db: DbDevState,
|
|
822
|
+
handle: number,
|
|
823
|
+
keyPtr: number,
|
|
824
|
+
keyLen: number,
|
|
825
|
+
valPtr: number,
|
|
826
|
+
valLen: number,
|
|
827
|
+
idemPtr: number,
|
|
828
|
+
): number {
|
|
829
|
+
const coll = collForOp(db, handle, DbOp.UniqueClaim, CollectionFamily.Unique);
|
|
830
|
+
if (typeof coll === 'number') return coll;
|
|
831
|
+
if (keyLen > MAX_KEY || valLen > MAX_VALUE) throw new Error('data: key/value too large');
|
|
832
|
+
const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
|
|
833
|
+
const owner = readCopy(ref, valPtr, valLen);
|
|
834
|
+
const idem = readIdem(ref, idemPtr)?.toString('hex') ?? '';
|
|
835
|
+
const existing = this.store.get(sk);
|
|
836
|
+
if (existing === undefined) {
|
|
837
|
+
this.store.set(sk, owner);
|
|
838
|
+
this.uniqueIdem.set(sk, idem);
|
|
839
|
+
this.stampVersion(coll, sk);
|
|
840
|
+
return 0; // Claimed
|
|
841
|
+
}
|
|
842
|
+
if (existing.equals(owner)) {
|
|
843
|
+
return (this.uniqueIdem.get(sk) ?? '') === idem ? 2 : CONFLICT;
|
|
844
|
+
}
|
|
845
|
+
db.lastResult = existing;
|
|
846
|
+
return 1; // AlreadyClaimed (current owner stashed)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
uniqueRelease(
|
|
850
|
+
ref: MemoryRef,
|
|
851
|
+
db: DbDevState,
|
|
852
|
+
handle: number,
|
|
853
|
+
keyPtr: number,
|
|
854
|
+
keyLen: number,
|
|
855
|
+
valPtr: number,
|
|
856
|
+
valLen: number,
|
|
857
|
+
): number {
|
|
858
|
+
const coll = collForOp(db, handle, DbOp.UniqueRelease, CollectionFamily.Unique);
|
|
859
|
+
if (typeof coll === 'number') return coll;
|
|
860
|
+
const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
|
|
861
|
+
const existing = this.store.get(sk);
|
|
862
|
+
if (existing === undefined) return 0; // idempotent
|
|
863
|
+
if (!existing.equals(readCopy(ref, valPtr, valLen))) return CONFLICT; // not the owner
|
|
864
|
+
this.store.delete(sk);
|
|
865
|
+
this.uniqueIdem.delete(sk);
|
|
866
|
+
this.versions.delete(sk);
|
|
867
|
+
return 0;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// --- membership family (contains / add / remove / list) ---
|
|
871
|
+
|
|
872
|
+
membershipContains(
|
|
873
|
+
ref: MemoryRef,
|
|
874
|
+
db: DbDevState,
|
|
875
|
+
handle: number,
|
|
876
|
+
setPtr: number,
|
|
877
|
+
setLen: number,
|
|
878
|
+
memberPtr: number,
|
|
879
|
+
memberLen: number,
|
|
880
|
+
): number {
|
|
881
|
+
const coll = collForOp(db, handle, DbOp.MembershipContains, CollectionFamily.Membership);
|
|
882
|
+
if (typeof coll === 'number') return coll;
|
|
883
|
+
const set = this.members.get(storeKey(coll.name, readKey(ref, setPtr, setLen)));
|
|
884
|
+
if (set === undefined) return 0;
|
|
885
|
+
return set.has(readCopy(ref, memberPtr, memberLen).toString('latin1')) ? 1 : 0;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
membershipAdd(
|
|
889
|
+
ref: MemoryRef,
|
|
890
|
+
db: DbDevState,
|
|
891
|
+
handle: number,
|
|
892
|
+
setPtr: number,
|
|
893
|
+
setLen: number,
|
|
894
|
+
memberPtr: number,
|
|
895
|
+
memberLen: number,
|
|
896
|
+
): number {
|
|
897
|
+
const coll = collForOp(db, handle, DbOp.MembershipAdd, CollectionFamily.Membership);
|
|
898
|
+
if (typeof coll === 'number') return coll;
|
|
899
|
+
if (setLen > MAX_KEY || memberLen > MAX_VALUE)
|
|
900
|
+
throw new Error('data: set/member too large');
|
|
901
|
+
const sk = storeKey(coll.name, readKey(ref, setPtr, setLen));
|
|
902
|
+
const member = readCopy(ref, memberPtr, memberLen);
|
|
903
|
+
let set = this.members.get(sk);
|
|
904
|
+
if (set === undefined) {
|
|
905
|
+
set = new Map();
|
|
906
|
+
this.members.set(sk, set);
|
|
907
|
+
}
|
|
908
|
+
const ml = member.toString('latin1');
|
|
909
|
+
set.set(ml, member);
|
|
910
|
+
let mv = this.memberVersions.get(sk);
|
|
911
|
+
if (mv === undefined) {
|
|
912
|
+
mv = new Map();
|
|
913
|
+
this.memberVersions.set(sk, mv);
|
|
914
|
+
}
|
|
915
|
+
mv.set(ml, this.currentSchemaVersion(coll));
|
|
916
|
+
return 0;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
membershipRemove(
|
|
920
|
+
ref: MemoryRef,
|
|
921
|
+
db: DbDevState,
|
|
922
|
+
handle: number,
|
|
923
|
+
setPtr: number,
|
|
924
|
+
setLen: number,
|
|
925
|
+
memberPtr: number,
|
|
926
|
+
memberLen: number,
|
|
927
|
+
): number {
|
|
928
|
+
const coll = collForOp(db, handle, DbOp.MembershipRemove, CollectionFamily.Membership);
|
|
929
|
+
if (typeof coll === 'number') return coll;
|
|
930
|
+
const sk = storeKey(coll.name, readKey(ref, setPtr, setLen));
|
|
931
|
+
const ml = readCopy(ref, memberPtr, memberLen).toString('latin1');
|
|
932
|
+
this.members.get(sk)?.delete(ml);
|
|
933
|
+
this.memberVersions.get(sk)?.delete(ml);
|
|
934
|
+
return 0;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Frame the members (sorted by bytes, matching the edge BTreeMap) as
|
|
938
|
+
// `u32 count` + per member `u32 len + bytes`; stash + return the length.
|
|
939
|
+
membershipList(
|
|
940
|
+
ref: MemoryRef,
|
|
941
|
+
db: DbDevState,
|
|
942
|
+
handle: number,
|
|
943
|
+
setPtr: number,
|
|
944
|
+
setLen: number,
|
|
945
|
+
limit: number,
|
|
946
|
+
): number {
|
|
947
|
+
const coll = collForOp(db, handle, DbOp.MembershipList, CollectionFamily.Membership);
|
|
948
|
+
if (typeof coll === 'number') return coll;
|
|
949
|
+
const sk = storeKey(coll.name, readKey(ref, setPtr, setLen));
|
|
950
|
+
const set = this.members.get(sk);
|
|
951
|
+
const mv = this.memberVersions.get(sk);
|
|
952
|
+
const n = Math.max(0, Math.min(limit, 0xffff));
|
|
953
|
+
const members =
|
|
954
|
+
set === undefined ? [] : Array.from(set.values()).sort(Buffer.compare).slice(0, n);
|
|
955
|
+
// u32 count, then per member its stored schema_version (u32) + bytes (u32
|
|
956
|
+
// len + bytes). Same framing as the edge op_membership_list.
|
|
957
|
+
const w = new DataWriter();
|
|
958
|
+
w.writeU32(members.length);
|
|
959
|
+
for (const m of members) {
|
|
960
|
+
w.writeU32(mv?.get(m.toString('latin1')) ?? 0).writeBytes(m);
|
|
961
|
+
}
|
|
962
|
+
db.lastResult = Buffer.from(w.toBytes());
|
|
963
|
+
return db.lastResult.length;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// --- view family (get / publish) ---
|
|
967
|
+
|
|
968
|
+
viewGet(
|
|
969
|
+
ref: MemoryRef,
|
|
970
|
+
db: DbDevState,
|
|
971
|
+
handle: number,
|
|
972
|
+
keyPtr: number,
|
|
973
|
+
keyLen: number,
|
|
974
|
+
): number {
|
|
975
|
+
const coll = collForOp(db, handle, DbOp.ViewGet, CollectionFamily.View);
|
|
976
|
+
if (typeof coll === 'number') return coll;
|
|
977
|
+
const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
|
|
978
|
+
const v = this.views.get(sk);
|
|
979
|
+
if (v === undefined) return ABSENT;
|
|
980
|
+
db.lastResult = v;
|
|
981
|
+
db.lastResultVersion = this.versions.get(sk) ?? 0;
|
|
982
|
+
return v.length;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Publish overwrites (the host assigns the version; dev keeps the latest).
|
|
986
|
+
viewPublish(
|
|
987
|
+
ref: MemoryRef,
|
|
988
|
+
db: DbDevState,
|
|
989
|
+
handle: number,
|
|
990
|
+
keyPtr: number,
|
|
991
|
+
keyLen: number,
|
|
992
|
+
valPtr: number,
|
|
993
|
+
valLen: number,
|
|
994
|
+
): number {
|
|
995
|
+
const coll = collForOp(db, handle, DbOp.ViewPublish, CollectionFamily.View);
|
|
996
|
+
if (typeof coll === 'number') return coll;
|
|
997
|
+
if (keyLen > MAX_KEY || valLen > MAX_VALUE) throw new Error('data: key/view too large');
|
|
998
|
+
const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
|
|
999
|
+
this.views.set(sk, readCopy(ref, valPtr, valLen));
|
|
1000
|
+
this.stampVersion(coll, sk);
|
|
1001
|
+
return 0;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// --- counter family (get / add) ---
|
|
1005
|
+
|
|
1006
|
+
// Stash the i64 sum as 8 LE bytes; the guest pulls + loads it. A counter
|
|
1007
|
+
// with no deltas reads as 0 (never absent).
|
|
1008
|
+
counterGet(
|
|
1009
|
+
ref: MemoryRef,
|
|
1010
|
+
db: DbDevState,
|
|
1011
|
+
handle: number,
|
|
1012
|
+
keyPtr: number,
|
|
1013
|
+
keyLen: number,
|
|
1014
|
+
): number {
|
|
1015
|
+
const coll = collForOp(db, handle, DbOp.CounterGet, CollectionFamily.Counter);
|
|
1016
|
+
if (typeof coll === 'number') return coll;
|
|
1017
|
+
const sum = this.counters.get(storeKey(coll.name, readKey(ref, keyPtr, keyLen))) ?? 0n;
|
|
1018
|
+
const out = Buffer.alloc(8);
|
|
1019
|
+
out.writeBigInt64LE(sum);
|
|
1020
|
+
db.lastResult = out;
|
|
1021
|
+
return out.length;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// `delta` is the wasm i64 (a BigInt across the boundary); `BigInt()`
|
|
1025
|
+
// normalizes the test's plain-number form too. Saturates like the edge.
|
|
1026
|
+
counterAdd(
|
|
1027
|
+
ref: MemoryRef,
|
|
1028
|
+
db: DbDevState,
|
|
1029
|
+
handle: number,
|
|
1030
|
+
keyPtr: number,
|
|
1031
|
+
keyLen: number,
|
|
1032
|
+
delta: number | bigint,
|
|
1033
|
+
idemPtr: number,
|
|
1034
|
+
): number {
|
|
1035
|
+
const coll = collForOp(db, handle, DbOp.CounterAdd, CollectionFamily.Counter);
|
|
1036
|
+
if (typeof coll === 'number') return coll;
|
|
1037
|
+
const key = readKey(ref, keyPtr, keyLen);
|
|
1038
|
+
const idem = readIdem(ref, idemPtr);
|
|
1039
|
+
const d = BigInt(delta);
|
|
1040
|
+
if (idem !== null) {
|
|
1041
|
+
const ik = idemKey(coll, key, 'A', idem);
|
|
1042
|
+
const seen = this.counterIdem.get(ik);
|
|
1043
|
+
if (seen !== undefined) return seen === d ? 0 : CONFLICT;
|
|
1044
|
+
this.counterIdem.set(ik, d);
|
|
1045
|
+
}
|
|
1046
|
+
const sk = storeKey(coll.name, key);
|
|
1047
|
+
this.counters.set(sk, satI64((this.counters.get(sk) ?? 0n) + d));
|
|
1048
|
+
return 0;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// --- events family (append / append_once / enqueue / latest) ---
|
|
1052
|
+
|
|
1053
|
+
append(
|
|
1054
|
+
ref: MemoryRef,
|
|
1055
|
+
db: DbDevState,
|
|
1056
|
+
handle: number,
|
|
1057
|
+
keyPtr: number,
|
|
1058
|
+
keyLen: number,
|
|
1059
|
+
evPtr: number,
|
|
1060
|
+
evLen: number,
|
|
1061
|
+
idemPtr: number,
|
|
1062
|
+
): number {
|
|
1063
|
+
const coll = collForOp(db, handle, DbOp.Append, CollectionFamily.Events);
|
|
1064
|
+
if (typeof coll === 'number') return coll;
|
|
1065
|
+
if (keyLen > MAX_KEY || evLen > MAX_VALUE) throw new Error('data: key/event too large');
|
|
1066
|
+
const key = readKey(ref, keyPtr, keyLen);
|
|
1067
|
+
const sk = storeKey(coll.name, key);
|
|
1068
|
+
const idem = readIdem(ref, idemPtr);
|
|
1069
|
+
if (idem !== null) {
|
|
1070
|
+
let seen = this.eventDedup.get(sk);
|
|
1071
|
+
if (seen === undefined) {
|
|
1072
|
+
seen = new Set();
|
|
1073
|
+
this.eventDedup.set(sk, seen);
|
|
1074
|
+
}
|
|
1075
|
+
const eventId = idem.toString('latin1');
|
|
1076
|
+
if (seen.has(eventId)) return 0;
|
|
1077
|
+
seen.add(eventId);
|
|
1078
|
+
}
|
|
1079
|
+
const log = this.events.get(sk);
|
|
1080
|
+
const ev = readCopy(ref, evPtr, evLen);
|
|
1081
|
+
const sv = this.currentSchemaVersion(coll);
|
|
1082
|
+
if (log === undefined) {
|
|
1083
|
+
this.events.set(sk, [ev]);
|
|
1084
|
+
this.eventVersions.set(sk, [sv]);
|
|
1085
|
+
} else {
|
|
1086
|
+
log.push(ev);
|
|
1087
|
+
(this.eventVersions.get(sk) ?? this.eventVersions.set(sk, []).get(sk)!).push(sv);
|
|
1088
|
+
}
|
|
1089
|
+
return 0;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Idempotent append: dedup on eventId. 1 appended, 0 duplicate. Mirrors the
|
|
1093
|
+
// edge's (key, event_id) dedup marker (just an in-memory set in dev).
|
|
1094
|
+
appendOnce(
|
|
1095
|
+
ref: MemoryRef,
|
|
1096
|
+
db: DbDevState,
|
|
1097
|
+
handle: number,
|
|
1098
|
+
keyPtr: number,
|
|
1099
|
+
keyLen: number,
|
|
1100
|
+
evidPtr: number,
|
|
1101
|
+
evidLen: number,
|
|
1102
|
+
evPtr: number,
|
|
1103
|
+
evLen: number,
|
|
1104
|
+
): number {
|
|
1105
|
+
const coll = collForOp(db, handle, DbOp.AppendOnce, CollectionFamily.Events);
|
|
1106
|
+
if (typeof coll === 'number') return coll;
|
|
1107
|
+
if (keyLen > MAX_KEY || evLen > MAX_VALUE) throw new Error('data: key/event too large');
|
|
1108
|
+
const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
|
|
1109
|
+
const evid = readCopy(ref, evidPtr, evidLen).toString('latin1');
|
|
1110
|
+
let seen = this.eventDedup.get(sk);
|
|
1111
|
+
if (seen === undefined) {
|
|
1112
|
+
seen = new Set();
|
|
1113
|
+
this.eventDedup.set(sk, seen);
|
|
1114
|
+
}
|
|
1115
|
+
if (seen.has(evid)) return 0; // already appended under this id
|
|
1116
|
+
const ev = readCopy(ref, evPtr, evLen);
|
|
1117
|
+
const sv = this.currentSchemaVersion(coll);
|
|
1118
|
+
const log = this.events.get(sk);
|
|
1119
|
+
if (log === undefined) {
|
|
1120
|
+
this.events.set(sk, [ev]);
|
|
1121
|
+
this.eventVersions.set(sk, [sv]);
|
|
1122
|
+
} else {
|
|
1123
|
+
log.push(ev);
|
|
1124
|
+
(this.eventVersions.get(sk) ?? this.eventVersions.set(sk, []).get(sk)!).push(sv);
|
|
1125
|
+
}
|
|
1126
|
+
seen.add(evid);
|
|
1127
|
+
return 1;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Version-checked replace of an EXISTING record's value. Returns 0 on apply,
|
|
1131
|
+
// ABSENT (-2) if the record is absent. A single dev process has no concurrent
|
|
1132
|
+
// writer, so the optimistic-concurrency check always succeeds here.
|
|
1133
|
+
enqueue(
|
|
1134
|
+
ref: MemoryRef,
|
|
1135
|
+
db: DbDevState,
|
|
1136
|
+
handle: number,
|
|
1137
|
+
keyPtr: number,
|
|
1138
|
+
keyLen: number,
|
|
1139
|
+
valPtr: number,
|
|
1140
|
+
valLen: number,
|
|
1141
|
+
idemPtr: number,
|
|
1142
|
+
): number {
|
|
1143
|
+
const coll = collForOp(db, handle, DbOp.Enqueue, CollectionFamily.Record);
|
|
1144
|
+
if (typeof coll === 'number') return coll;
|
|
1145
|
+
if (keyLen > MAX_KEY || valLen > MAX_VALUE) throw new Error('data: key/value too large');
|
|
1146
|
+
const key = readKey(ref, keyPtr, keyLen);
|
|
1147
|
+
const value = readCopy(ref, valPtr, valLen);
|
|
1148
|
+
const idem = readIdem(ref, idemPtr);
|
|
1149
|
+
const requestHash = recordRequestHash(RECORD_OP_ENQUEUE, key, value);
|
|
1150
|
+
const start = this.recordIdemStart(coll, key, 'E', idem, requestHash);
|
|
1151
|
+
if (!start.fresh)
|
|
1152
|
+
return start.outcome ? this.replayRecordOutcome(db, start.outcome) : start.status;
|
|
1153
|
+
const sk = storeKey(coll.name, key);
|
|
1154
|
+
const outcome: RecordOutcome = this.store.has(sk)
|
|
1155
|
+
? { kind: 'unit' }
|
|
1156
|
+
: { kind: 'not_found' };
|
|
1157
|
+
if (outcome.kind === 'unit') {
|
|
1158
|
+
this.store.set(sk, value);
|
|
1159
|
+
this.stampVersion(coll, sk);
|
|
1160
|
+
}
|
|
1161
|
+
this.recordIdemFinish(coll, key, 'E', idem, requestHash, outcome);
|
|
1162
|
+
return this.replayRecordOutcome(db, outcome);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Frame the newest-`limit` events as `u32 count` then per event a
|
|
1166
|
+
// length-prefixed blob (`u32 len + bytes`), newest first; stash + return
|
|
1167
|
+
// the blob length. Matches the edge `op_latest` / `toildb::Writer` framing.
|
|
1168
|
+
latest(
|
|
1169
|
+
ref: MemoryRef,
|
|
1170
|
+
db: DbDevState,
|
|
1171
|
+
handle: number,
|
|
1172
|
+
keyPtr: number,
|
|
1173
|
+
keyLen: number,
|
|
1174
|
+
limit: number,
|
|
1175
|
+
): number {
|
|
1176
|
+
const coll = collForOp(db, handle, DbOp.Latest, CollectionFamily.Events);
|
|
1177
|
+
if (typeof coll === 'number') return coll;
|
|
1178
|
+
const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
|
|
1179
|
+
const log = this.events.get(sk) ?? [];
|
|
1180
|
+
const vers = this.eventVersions.get(sk) ?? [];
|
|
1181
|
+
const n = Math.max(0, Math.min(limit, 0xffff));
|
|
1182
|
+
const start = Math.max(0, log.length - n);
|
|
1183
|
+
const newest = log.slice(start).reverse();
|
|
1184
|
+
const newestVers = vers.slice(start).reverse();
|
|
1185
|
+
// u32 count, then per event its stored schema_version (u32) + bytes (u32 len
|
|
1186
|
+
// + bytes), newest first. Same framing as the edge op_latest.
|
|
1187
|
+
const w = new DataWriter();
|
|
1188
|
+
w.writeU32(newest.length);
|
|
1189
|
+
for (let i = 0; i < newest.length; i++) {
|
|
1190
|
+
w.writeU32(newestVers[i] ?? 0).writeBytes(newest[i]);
|
|
1191
|
+
}
|
|
1192
|
+
db.lastResult = Buffer.from(w.toBytes());
|
|
1193
|
+
return db.lastResult.length;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// --- capacity family (escrow: set_total / available / reserve / confirm / cancel) ---
|
|
1197
|
+
|
|
1198
|
+
// Set the ceiling (restock / reduce). Job/derive only (kind-gated upstream).
|
|
1199
|
+
// A ceiling is never negative.
|
|
1200
|
+
capacitySetTotal(
|
|
1201
|
+
ref: MemoryRef,
|
|
1202
|
+
db: DbDevState,
|
|
1203
|
+
handle: number,
|
|
1204
|
+
keyPtr: number,
|
|
1205
|
+
keyLen: number,
|
|
1206
|
+
total: number | bigint,
|
|
1207
|
+
): number {
|
|
1208
|
+
const coll = collForOp(db, handle, DbOp.CapacitySetTotal, CollectionFamily.Capacity);
|
|
1209
|
+
if (typeof coll === 'number') return coll;
|
|
1210
|
+
const l = this.capLedger(storeKey(coll.name, readKey(ref, keyPtr, keyLen)));
|
|
1211
|
+
const t = BigInt(total);
|
|
1212
|
+
l.total = satI64(t < 0n ? 0n : t);
|
|
1213
|
+
return 0;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Stash the i64 available (total - reserved [held + confirmed], floored at 0).
|
|
1217
|
+
capacityAvailable(
|
|
1218
|
+
ref: MemoryRef,
|
|
1219
|
+
db: DbDevState,
|
|
1220
|
+
handle: number,
|
|
1221
|
+
keyPtr: number,
|
|
1222
|
+
keyLen: number,
|
|
1223
|
+
): number {
|
|
1224
|
+
const coll = collForOp(db, handle, DbOp.CapacityAvailable, CollectionFamily.Capacity);
|
|
1225
|
+
if (typeof coll === 'number') return coll;
|
|
1226
|
+
const l = this.capLedger(storeKey(coll.name, readKey(ref, keyPtr, keyLen)));
|
|
1227
|
+
this.capPrune(l, Date.now());
|
|
1228
|
+
const avail = l.total - this.capReserved(l);
|
|
1229
|
+
const out = Buffer.alloc(8);
|
|
1230
|
+
out.writeBigInt64LE(avail < 0n ? 0n : avail);
|
|
1231
|
+
db.lastResult = out;
|
|
1232
|
+
return out.length;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Hold `amount` for `ttlMs`: stash the u64 reservation id (8 bytes) on
|
|
1236
|
+
// success. A non-positive amount is a typed error (CODEC_ERR), matching the
|
|
1237
|
+
// edge's BadAmount; insufficient available OR too many live reservations is
|
|
1238
|
+
// ABSENT (-2) (the guest maps that to reservation 0 = no oversell). The TTL
|
|
1239
|
+
// is clamped to the edge's 24h ceiling. `now` is the HOST clock.
|
|
1240
|
+
capacityReserve(
|
|
1241
|
+
ref: MemoryRef,
|
|
1242
|
+
db: DbDevState,
|
|
1243
|
+
handle: number,
|
|
1244
|
+
keyPtr: number,
|
|
1245
|
+
keyLen: number,
|
|
1246
|
+
amount: number | bigint,
|
|
1247
|
+
ttlMs: number | bigint,
|
|
1248
|
+
idemPtr: number,
|
|
1249
|
+
): number {
|
|
1250
|
+
const coll = collForOp(db, handle, DbOp.CapacityReserve, CollectionFamily.Capacity);
|
|
1251
|
+
if (typeof coll === 'number') return coll;
|
|
1252
|
+
const want = BigInt(amount);
|
|
1253
|
+
if (want <= 0n) return CODEC_ERR; // BadAmount (edge: -1006)
|
|
1254
|
+
const key = readKey(ref, keyPtr, keyLen);
|
|
1255
|
+
const idem = readIdem(ref, idemPtr);
|
|
1256
|
+
const ttl = Math.min(Math.max(0, Number(ttlMs)), MAX_RESERVATION_TTL_MS);
|
|
1257
|
+
const requestedId = idem === null ? null : reservationIdFromIdem(coll, key, idem);
|
|
1258
|
+
const l = this.capLedger(storeKey(coll.name, key));
|
|
1259
|
+
const now = Date.now();
|
|
1260
|
+
this.capPrune(l, now);
|
|
1261
|
+
if (requestedId !== null) {
|
|
1262
|
+
const existing = l.reservations.get(requestedId);
|
|
1263
|
+
if (existing !== undefined) {
|
|
1264
|
+
if (existing.amount !== want) return CONFLICT;
|
|
1265
|
+
const out = Buffer.alloc(8);
|
|
1266
|
+
out.writeBigUInt64LE(requestedId);
|
|
1267
|
+
db.lastResult = out;
|
|
1268
|
+
return out.length;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if (l.total - this.capReserved(l) < want || l.reservations.size >= MAX_RESERVATIONS)
|
|
1272
|
+
return ABSENT; // never oversell; bound the reservation count
|
|
1273
|
+
const id = requestedId ?? l.nextId++;
|
|
1274
|
+
l.reservations.set(id, { amount: want, expiresMs: now + ttl, confirmed: false });
|
|
1275
|
+
const out = Buffer.alloc(8);
|
|
1276
|
+
out.writeBigUInt64LE(id);
|
|
1277
|
+
db.lastResult = out;
|
|
1278
|
+
return out.length;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Finalize a reservation into a permanent consume. IDEMPOTENT: the
|
|
1282
|
+
// reservation is flagged confirmed (and kept), so a retry of a settled id
|
|
1283
|
+
// still returns 1; 0 only when the id is unknown / expired-and-pruned.
|
|
1284
|
+
capacityConfirm(
|
|
1285
|
+
ref: MemoryRef,
|
|
1286
|
+
db: DbDevState,
|
|
1287
|
+
handle: number,
|
|
1288
|
+
keyPtr: number,
|
|
1289
|
+
keyLen: number,
|
|
1290
|
+
reservationId: number | bigint,
|
|
1291
|
+
): number {
|
|
1292
|
+
const coll = collForOp(db, handle, DbOp.CapacityConfirm, CollectionFamily.Capacity);
|
|
1293
|
+
if (typeof coll === 'number') return coll;
|
|
1294
|
+
const l = this.capLedger(storeKey(coll.name, readKey(ref, keyPtr, keyLen)));
|
|
1295
|
+
this.capPrune(l, Date.now());
|
|
1296
|
+
const r = l.reservations.get(BigInt(reservationId));
|
|
1297
|
+
if (r === undefined) return 0;
|
|
1298
|
+
r.confirmed = true;
|
|
1299
|
+
return 1;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// Release a HELD reservation back to available. A confirmed sale cannot be
|
|
1303
|
+
// cancelled (returns 0), nor an unknown id.
|
|
1304
|
+
capacityCancel(
|
|
1305
|
+
ref: MemoryRef,
|
|
1306
|
+
db: DbDevState,
|
|
1307
|
+
handle: number,
|
|
1308
|
+
keyPtr: number,
|
|
1309
|
+
keyLen: number,
|
|
1310
|
+
reservationId: number | bigint,
|
|
1311
|
+
): number {
|
|
1312
|
+
const coll = collForOp(db, handle, DbOp.CapacityCancel, CollectionFamily.Capacity);
|
|
1313
|
+
if (typeof coll === 'number') return coll;
|
|
1314
|
+
const l = this.capLedger(storeKey(coll.name, readKey(ref, keyPtr, keyLen)));
|
|
1315
|
+
this.capPrune(l, Date.now());
|
|
1316
|
+
const id = BigInt(reservationId);
|
|
1317
|
+
const r = l.reservations.get(id);
|
|
1318
|
+
if (r === undefined || r.confirmed) return 0;
|
|
1319
|
+
l.reservations.delete(id);
|
|
1320
|
+
return 1;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// --- result pull + per-row metadata ---
|
|
1324
|
+
|
|
1325
|
+
// Drain the last stashed variable-length result into the caller buffer.
|
|
1326
|
+
takeResult(ref: MemoryRef, db: DbDevState, outPtr: number, outCap: number): number {
|
|
1327
|
+
const v = db.lastResult;
|
|
1328
|
+
if (v === null) return 0;
|
|
1329
|
+
if (v.length > outCap) return TOO_SMALL; // keep the stash for retry
|
|
1330
|
+
const m = mem(ref);
|
|
1331
|
+
if (outPtr < 0 || outPtr + v.length > m.length)
|
|
1332
|
+
throw new Error('data: take_result out of bounds');
|
|
1333
|
+
v.copy(m, outPtr);
|
|
1334
|
+
db.lastResult = null;
|
|
1335
|
+
return v.length;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// `data.result_schema_version() -> i64`: the schema_version the last
|
|
1339
|
+
// value-returning read's row was written under, so the guest's woven decoder
|
|
1340
|
+
// runs the right @migrate. With on-disk persistence + catalog version
|
|
1341
|
+
// stamping, dev DOES surface historical versions: a row written under an old
|
|
1342
|
+
// @data layout reports that old version after the type evolves, so the dev
|
|
1343
|
+
// server exercises real cross-version decode. -1 means the last op returned
|
|
1344
|
+
// no value. An i64 result returns a BigInt in Node's WASM ABI.
|
|
1345
|
+
resultSchemaVersion(db: DbDevState): bigint {
|
|
1346
|
+
return BigInt(db.lastResultVersion);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// --- test hooks ---
|
|
1350
|
+
|
|
1351
|
+
/** Test-only: clear the stores + catalog + persistence between unit tests. */
|
|
1352
|
+
resetForTests(): void {
|
|
1353
|
+
this.clear();
|
|
1354
|
+
this.catalog = { kind: 'no-section' };
|
|
1355
|
+
this.persistPath = null;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/** Test-only: seed the catalog directly. Number values default to record-family entries. */
|
|
1359
|
+
setCatalogForTests(entries: Record<string, CatalogSeedEntry>): void {
|
|
1360
|
+
const collections = new Map<string, DevCollectionHandle>();
|
|
1361
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
1362
|
+
const schemaVersion = typeof entry === 'number' ? entry : (entry.schemaVersion ?? 0);
|
|
1363
|
+
const family =
|
|
1364
|
+
typeof entry === 'number'
|
|
1365
|
+
? CollectionFamily.Record
|
|
1366
|
+
: (entry.family ?? CollectionFamily.Record);
|
|
1367
|
+
const replication = typeof entry === 'number' ? 0 : (entry.replication ?? 0);
|
|
1368
|
+
const placement = typeof entry === 'number' ? 0 : (entry.placement ?? 0);
|
|
1369
|
+
const fillMaxWaitMs =
|
|
1370
|
+
typeof entry === 'number'
|
|
1371
|
+
? DEFAULT_FILL_WAIT_MS
|
|
1372
|
+
: (entry.fillMaxWaitMs ?? DEFAULT_FILL_WAIT_MS);
|
|
1373
|
+
const fillAllowStale =
|
|
1374
|
+
typeof entry === 'number' ? true : (entry.fillAllowStale ?? true);
|
|
1375
|
+
if (!isCollectionFamily(family)) {
|
|
1376
|
+
this.catalog = { kind: 'malformed' };
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
if (fillMaxWaitMs > MAX_FILL_WAIT_MS) {
|
|
1380
|
+
this.catalog = { kind: 'malformed' };
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
collections.set(name, {
|
|
1384
|
+
name,
|
|
1385
|
+
family,
|
|
1386
|
+
schemaVersion: schemaVersion >>> 0,
|
|
1387
|
+
replication,
|
|
1388
|
+
placement,
|
|
1389
|
+
fillMaxWaitMs,
|
|
1390
|
+
fillAllowStale,
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
this.catalog = { kind: 'present', collections };
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/** The single process-lifetime dev database (one store per dev/self-host process). */
|
|
1398
|
+
export const devDb = new DevDatabase();
|
|
1399
|
+
|
|
1400
|
+
/** (Re)load the collection -> current schema_version map from a server wasm. */
|
|
1401
|
+
export function setDbCatalog(wasm: Buffer): void {
|
|
1402
|
+
devDb.setCatalog(wasm);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/** Point the dev DB at an on-disk file and load any existing snapshot. */
|
|
1406
|
+
export function configureDbPersistence(filePath: string): void {
|
|
1407
|
+
devDb.configurePersistence(filePath);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/** Write the current store to disk (no-op if persistence is not configured). */
|
|
1411
|
+
export function persistDb(): void {
|
|
1412
|
+
devDb.persist();
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* Build the `data.*` host imports for one instance, delegating to the shared
|
|
1417
|
+
* {@link devDb}. `db` is the per-request state (handles + result stash); bind a
|
|
1418
|
+
* fresh one per dispatch.
|
|
1419
|
+
*/
|
|
1420
|
+
export function buildDatabaseImports(
|
|
1421
|
+
ref: MemoryRef,
|
|
1422
|
+
db: DbDevState,
|
|
1423
|
+
): Record<string, (...args: number[]) => number | bigint> {
|
|
1424
|
+
return {
|
|
1425
|
+
'data.resolve_collection': (
|
|
1426
|
+
namePtr: number,
|
|
1427
|
+
nameLen: number,
|
|
1428
|
+
outHandlePtr: number,
|
|
1429
|
+
): number => devDb.resolveCollection(ref, db, namePtr, nameLen, outHandlePtr),
|
|
1430
|
+
|
|
1431
|
+
'data.get': (handle: number, keyPtr: number, keyLen: number): number =>
|
|
1432
|
+
devDb.get(ref, db, handle, keyPtr, keyLen),
|
|
1433
|
+
|
|
1434
|
+
'data.get_many': (handle: number, keysPtr: number, keysLen: number): number =>
|
|
1435
|
+
devDb.getMany(ref, db, handle, keysPtr, keysLen),
|
|
1436
|
+
|
|
1437
|
+
'data.exists': (handle: number, keyPtr: number, keyLen: number): number =>
|
|
1438
|
+
devDb.exists(ref, db, handle, keyPtr, keyLen),
|
|
1439
|
+
|
|
1440
|
+
'data.create': (
|
|
1441
|
+
handle: number,
|
|
1442
|
+
keyPtr: number,
|
|
1443
|
+
keyLen: number,
|
|
1444
|
+
valPtr: number,
|
|
1445
|
+
valLen: number,
|
|
1446
|
+
idemPtr: number,
|
|
1447
|
+
): number => devDb.create(ref, db, handle, keyPtr, keyLen, valPtr, valLen, idemPtr),
|
|
1448
|
+
|
|
1449
|
+
'data.patch': (
|
|
1450
|
+
handle: number,
|
|
1451
|
+
keyPtr: number,
|
|
1452
|
+
keyLen: number,
|
|
1453
|
+
patchPtr: number,
|
|
1454
|
+
patchLen: number,
|
|
1455
|
+
idemPtr: number,
|
|
1456
|
+
): number => devDb.patch(ref, db, handle, keyPtr, keyLen, patchPtr, patchLen, idemPtr),
|
|
1457
|
+
|
|
1458
|
+
'data.delete': (handle: number, keyPtr: number, keyLen: number, idemPtr: number): number =>
|
|
1459
|
+
devDb.delete(ref, db, handle, keyPtr, keyLen, idemPtr),
|
|
1460
|
+
|
|
1461
|
+
'data.get_delete': (
|
|
1462
|
+
handle: number,
|
|
1463
|
+
keyPtr: number,
|
|
1464
|
+
keyLen: number,
|
|
1465
|
+
idemPtr: number,
|
|
1466
|
+
): number => devDb.getDelete(ref, db, handle, keyPtr, keyLen, idemPtr),
|
|
1467
|
+
|
|
1468
|
+
'data.unique_lookup': (handle: number, keyPtr: number, keyLen: number): number =>
|
|
1469
|
+
devDb.uniqueLookup(ref, db, handle, keyPtr, keyLen),
|
|
1470
|
+
|
|
1471
|
+
'data.unique_claim': (
|
|
1472
|
+
handle: number,
|
|
1473
|
+
keyPtr: number,
|
|
1474
|
+
keyLen: number,
|
|
1475
|
+
valPtr: number,
|
|
1476
|
+
valLen: number,
|
|
1477
|
+
idemPtr: number,
|
|
1478
|
+
): number => devDb.uniqueClaim(ref, db, handle, keyPtr, keyLen, valPtr, valLen, idemPtr),
|
|
1479
|
+
|
|
1480
|
+
'data.unique_release': (
|
|
1481
|
+
handle: number,
|
|
1482
|
+
keyPtr: number,
|
|
1483
|
+
keyLen: number,
|
|
1484
|
+
valPtr: number,
|
|
1485
|
+
valLen: number,
|
|
1486
|
+
_idemPtr: number,
|
|
1487
|
+
): number => devDb.uniqueRelease(ref, db, handle, keyPtr, keyLen, valPtr, valLen),
|
|
1488
|
+
|
|
1489
|
+
'data.membership_contains': (
|
|
1490
|
+
handle: number,
|
|
1491
|
+
setPtr: number,
|
|
1492
|
+
setLen: number,
|
|
1493
|
+
memberPtr: number,
|
|
1494
|
+
memberLen: number,
|
|
1495
|
+
): number =>
|
|
1496
|
+
devDb.membershipContains(ref, db, handle, setPtr, setLen, memberPtr, memberLen),
|
|
1497
|
+
|
|
1498
|
+
'data.membership_add': (
|
|
1499
|
+
handle: number,
|
|
1500
|
+
setPtr: number,
|
|
1501
|
+
setLen: number,
|
|
1502
|
+
memberPtr: number,
|
|
1503
|
+
memberLen: number,
|
|
1504
|
+
_idemPtr: number,
|
|
1505
|
+
): number => devDb.membershipAdd(ref, db, handle, setPtr, setLen, memberPtr, memberLen),
|
|
1506
|
+
|
|
1507
|
+
'data.membership_remove': (
|
|
1508
|
+
handle: number,
|
|
1509
|
+
setPtr: number,
|
|
1510
|
+
setLen: number,
|
|
1511
|
+
memberPtr: number,
|
|
1512
|
+
memberLen: number,
|
|
1513
|
+
_idemPtr: number,
|
|
1514
|
+
): number => devDb.membershipRemove(ref, db, handle, setPtr, setLen, memberPtr, memberLen),
|
|
1515
|
+
|
|
1516
|
+
'data.membership_list': (
|
|
1517
|
+
handle: number,
|
|
1518
|
+
setPtr: number,
|
|
1519
|
+
setLen: number,
|
|
1520
|
+
limit: number,
|
|
1521
|
+
): number => devDb.membershipList(ref, db, handle, setPtr, setLen, limit),
|
|
1522
|
+
|
|
1523
|
+
'data.view_get': (handle: number, keyPtr: number, keyLen: number): number =>
|
|
1524
|
+
devDb.viewGet(ref, db, handle, keyPtr, keyLen),
|
|
1525
|
+
|
|
1526
|
+
'data.view_publish': (
|
|
1527
|
+
handle: number,
|
|
1528
|
+
keyPtr: number,
|
|
1529
|
+
keyLen: number,
|
|
1530
|
+
valPtr: number,
|
|
1531
|
+
valLen: number,
|
|
1532
|
+
_idemPtr: number,
|
|
1533
|
+
): number => devDb.viewPublish(ref, db, handle, keyPtr, keyLen, valPtr, valLen),
|
|
1534
|
+
|
|
1535
|
+
'data.counter_get': (handle: number, keyPtr: number, keyLen: number): number =>
|
|
1536
|
+
devDb.counterGet(ref, db, handle, keyPtr, keyLen),
|
|
1537
|
+
|
|
1538
|
+
'data.counter_add': (
|
|
1539
|
+
handle: number,
|
|
1540
|
+
keyPtr: number,
|
|
1541
|
+
keyLen: number,
|
|
1542
|
+
delta: number | bigint,
|
|
1543
|
+
idemPtr: number,
|
|
1544
|
+
): number => devDb.counterAdd(ref, db, handle, keyPtr, keyLen, delta, idemPtr),
|
|
1545
|
+
|
|
1546
|
+
'data.append': (
|
|
1547
|
+
handle: number,
|
|
1548
|
+
keyPtr: number,
|
|
1549
|
+
keyLen: number,
|
|
1550
|
+
evPtr: number,
|
|
1551
|
+
evLen: number,
|
|
1552
|
+
idemPtr: number,
|
|
1553
|
+
): number => devDb.append(ref, db, handle, keyPtr, keyLen, evPtr, evLen, idemPtr),
|
|
1554
|
+
|
|
1555
|
+
'data.append_once': (
|
|
1556
|
+
handle: number,
|
|
1557
|
+
keyPtr: number,
|
|
1558
|
+
keyLen: number,
|
|
1559
|
+
evidPtr: number,
|
|
1560
|
+
evidLen: number,
|
|
1561
|
+
evPtr: number,
|
|
1562
|
+
evLen: number,
|
|
1563
|
+
): number =>
|
|
1564
|
+
devDb.appendOnce(ref, db, handle, keyPtr, keyLen, evidPtr, evidLen, evPtr, evLen),
|
|
1565
|
+
|
|
1566
|
+
'data.enqueue': (
|
|
1567
|
+
handle: number,
|
|
1568
|
+
keyPtr: number,
|
|
1569
|
+
keyLen: number,
|
|
1570
|
+
valPtr: number,
|
|
1571
|
+
valLen: number,
|
|
1572
|
+
idemPtr: number,
|
|
1573
|
+
): number => devDb.enqueue(ref, db, handle, keyPtr, keyLen, valPtr, valLen, idemPtr),
|
|
1574
|
+
|
|
1575
|
+
'data.latest': (handle: number, keyPtr: number, keyLen: number, limit: number): number =>
|
|
1576
|
+
devDb.latest(ref, db, handle, keyPtr, keyLen, limit),
|
|
1577
|
+
|
|
1578
|
+
'data.capacity_set_total': (
|
|
1579
|
+
handle: number,
|
|
1580
|
+
keyPtr: number,
|
|
1581
|
+
keyLen: number,
|
|
1582
|
+
total: number | bigint,
|
|
1583
|
+
_idemPtr: number,
|
|
1584
|
+
): number => devDb.capacitySetTotal(ref, db, handle, keyPtr, keyLen, total),
|
|
1585
|
+
|
|
1586
|
+
'data.capacity_available': (handle: number, keyPtr: number, keyLen: number): number =>
|
|
1587
|
+
devDb.capacityAvailable(ref, db, handle, keyPtr, keyLen),
|
|
1588
|
+
|
|
1589
|
+
'data.capacity_reserve': (
|
|
1590
|
+
handle: number,
|
|
1591
|
+
keyPtr: number,
|
|
1592
|
+
keyLen: number,
|
|
1593
|
+
amount: number | bigint,
|
|
1594
|
+
ttlMs: number | bigint,
|
|
1595
|
+
idemPtr: number,
|
|
1596
|
+
): number => devDb.capacityReserve(ref, db, handle, keyPtr, keyLen, amount, ttlMs, idemPtr),
|
|
1597
|
+
|
|
1598
|
+
'data.capacity_confirm': (
|
|
1599
|
+
handle: number,
|
|
1600
|
+
keyPtr: number,
|
|
1601
|
+
keyLen: number,
|
|
1602
|
+
reservationId: number | bigint,
|
|
1603
|
+
_idemPtr: number,
|
|
1604
|
+
): number => devDb.capacityConfirm(ref, db, handle, keyPtr, keyLen, reservationId),
|
|
1605
|
+
|
|
1606
|
+
'data.capacity_cancel': (
|
|
1607
|
+
handle: number,
|
|
1608
|
+
keyPtr: number,
|
|
1609
|
+
keyLen: number,
|
|
1610
|
+
reservationId: number | bigint,
|
|
1611
|
+
_idemPtr: number,
|
|
1612
|
+
): number => devDb.capacityCancel(ref, db, handle, keyPtr, keyLen, reservationId),
|
|
1613
|
+
|
|
1614
|
+
'data.take_result': (outPtr: number, outCap: number): number =>
|
|
1615
|
+
devDb.takeResult(ref, db, outPtr, outCap),
|
|
1616
|
+
|
|
1617
|
+
'data.result_schema_version': (): bigint => devDb.resultSchemaVersion(db),
|
|
1618
|
+
|
|
1619
|
+
// `data.write_allowed() -> i32`: 1 if the current call may issue the
|
|
1620
|
+
// record patch used by rewrite-on-read migration convergence.
|
|
1621
|
+
'data.write_allowed': (): number => (kindAllows(db.functionKind, DbOp.Patch) ? 1 : 0),
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/** Test-only: clear the stores + catalog + persistence between unit tests. */
|
|
1626
|
+
export function __resetDbForTests(): void {
|
|
1627
|
+
devDb.resetForTests();
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
/** Test-only: seed the catalog directly. Number values default to record-family entries. */
|
|
1631
|
+
export function __setDbCatalogForTests(entries: Record<string, CatalogSeedEntry>): void {
|
|
1632
|
+
devDb.setCatalogForTests(entries);
|
|
1633
|
+
}
|