toiljs 0.0.58 → 0.0.60
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/CHANGELOG.md +19 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +309 -116
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/db/catalog.d.ts +1 -0
- package/build/devserver/db/catalog.js +80 -0
- package/build/devserver/db/database.d.ts +64 -0
- package/build/devserver/db/database.js +662 -0
- package/build/devserver/db/index.d.ts +3 -0
- package/build/devserver/db/index.js +3 -0
- package/build/devserver/db/types.d.ts +58 -0
- package/build/devserver/db/types.js +20 -0
- package/build/devserver/email/index.js +1 -1
- package/build/devserver/index.d.ts +9 -24
- package/build/devserver/index.js +4 -165
- package/build/devserver/{host.d.ts → runtime/host.d.ts} +1 -1
- package/build/devserver/{host.js → runtime/host.js} +6 -6
- package/build/devserver/{module.d.ts → runtime/module.d.ts} +1 -1
- package/build/devserver/{module.js → runtime/module.js} +8 -1
- package/build/devserver/server.d.ts +17 -0
- package/build/devserver/server.js +164 -0
- package/docs/time.md +2 -2
- package/examples/basic/server/migrations/GuestEntry.migration.ts +39 -0
- package/package.json +5 -2
- package/server/runtime/time.ts +3 -3
- package/src/cli/create.ts +38 -1
- 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/devserver/db/catalog.ts +100 -0
- package/src/devserver/db/database.ts +1169 -0
- package/src/devserver/db/index.ts +18 -0
- package/src/devserver/db/types.ts +76 -0
- package/src/devserver/email/index.ts +1 -1
- package/src/devserver/index.ts +19 -287
- package/src/devserver/{host.ts → runtime/host.ts} +6 -6
- package/src/devserver/{module.ts → runtime/module.ts} +13 -1
- package/src/devserver/server.ts +292 -0
- package/test/db.test.ts +0 -0
- package/test/devserver-database.test.ts +114 -9
- 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/example-guestbook.test.ts +43 -1
- package/test/pqauth-e2e.test.ts +1 -1
- 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,1169 @@
|
|
|
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 path from 'node:path';
|
|
21
|
+
|
|
22
|
+
import { DataReader, DataWriter } from 'toiljs/io';
|
|
23
|
+
import type { MemoryRef } from '../runtime/host.js';
|
|
24
|
+
import { parseCatalog } from './catalog.js';
|
|
25
|
+
import {
|
|
26
|
+
ABSENT,
|
|
27
|
+
ALREADY_EXISTS,
|
|
28
|
+
type CapLedger,
|
|
29
|
+
CODEC_ERR,
|
|
30
|
+
CONFLICT,
|
|
31
|
+
type DbDevState,
|
|
32
|
+
type DbSnapshot,
|
|
33
|
+
INVALID_HANDLE,
|
|
34
|
+
MAX_KEY,
|
|
35
|
+
MAX_NAME,
|
|
36
|
+
MAX_RESERVATION_TTL_MS,
|
|
37
|
+
MAX_RESERVATIONS,
|
|
38
|
+
MAX_VALUE,
|
|
39
|
+
type Reservation,
|
|
40
|
+
satI64,
|
|
41
|
+
TOO_MANY_KEYS,
|
|
42
|
+
TOO_SMALL,
|
|
43
|
+
} from './types.js';
|
|
44
|
+
|
|
45
|
+
// ---- schema versions: the dev equivalent of the edge binding the row's
|
|
46
|
+
// schema_version. Writes STAMP the value type's CURRENT version (from the loaded
|
|
47
|
+
// wasm's catalog); reads SURFACE the stamp. When a @data type evolves and the wasm
|
|
48
|
+
// is rebuilt, the catalog version changes but data on disk keeps its old stamp, so
|
|
49
|
+
// a read reports the old version and the guest's woven decoder runs the @migrate.
|
|
50
|
+
|
|
51
|
+
function mem(ref: MemoryRef): Buffer {
|
|
52
|
+
if (!ref.memory) throw new Error('data host import called before memory was bound');
|
|
53
|
+
return Buffer.from(ref.memory.buffer);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Bounds-checked read that COPIES out of guest memory (the buffer is reused). */
|
|
57
|
+
function readCopy(ref: MemoryRef, ptr: number, len: number): Buffer {
|
|
58
|
+
const m = mem(ref);
|
|
59
|
+
if (ptr < 0 || len < 0 || ptr + len > m.length)
|
|
60
|
+
throw new Error(`data read out of bounds: ptr=${String(ptr)} len=${String(len)}`);
|
|
61
|
+
return Buffer.from(m.subarray(ptr, ptr + len));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Read + length-cap a KEY. The edge traps a key over MAX_KEY on EVERY op (via
|
|
65
|
+
* `bound` in prepare_key); enforce it uniformly here so a dev read of an over-cap
|
|
66
|
+
* key fails the same way instead of silently succeeding. */
|
|
67
|
+
function readKey(ref: MemoryRef, ptr: number, len: number): Buffer {
|
|
68
|
+
if (len > MAX_KEY) throw new Error('data: key too long');
|
|
69
|
+
return readCopy(ref, ptr, len);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function storeKey(collection: string, key: Buffer): string {
|
|
73
|
+
return collection + '\0' + key.toString('latin1');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function collOf(db: DbDevState, handle: number): string | null {
|
|
77
|
+
return handle >= 0 && handle < db.handles.length ? db.handles[handle] : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The single-process dev data store: the seven ToilDB families, their per-row
|
|
82
|
+
* schema_versions, the loaded wasm's catalog, and optional on-disk persistence.
|
|
83
|
+
* Process-lifetime, shared across dispatches via the module singleton {@link devDb}.
|
|
84
|
+
*/
|
|
85
|
+
export class DevDatabase {
|
|
86
|
+
/** Process-lifetime store: `"collection\0keyLatin1"` -> value. Shared across dispatches. */
|
|
87
|
+
private readonly store = new Map<string, Buffer>();
|
|
88
|
+
/** View family: `"collection\0key"` -> the latest published view blob. */
|
|
89
|
+
private readonly views = new Map<string, Buffer>();
|
|
90
|
+
/** Membership family: `"collection\0setKey"` -> (memberLatin1 -> member bytes). */
|
|
91
|
+
private readonly members = new Map<string, Map<string, Buffer>>();
|
|
92
|
+
/** Counter family: `"collection\0key"` -> saturating i64 sum of deltas. */
|
|
93
|
+
private readonly counters = new Map<string, bigint>();
|
|
94
|
+
/** Events family: `"collection\0key"` -> append-ordered event blobs (oldest first). */
|
|
95
|
+
private readonly events = new Map<string, Buffer[]>();
|
|
96
|
+
/** append_once dedup: `"collection\0key"` -> set of eventIds already appended. */
|
|
97
|
+
private readonly eventDedup = new Map<string, Set<string>>();
|
|
98
|
+
/** Capacity family: `"collection\0key"` -> an escrow ledger (ceiling + reservations). */
|
|
99
|
+
private readonly capacity = new Map<string, CapLedger>();
|
|
100
|
+
|
|
101
|
+
/** `"collection\0key"` -> the schema_version the record/view/unique-owner was last
|
|
102
|
+
* written under (single-value families; the edge stores it per StoredValue). */
|
|
103
|
+
private readonly versions = new Map<string, number>();
|
|
104
|
+
/** Per-event schema_version, parallel to `events[sk]` (append order). */
|
|
105
|
+
private readonly eventVersions = new Map<string, number[]>();
|
|
106
|
+
/** Per-member schema_version: `sk` -> (memberLatin1 -> version), parallel to `members`. */
|
|
107
|
+
private readonly memberVersions = new Map<string, Map<string, number>>();
|
|
108
|
+
/** `"<Db>/<collection>"` -> the CURRENT schema_version, from the loaded wasm catalog. */
|
|
109
|
+
private catalog = new Map<string, number>();
|
|
110
|
+
|
|
111
|
+
// ---- on-disk persistence: dev data + its versions survive restarts, so a
|
|
112
|
+
// developer can write rows, evolve a @data type, restart, and watch the @migrate
|
|
113
|
+
// run. Delete the file to reset the dev database. JSON with base64 buffers.
|
|
114
|
+
private persistPath: string | null = null;
|
|
115
|
+
|
|
116
|
+
/** (Re)load the collection -> current-schema_version map from a server wasm. The
|
|
117
|
+
* module loader calls this on every (re)compile so writes stamp the live version. */
|
|
118
|
+
setCatalog(wasm: Buffer): void {
|
|
119
|
+
this.catalog = parseCatalog(wasm);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private stampVersion(coll: string, sk: string): void {
|
|
123
|
+
this.versions.set(sk, this.catalog.get(coll) ?? 0); // stamp the value type's current version
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Point the dev DB at an on-disk file and load any existing snapshot. Call once at
|
|
127
|
+
* dev-server startup. Deleting the file (or the whole `.toil/` dir) resets dev data. */
|
|
128
|
+
configurePersistence(filePath: string): void {
|
|
129
|
+
this.persistPath = filePath;
|
|
130
|
+
this.load();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Write the current store to disk (no-op if persistence is not configured). The
|
|
134
|
+
* module loader calls this after each dispatch so a crash never loses a write. */
|
|
135
|
+
persist(): void {
|
|
136
|
+
if (this.persistPath === null) return;
|
|
137
|
+
const snap: DbSnapshot = {
|
|
138
|
+
store: {},
|
|
139
|
+
views: {},
|
|
140
|
+
members: {},
|
|
141
|
+
counters: {},
|
|
142
|
+
events: {},
|
|
143
|
+
eventDedup: {},
|
|
144
|
+
capacity: {},
|
|
145
|
+
};
|
|
146
|
+
for (const [k, v] of this.store)
|
|
147
|
+
snap.store[k] = { v: v.toString('base64'), sv: this.versions.get(k) ?? 0 };
|
|
148
|
+
for (const [k, v] of this.views)
|
|
149
|
+
snap.views[k] = { v: v.toString('base64'), sv: this.versions.get(k) ?? 0 };
|
|
150
|
+
for (const [k, m] of this.members) {
|
|
151
|
+
const o: Record<string, { v: string; sv: number }> = {};
|
|
152
|
+
const mv = this.memberVersions.get(k);
|
|
153
|
+
for (const [mk, mvb] of m) o[mk] = { v: mvb.toString('base64'), sv: mv?.get(mk) ?? 0 };
|
|
154
|
+
snap.members[k] = o;
|
|
155
|
+
}
|
|
156
|
+
for (const [k, v] of this.counters) snap.counters[k] = v.toString();
|
|
157
|
+
for (const [k, log] of this.events) {
|
|
158
|
+
const ver = this.eventVersions.get(k) ?? [];
|
|
159
|
+
snap.events[k] = log.map((b, i) => ({ v: b.toString('base64'), sv: ver[i] ?? 0 }));
|
|
160
|
+
}
|
|
161
|
+
for (const [k, s] of this.eventDedup) snap.eventDedup[k] = [...s];
|
|
162
|
+
for (const [k, l] of this.capacity)
|
|
163
|
+
snap.capacity[k] = {
|
|
164
|
+
total: l.total.toString(),
|
|
165
|
+
nextId: l.nextId.toString(),
|
|
166
|
+
reservations: [...l.reservations].map(([id, r]) => [
|
|
167
|
+
id.toString(),
|
|
168
|
+
{ amount: r.amount.toString(), expiresMs: r.expiresMs, confirmed: r.confirmed },
|
|
169
|
+
]),
|
|
170
|
+
};
|
|
171
|
+
try {
|
|
172
|
+
fs.mkdirSync(path.dirname(this.persistPath), { recursive: true });
|
|
173
|
+
// Write to a temp file then rename: atomic on POSIX, so a crash mid-write
|
|
174
|
+
// leaves the previous good snapshot intact instead of a truncated file
|
|
175
|
+
// that would fail to parse and drop the whole dev database on next load.
|
|
176
|
+
const tmp = `${this.persistPath}.${process.pid}.tmp`;
|
|
177
|
+
fs.writeFileSync(tmp, JSON.stringify(snap));
|
|
178
|
+
fs.renameSync(tmp, this.persistPath);
|
|
179
|
+
} catch {
|
|
180
|
+
// dev-only best effort; a write failure must never crash a request.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private load(): void {
|
|
185
|
+
if (this.persistPath === null) return;
|
|
186
|
+
let snap: DbSnapshot;
|
|
187
|
+
try {
|
|
188
|
+
snap = JSON.parse(fs.readFileSync(this.persistPath, 'utf8')) as DbSnapshot;
|
|
189
|
+
} catch {
|
|
190
|
+
return; // no snapshot yet (or unreadable) - start empty
|
|
191
|
+
}
|
|
192
|
+
this.clear();
|
|
193
|
+
for (const [k, e] of Object.entries(snap.store ?? {})) {
|
|
194
|
+
this.store.set(k, Buffer.from(e.v, 'base64'));
|
|
195
|
+
this.versions.set(k, e.sv);
|
|
196
|
+
}
|
|
197
|
+
for (const [k, e] of Object.entries(snap.views ?? {})) {
|
|
198
|
+
this.views.set(k, Buffer.from(e.v, 'base64'));
|
|
199
|
+
this.versions.set(k, e.sv);
|
|
200
|
+
}
|
|
201
|
+
for (const [k, m] of Object.entries(snap.members ?? {})) {
|
|
202
|
+
const map = new Map<string, Buffer>();
|
|
203
|
+
const ver = new Map<string, number>();
|
|
204
|
+
for (const [mk, e] of Object.entries(m)) {
|
|
205
|
+
map.set(mk, Buffer.from(e.v, 'base64'));
|
|
206
|
+
ver.set(mk, e.sv);
|
|
207
|
+
}
|
|
208
|
+
this.members.set(k, map);
|
|
209
|
+
this.memberVersions.set(k, ver);
|
|
210
|
+
}
|
|
211
|
+
for (const [k, v] of Object.entries(snap.counters ?? {})) this.counters.set(k, BigInt(v));
|
|
212
|
+
for (const [k, log] of Object.entries(snap.events ?? {})) {
|
|
213
|
+
this.events.set(
|
|
214
|
+
k,
|
|
215
|
+
log.map((e) => Buffer.from(e.v, 'base64')),
|
|
216
|
+
);
|
|
217
|
+
this.eventVersions.set(
|
|
218
|
+
k,
|
|
219
|
+
log.map((e) => e.sv),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
for (const [k, ids] of Object.entries(snap.eventDedup ?? {}))
|
|
223
|
+
this.eventDedup.set(k, new Set(ids));
|
|
224
|
+
for (const [k, l] of Object.entries(snap.capacity ?? {})) {
|
|
225
|
+
const res = new Map<bigint, Reservation>();
|
|
226
|
+
for (const [id, r] of l.reservations)
|
|
227
|
+
res.set(BigInt(id), {
|
|
228
|
+
amount: BigInt(r.amount),
|
|
229
|
+
expiresMs: r.expiresMs,
|
|
230
|
+
confirmed: r.confirmed,
|
|
231
|
+
});
|
|
232
|
+
this.capacity.set(k, {
|
|
233
|
+
total: BigInt(l.total),
|
|
234
|
+
nextId: BigInt(l.nextId),
|
|
235
|
+
reservations: res,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private clear(): void {
|
|
241
|
+
this.store.clear();
|
|
242
|
+
this.versions.clear();
|
|
243
|
+
this.views.clear();
|
|
244
|
+
this.members.clear();
|
|
245
|
+
this.memberVersions.clear();
|
|
246
|
+
this.counters.clear();
|
|
247
|
+
this.eventVersions.clear();
|
|
248
|
+
this.events.clear();
|
|
249
|
+
this.eventDedup.clear();
|
|
250
|
+
this.capacity.clear();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private capLedger(sk: string): CapLedger {
|
|
254
|
+
let l = this.capacity.get(sk);
|
|
255
|
+
if (l === undefined) {
|
|
256
|
+
l = { total: 0n, reservations: new Map(), nextId: 1n };
|
|
257
|
+
this.capacity.set(sk, l);
|
|
258
|
+
}
|
|
259
|
+
return l;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Drop UN-confirmed reservations whose TTL elapsed (a confirmed sale never expires). */
|
|
263
|
+
private capPrune(l: CapLedger, nowMs: number): void {
|
|
264
|
+
for (const [id, r] of l.reservations)
|
|
265
|
+
if (!r.confirmed && r.expiresMs <= nowMs) l.reservations.delete(id);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Units reserved against the ceiling: held (un-expired) + confirmed (call capPrune first). */
|
|
269
|
+
private capReserved(l: CapLedger): bigint {
|
|
270
|
+
let sum = 0n;
|
|
271
|
+
for (const r of l.reservations.values()) sum += r.amount;
|
|
272
|
+
return sum;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// --- record family (resolve / get / get_many / exists / create / patch /
|
|
276
|
+
// delete / get_delete) ---
|
|
277
|
+
|
|
278
|
+
resolveCollection(
|
|
279
|
+
ref: MemoryRef,
|
|
280
|
+
db: DbDevState,
|
|
281
|
+
namePtr: number,
|
|
282
|
+
nameLen: number,
|
|
283
|
+
outHandlePtr: number,
|
|
284
|
+
): number {
|
|
285
|
+
if (nameLen < 0 || nameLen > MAX_NAME) throw new Error('data: collection name too long');
|
|
286
|
+
const name = readCopy(ref, namePtr, nameLen).toString('utf8');
|
|
287
|
+
const handle = db.handles.length;
|
|
288
|
+
db.handles.push(name);
|
|
289
|
+
const m = mem(ref);
|
|
290
|
+
if (outHandlePtr < 0 || outHandlePtr + 4 > m.length)
|
|
291
|
+
throw new Error('data: resolve out-handle out of bounds');
|
|
292
|
+
m.writeUInt32LE(handle, outHandlePtr);
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
get(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number {
|
|
297
|
+
const coll = collOf(db, handle);
|
|
298
|
+
if (coll === null) return INVALID_HANDLE;
|
|
299
|
+
if (keyLen > MAX_KEY) throw new Error('data: key too long');
|
|
300
|
+
const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
|
|
301
|
+
const v = this.store.get(sk);
|
|
302
|
+
if (v === undefined) return ABSENT;
|
|
303
|
+
db.lastResult = v;
|
|
304
|
+
db.lastResultVersion = this.versions.get(sk) ?? 0; // surface the row's stored version
|
|
305
|
+
return v.length;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Bounded multi-get. Keys blob: u32 count + per key (u32 len + bytes).
|
|
309
|
+
// Result (stashed): u32 count + per item u8 present (+ u32 len + bytes),
|
|
310
|
+
// in request order. Mirrors the edge `op_get_many` framing byte-for-byte.
|
|
311
|
+
getMany(
|
|
312
|
+
ref: MemoryRef,
|
|
313
|
+
db: DbDevState,
|
|
314
|
+
handle: number,
|
|
315
|
+
keysPtr: number,
|
|
316
|
+
keysLen: number,
|
|
317
|
+
): number {
|
|
318
|
+
const coll = collOf(db, handle);
|
|
319
|
+
if (coll === null) return INVALID_HANDLE;
|
|
320
|
+
if (keysLen > MAX_VALUE) throw new Error('data: keys blob too large');
|
|
321
|
+
// Keys blob: u32 count, then per key a u32-length-prefixed blob. The shared
|
|
322
|
+
// DataReader is bounds-safe (empty past end), so a malformed/truncated blob
|
|
323
|
+
// can't over-read; cap each key at MAX_KEY like the edge's prepare_key.
|
|
324
|
+
const r = new DataReader(readCopy(ref, keysPtr, keysLen));
|
|
325
|
+
const count = r.readU32();
|
|
326
|
+
if (count > 1024) return TOO_MANY_KEYS; // anti-OOM cap, mirrors the edge
|
|
327
|
+
// Result: u32 count, then per item present(u8) + (when present) the row's
|
|
328
|
+
// stored schema_version (u32, per-item @migrate dispatch) + value (u32 len +
|
|
329
|
+
// bytes). Byte-identical to the edge op_get_many framing.
|
|
330
|
+
const w = new DataWriter();
|
|
331
|
+
w.writeU32(count);
|
|
332
|
+
for (let i = 0; i < count; i++) {
|
|
333
|
+
const key = r.readBytes();
|
|
334
|
+
if (key.length > MAX_KEY) throw new Error('data: key too long');
|
|
335
|
+
const sk = storeKey(coll, Buffer.from(key));
|
|
336
|
+
const v = this.store.get(sk);
|
|
337
|
+
if (v === undefined) {
|
|
338
|
+
w.writeU8(0);
|
|
339
|
+
} else {
|
|
340
|
+
w.writeU8(1).writeU32(this.versions.get(sk) ?? 0).writeBytes(v);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
db.lastResult = Buffer.from(w.toBytes());
|
|
344
|
+
return db.lastResult.length;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
exists(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number {
|
|
348
|
+
const coll = collOf(db, handle);
|
|
349
|
+
if (coll === null) return INVALID_HANDLE;
|
|
350
|
+
return this.store.has(storeKey(coll, readKey(ref, keyPtr, keyLen))) ? 1 : 0;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
create(
|
|
354
|
+
ref: MemoryRef,
|
|
355
|
+
db: DbDevState,
|
|
356
|
+
handle: number,
|
|
357
|
+
keyPtr: number,
|
|
358
|
+
keyLen: number,
|
|
359
|
+
valPtr: number,
|
|
360
|
+
valLen: number,
|
|
361
|
+
): number {
|
|
362
|
+
const coll = collOf(db, handle);
|
|
363
|
+
if (coll === null) return INVALID_HANDLE;
|
|
364
|
+
if (keyLen > MAX_KEY || valLen > MAX_VALUE) throw new Error('data: key/value too large');
|
|
365
|
+
const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
|
|
366
|
+
if (this.store.has(sk)) return ALREADY_EXISTS;
|
|
367
|
+
this.store.set(sk, readCopy(ref, valPtr, valLen));
|
|
368
|
+
this.stampVersion(coll, sk); // stamp the value type's current schema version
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
patch(
|
|
373
|
+
ref: MemoryRef,
|
|
374
|
+
db: DbDevState,
|
|
375
|
+
handle: number,
|
|
376
|
+
keyPtr: number,
|
|
377
|
+
keyLen: number,
|
|
378
|
+
patchPtr: number,
|
|
379
|
+
patchLen: number,
|
|
380
|
+
): number {
|
|
381
|
+
const coll = collOf(db, handle);
|
|
382
|
+
if (coll === null) return INVALID_HANDLE;
|
|
383
|
+
if (keyLen > MAX_KEY || patchLen > MAX_VALUE) throw new Error('data: key/patch too large');
|
|
384
|
+
const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
|
|
385
|
+
if (!this.store.has(sk)) return ABSENT; // NotFound -> ABSENT on the edge
|
|
386
|
+
const v = readCopy(ref, patchPtr, patchLen);
|
|
387
|
+
this.store.set(sk, v);
|
|
388
|
+
this.stampVersion(coll, sk); // a patch rewrites the row at the current version
|
|
389
|
+
db.lastResult = v; // patch returns the stored record
|
|
390
|
+
db.lastResultVersion = -1; // the just-written value is current; never migrate it (matches the edge's LenStash None)
|
|
391
|
+
return v.length;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
delete(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number {
|
|
395
|
+
const coll = collOf(db, handle);
|
|
396
|
+
if (coll === null) return INVALID_HANDLE;
|
|
397
|
+
const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
|
|
398
|
+
this.store.delete(sk);
|
|
399
|
+
this.versions.delete(sk);
|
|
400
|
+
return 0;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Atomic fetch-and-delete (consume-once); deletes only on a real read.
|
|
404
|
+
getDelete(
|
|
405
|
+
ref: MemoryRef,
|
|
406
|
+
db: DbDevState,
|
|
407
|
+
handle: number,
|
|
408
|
+
keyPtr: number,
|
|
409
|
+
keyLen: number,
|
|
410
|
+
): number {
|
|
411
|
+
const coll = collOf(db, handle);
|
|
412
|
+
if (coll === null) return INVALID_HANDLE;
|
|
413
|
+
const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
|
|
414
|
+
const v = this.store.get(sk);
|
|
415
|
+
if (v === undefined) return ABSENT;
|
|
416
|
+
db.lastResultVersion = this.versions.get(sk) ?? 0; // surface before consuming
|
|
417
|
+
this.store.delete(sk);
|
|
418
|
+
this.versions.delete(sk);
|
|
419
|
+
db.lastResult = v;
|
|
420
|
+
return v.length;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// --- unique family (lookup / claim / release) ---
|
|
424
|
+
|
|
425
|
+
uniqueLookup(
|
|
426
|
+
ref: MemoryRef,
|
|
427
|
+
db: DbDevState,
|
|
428
|
+
handle: number,
|
|
429
|
+
keyPtr: number,
|
|
430
|
+
keyLen: number,
|
|
431
|
+
): number {
|
|
432
|
+
const coll = collOf(db, handle);
|
|
433
|
+
if (coll === null) return INVALID_HANDLE;
|
|
434
|
+
const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
|
|
435
|
+
const v = this.store.get(sk);
|
|
436
|
+
if (v === undefined) return ABSENT;
|
|
437
|
+
db.lastResult = v;
|
|
438
|
+
db.lastResultVersion = this.versions.get(sk) ?? 0;
|
|
439
|
+
return v.length;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Tag: 0 Claimed, 1 AlreadyClaimed (owner stashed), 2 AlreadyOwnedByCaller.
|
|
443
|
+
uniqueClaim(
|
|
444
|
+
ref: MemoryRef,
|
|
445
|
+
db: DbDevState,
|
|
446
|
+
handle: number,
|
|
447
|
+
keyPtr: number,
|
|
448
|
+
keyLen: number,
|
|
449
|
+
valPtr: number,
|
|
450
|
+
valLen: number,
|
|
451
|
+
): number {
|
|
452
|
+
const coll = collOf(db, handle);
|
|
453
|
+
if (coll === null) return INVALID_HANDLE;
|
|
454
|
+
if (keyLen > MAX_KEY || valLen > MAX_VALUE) throw new Error('data: key/value too large');
|
|
455
|
+
const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
|
|
456
|
+
const owner = readCopy(ref, valPtr, valLen);
|
|
457
|
+
const existing = this.store.get(sk);
|
|
458
|
+
if (existing === undefined) {
|
|
459
|
+
this.store.set(sk, owner);
|
|
460
|
+
this.stampVersion(coll, sk);
|
|
461
|
+
return 0; // Claimed
|
|
462
|
+
}
|
|
463
|
+
if (existing.equals(owner)) return 2; // AlreadyOwnedByCaller
|
|
464
|
+
db.lastResult = existing;
|
|
465
|
+
return 1; // AlreadyClaimed (current owner stashed)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
uniqueRelease(
|
|
469
|
+
ref: MemoryRef,
|
|
470
|
+
db: DbDevState,
|
|
471
|
+
handle: number,
|
|
472
|
+
keyPtr: number,
|
|
473
|
+
keyLen: number,
|
|
474
|
+
valPtr: number,
|
|
475
|
+
valLen: number,
|
|
476
|
+
): number {
|
|
477
|
+
const coll = collOf(db, handle);
|
|
478
|
+
if (coll === null) return INVALID_HANDLE;
|
|
479
|
+
const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
|
|
480
|
+
const existing = this.store.get(sk);
|
|
481
|
+
if (existing === undefined) return 0; // idempotent
|
|
482
|
+
if (!existing.equals(readCopy(ref, valPtr, valLen))) return CONFLICT; // not the owner
|
|
483
|
+
this.store.delete(sk);
|
|
484
|
+
this.versions.delete(sk);
|
|
485
|
+
return 0;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// --- membership family (contains / add / remove / list) ---
|
|
489
|
+
|
|
490
|
+
membershipContains(
|
|
491
|
+
ref: MemoryRef,
|
|
492
|
+
db: DbDevState,
|
|
493
|
+
handle: number,
|
|
494
|
+
setPtr: number,
|
|
495
|
+
setLen: number,
|
|
496
|
+
memberPtr: number,
|
|
497
|
+
memberLen: number,
|
|
498
|
+
): number {
|
|
499
|
+
const coll = collOf(db, handle);
|
|
500
|
+
if (coll === null) return INVALID_HANDLE;
|
|
501
|
+
const set = this.members.get(storeKey(coll, readKey(ref, setPtr, setLen)));
|
|
502
|
+
if (set === undefined) return 0;
|
|
503
|
+
return set.has(readCopy(ref, memberPtr, memberLen).toString('latin1')) ? 1 : 0;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
membershipAdd(
|
|
507
|
+
ref: MemoryRef,
|
|
508
|
+
db: DbDevState,
|
|
509
|
+
handle: number,
|
|
510
|
+
setPtr: number,
|
|
511
|
+
setLen: number,
|
|
512
|
+
memberPtr: number,
|
|
513
|
+
memberLen: number,
|
|
514
|
+
): number {
|
|
515
|
+
const coll = collOf(db, handle);
|
|
516
|
+
if (coll === null) return INVALID_HANDLE;
|
|
517
|
+
if (setLen > MAX_KEY || memberLen > MAX_VALUE)
|
|
518
|
+
throw new Error('data: set/member too large');
|
|
519
|
+
const sk = storeKey(coll, readKey(ref, setPtr, setLen));
|
|
520
|
+
const member = readCopy(ref, memberPtr, memberLen);
|
|
521
|
+
let set = this.members.get(sk);
|
|
522
|
+
if (set === undefined) {
|
|
523
|
+
set = new Map();
|
|
524
|
+
this.members.set(sk, set);
|
|
525
|
+
}
|
|
526
|
+
const ml = member.toString('latin1');
|
|
527
|
+
set.set(ml, member);
|
|
528
|
+
let mv = this.memberVersions.get(sk);
|
|
529
|
+
if (mv === undefined) {
|
|
530
|
+
mv = new Map();
|
|
531
|
+
this.memberVersions.set(sk, mv);
|
|
532
|
+
}
|
|
533
|
+
mv.set(ml, this.catalog.get(coll) ?? 0);
|
|
534
|
+
return 0;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
membershipRemove(
|
|
538
|
+
ref: MemoryRef,
|
|
539
|
+
db: DbDevState,
|
|
540
|
+
handle: number,
|
|
541
|
+
setPtr: number,
|
|
542
|
+
setLen: number,
|
|
543
|
+
memberPtr: number,
|
|
544
|
+
memberLen: number,
|
|
545
|
+
): number {
|
|
546
|
+
const coll = collOf(db, handle);
|
|
547
|
+
if (coll === null) return INVALID_HANDLE;
|
|
548
|
+
const sk = storeKey(coll, readKey(ref, setPtr, setLen));
|
|
549
|
+
const ml = readCopy(ref, memberPtr, memberLen).toString('latin1');
|
|
550
|
+
this.members.get(sk)?.delete(ml);
|
|
551
|
+
this.memberVersions.get(sk)?.delete(ml);
|
|
552
|
+
return 0;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Frame the members (sorted by bytes, matching the edge BTreeMap) as
|
|
556
|
+
// `u32 count` + per member `u32 len + bytes`; stash + return the length.
|
|
557
|
+
membershipList(
|
|
558
|
+
ref: MemoryRef,
|
|
559
|
+
db: DbDevState,
|
|
560
|
+
handle: number,
|
|
561
|
+
setPtr: number,
|
|
562
|
+
setLen: number,
|
|
563
|
+
limit: number,
|
|
564
|
+
): number {
|
|
565
|
+
const coll = collOf(db, handle);
|
|
566
|
+
if (coll === null) return INVALID_HANDLE;
|
|
567
|
+
const sk = storeKey(coll, readKey(ref, setPtr, setLen));
|
|
568
|
+
const set = this.members.get(sk);
|
|
569
|
+
const mv = this.memberVersions.get(sk);
|
|
570
|
+
const n = Math.max(0, Math.min(limit, 0xffff));
|
|
571
|
+
const members =
|
|
572
|
+
set === undefined ? [] : Array.from(set.values()).sort(Buffer.compare).slice(0, n);
|
|
573
|
+
// u32 count, then per member its stored schema_version (u32) + bytes (u32
|
|
574
|
+
// len + bytes). Same framing as the edge op_membership_list.
|
|
575
|
+
const w = new DataWriter();
|
|
576
|
+
w.writeU32(members.length);
|
|
577
|
+
for (const m of members) {
|
|
578
|
+
w.writeU32(mv?.get(m.toString('latin1')) ?? 0).writeBytes(m);
|
|
579
|
+
}
|
|
580
|
+
db.lastResult = Buffer.from(w.toBytes());
|
|
581
|
+
return db.lastResult.length;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// --- view family (get / publish) ---
|
|
585
|
+
|
|
586
|
+
viewGet(
|
|
587
|
+
ref: MemoryRef,
|
|
588
|
+
db: DbDevState,
|
|
589
|
+
handle: number,
|
|
590
|
+
keyPtr: number,
|
|
591
|
+
keyLen: number,
|
|
592
|
+
): number {
|
|
593
|
+
const coll = collOf(db, handle);
|
|
594
|
+
if (coll === null) return INVALID_HANDLE;
|
|
595
|
+
const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
|
|
596
|
+
const v = this.views.get(sk);
|
|
597
|
+
if (v === undefined) return ABSENT;
|
|
598
|
+
db.lastResult = v;
|
|
599
|
+
db.lastResultVersion = this.versions.get(sk) ?? 0;
|
|
600
|
+
return v.length;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Publish overwrites (the host assigns the version; dev keeps the latest).
|
|
604
|
+
viewPublish(
|
|
605
|
+
ref: MemoryRef,
|
|
606
|
+
db: DbDevState,
|
|
607
|
+
handle: number,
|
|
608
|
+
keyPtr: number,
|
|
609
|
+
keyLen: number,
|
|
610
|
+
valPtr: number,
|
|
611
|
+
valLen: number,
|
|
612
|
+
): number {
|
|
613
|
+
const coll = collOf(db, handle);
|
|
614
|
+
if (coll === null) return INVALID_HANDLE;
|
|
615
|
+
if (keyLen > MAX_KEY || valLen > MAX_VALUE) throw new Error('data: key/view too large');
|
|
616
|
+
const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
|
|
617
|
+
this.views.set(sk, readCopy(ref, valPtr, valLen));
|
|
618
|
+
this.stampVersion(coll, sk);
|
|
619
|
+
return 0;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// --- counter family (get / add) ---
|
|
623
|
+
|
|
624
|
+
// Stash the i64 sum as 8 LE bytes; the guest pulls + loads it. A counter
|
|
625
|
+
// with no deltas reads as 0 (never absent).
|
|
626
|
+
counterGet(
|
|
627
|
+
ref: MemoryRef,
|
|
628
|
+
db: DbDevState,
|
|
629
|
+
handle: number,
|
|
630
|
+
keyPtr: number,
|
|
631
|
+
keyLen: number,
|
|
632
|
+
): number {
|
|
633
|
+
const coll = collOf(db, handle);
|
|
634
|
+
if (coll === null) return INVALID_HANDLE;
|
|
635
|
+
const sum = this.counters.get(storeKey(coll, readKey(ref, keyPtr, keyLen))) ?? 0n;
|
|
636
|
+
const out = Buffer.alloc(8);
|
|
637
|
+
out.writeBigInt64LE(sum);
|
|
638
|
+
db.lastResult = out;
|
|
639
|
+
return out.length;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// `delta` is the wasm i64 (a BigInt across the boundary); `BigInt()`
|
|
643
|
+
// normalizes the test's plain-number form too. Saturates like the edge.
|
|
644
|
+
counterAdd(
|
|
645
|
+
ref: MemoryRef,
|
|
646
|
+
db: DbDevState,
|
|
647
|
+
handle: number,
|
|
648
|
+
keyPtr: number,
|
|
649
|
+
keyLen: number,
|
|
650
|
+
delta: number | bigint,
|
|
651
|
+
): number {
|
|
652
|
+
const coll = collOf(db, handle);
|
|
653
|
+
if (coll === null) return INVALID_HANDLE;
|
|
654
|
+
const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
|
|
655
|
+
this.counters.set(sk, satI64((this.counters.get(sk) ?? 0n) + BigInt(delta)));
|
|
656
|
+
return 0;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// --- events family (append / append_once / enqueue / latest) ---
|
|
660
|
+
|
|
661
|
+
append(
|
|
662
|
+
ref: MemoryRef,
|
|
663
|
+
db: DbDevState,
|
|
664
|
+
handle: number,
|
|
665
|
+
keyPtr: number,
|
|
666
|
+
keyLen: number,
|
|
667
|
+
evPtr: number,
|
|
668
|
+
evLen: number,
|
|
669
|
+
): number {
|
|
670
|
+
const coll = collOf(db, handle);
|
|
671
|
+
if (coll === null) return INVALID_HANDLE;
|
|
672
|
+
if (keyLen > MAX_KEY || evLen > MAX_VALUE) throw new Error('data: key/event too large');
|
|
673
|
+
const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
|
|
674
|
+
const log = this.events.get(sk);
|
|
675
|
+
const ev = readCopy(ref, evPtr, evLen);
|
|
676
|
+
const sv = this.catalog.get(coll) ?? 0;
|
|
677
|
+
if (log === undefined) {
|
|
678
|
+
this.events.set(sk, [ev]);
|
|
679
|
+
this.eventVersions.set(sk, [sv]);
|
|
680
|
+
} else {
|
|
681
|
+
log.push(ev);
|
|
682
|
+
(this.eventVersions.get(sk) ?? this.eventVersions.set(sk, []).get(sk)!).push(sv);
|
|
683
|
+
}
|
|
684
|
+
return 0;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Idempotent append: dedup on eventId. 1 appended, 0 duplicate. Mirrors the
|
|
688
|
+
// edge's (key, event_id) dedup marker (just an in-memory set in dev).
|
|
689
|
+
appendOnce(
|
|
690
|
+
ref: MemoryRef,
|
|
691
|
+
db: DbDevState,
|
|
692
|
+
handle: number,
|
|
693
|
+
keyPtr: number,
|
|
694
|
+
keyLen: number,
|
|
695
|
+
evidPtr: number,
|
|
696
|
+
evidLen: number,
|
|
697
|
+
evPtr: number,
|
|
698
|
+
evLen: number,
|
|
699
|
+
): number {
|
|
700
|
+
const coll = collOf(db, handle);
|
|
701
|
+
if (coll === null) return INVALID_HANDLE;
|
|
702
|
+
if (keyLen > MAX_KEY || evLen > MAX_VALUE) throw new Error('data: key/event too large');
|
|
703
|
+
const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
|
|
704
|
+
const evid = readCopy(ref, evidPtr, evidLen).toString('latin1');
|
|
705
|
+
let seen = this.eventDedup.get(sk);
|
|
706
|
+
if (seen === undefined) {
|
|
707
|
+
seen = new Set();
|
|
708
|
+
this.eventDedup.set(sk, seen);
|
|
709
|
+
}
|
|
710
|
+
if (seen.has(evid)) return 0; // already appended under this id
|
|
711
|
+
const ev = readCopy(ref, evPtr, evLen);
|
|
712
|
+
const sv = this.catalog.get(coll) ?? 0;
|
|
713
|
+
const log = this.events.get(sk);
|
|
714
|
+
if (log === undefined) {
|
|
715
|
+
this.events.set(sk, [ev]);
|
|
716
|
+
this.eventVersions.set(sk, [sv]);
|
|
717
|
+
} else {
|
|
718
|
+
log.push(ev);
|
|
719
|
+
(this.eventVersions.get(sk) ?? this.eventVersions.set(sk, []).get(sk)!).push(sv);
|
|
720
|
+
}
|
|
721
|
+
seen.add(evid);
|
|
722
|
+
return 1;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Version-checked replace of an EXISTING record's value. Returns 0 on apply,
|
|
726
|
+
// ABSENT (-2) if the record is absent. A single dev process has no concurrent
|
|
727
|
+
// writer, so the optimistic-concurrency check always succeeds here.
|
|
728
|
+
enqueue(
|
|
729
|
+
ref: MemoryRef,
|
|
730
|
+
db: DbDevState,
|
|
731
|
+
handle: number,
|
|
732
|
+
keyPtr: number,
|
|
733
|
+
keyLen: number,
|
|
734
|
+
valPtr: number,
|
|
735
|
+
valLen: number,
|
|
736
|
+
): number {
|
|
737
|
+
const coll = collOf(db, handle);
|
|
738
|
+
if (coll === null) return INVALID_HANDLE;
|
|
739
|
+
if (keyLen > MAX_KEY || valLen > MAX_VALUE) throw new Error('data: key/value too large');
|
|
740
|
+
const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
|
|
741
|
+
if (!this.store.has(sk)) return ABSENT; // enqueue replaces an existing record
|
|
742
|
+
this.store.set(sk, readCopy(ref, valPtr, valLen));
|
|
743
|
+
this.stampVersion(coll, sk);
|
|
744
|
+
return 0;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Frame the newest-`limit` events as `u32 count` then per event a
|
|
748
|
+
// length-prefixed blob (`u32 len + bytes`), newest first; stash + return
|
|
749
|
+
// the blob length. Matches the edge `op_latest` / `toildb::Writer` framing.
|
|
750
|
+
latest(
|
|
751
|
+
ref: MemoryRef,
|
|
752
|
+
db: DbDevState,
|
|
753
|
+
handle: number,
|
|
754
|
+
keyPtr: number,
|
|
755
|
+
keyLen: number,
|
|
756
|
+
limit: number,
|
|
757
|
+
): number {
|
|
758
|
+
const coll = collOf(db, handle);
|
|
759
|
+
if (coll === null) return INVALID_HANDLE;
|
|
760
|
+
const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
|
|
761
|
+
const log = this.events.get(sk) ?? [];
|
|
762
|
+
const vers = this.eventVersions.get(sk) ?? [];
|
|
763
|
+
const n = Math.max(0, Math.min(limit, 0xffff));
|
|
764
|
+
const start = Math.max(0, log.length - n);
|
|
765
|
+
const newest = log.slice(start).reverse();
|
|
766
|
+
const newestVers = vers.slice(start).reverse();
|
|
767
|
+
// u32 count, then per event its stored schema_version (u32) + bytes (u32 len
|
|
768
|
+
// + bytes), newest first. Same framing as the edge op_latest.
|
|
769
|
+
const w = new DataWriter();
|
|
770
|
+
w.writeU32(newest.length);
|
|
771
|
+
for (let i = 0; i < newest.length; i++) {
|
|
772
|
+
w.writeU32(newestVers[i] ?? 0).writeBytes(newest[i]);
|
|
773
|
+
}
|
|
774
|
+
db.lastResult = Buffer.from(w.toBytes());
|
|
775
|
+
return db.lastResult.length;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// --- capacity family (escrow: set_total / available / reserve / confirm / cancel) ---
|
|
779
|
+
|
|
780
|
+
// Set the ceiling (restock / reduce). Job/derive only (kind-gated upstream).
|
|
781
|
+
// A ceiling is never negative.
|
|
782
|
+
capacitySetTotal(
|
|
783
|
+
ref: MemoryRef,
|
|
784
|
+
db: DbDevState,
|
|
785
|
+
handle: number,
|
|
786
|
+
keyPtr: number,
|
|
787
|
+
keyLen: number,
|
|
788
|
+
total: number | bigint,
|
|
789
|
+
): number {
|
|
790
|
+
const coll = collOf(db, handle);
|
|
791
|
+
if (coll === null) return INVALID_HANDLE;
|
|
792
|
+
const l = this.capLedger(storeKey(coll, readKey(ref, keyPtr, keyLen)));
|
|
793
|
+
const t = BigInt(total);
|
|
794
|
+
l.total = satI64(t < 0n ? 0n : t);
|
|
795
|
+
return 0;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Stash the i64 available (total - reserved [held + confirmed], floored at 0).
|
|
799
|
+
capacityAvailable(
|
|
800
|
+
ref: MemoryRef,
|
|
801
|
+
db: DbDevState,
|
|
802
|
+
handle: number,
|
|
803
|
+
keyPtr: number,
|
|
804
|
+
keyLen: number,
|
|
805
|
+
): number {
|
|
806
|
+
const coll = collOf(db, handle);
|
|
807
|
+
if (coll === null) return INVALID_HANDLE;
|
|
808
|
+
const l = this.capLedger(storeKey(coll, readKey(ref, keyPtr, keyLen)));
|
|
809
|
+
this.capPrune(l, Date.now());
|
|
810
|
+
const avail = l.total - this.capReserved(l);
|
|
811
|
+
const out = Buffer.alloc(8);
|
|
812
|
+
out.writeBigInt64LE(avail < 0n ? 0n : avail);
|
|
813
|
+
db.lastResult = out;
|
|
814
|
+
return out.length;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Hold `amount` for `ttlMs`: stash the u64 reservation id (8 bytes) on
|
|
818
|
+
// success. A non-positive amount is a typed error (CODEC_ERR), matching the
|
|
819
|
+
// edge's BadAmount; insufficient available OR too many live reservations is
|
|
820
|
+
// ABSENT (-2) (the guest maps that to reservation 0 = no oversell). The TTL
|
|
821
|
+
// is clamped to the edge's 24h ceiling. `now` is the HOST clock.
|
|
822
|
+
capacityReserve(
|
|
823
|
+
ref: MemoryRef,
|
|
824
|
+
db: DbDevState,
|
|
825
|
+
handle: number,
|
|
826
|
+
keyPtr: number,
|
|
827
|
+
keyLen: number,
|
|
828
|
+
amount: number | bigint,
|
|
829
|
+
ttlMs: number | bigint,
|
|
830
|
+
): number {
|
|
831
|
+
const coll = collOf(db, handle);
|
|
832
|
+
if (coll === null) return INVALID_HANDLE;
|
|
833
|
+
const want = BigInt(amount);
|
|
834
|
+
if (want <= 0n) return CODEC_ERR; // BadAmount (edge: -1006)
|
|
835
|
+
const l = this.capLedger(storeKey(coll, readKey(ref, keyPtr, keyLen)));
|
|
836
|
+
const now = Date.now();
|
|
837
|
+
this.capPrune(l, now);
|
|
838
|
+
if (l.total - this.capReserved(l) < want || l.reservations.size >= MAX_RESERVATIONS)
|
|
839
|
+
return ABSENT; // never oversell; bound the reservation count
|
|
840
|
+
const ttl = Math.min(Math.max(0, Number(ttlMs)), MAX_RESERVATION_TTL_MS);
|
|
841
|
+
const id = l.nextId++;
|
|
842
|
+
l.reservations.set(id, { amount: want, expiresMs: now + ttl, confirmed: false });
|
|
843
|
+
const out = Buffer.alloc(8);
|
|
844
|
+
out.writeBigUInt64LE(id);
|
|
845
|
+
db.lastResult = out;
|
|
846
|
+
return out.length;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Finalize a reservation into a permanent consume. IDEMPOTENT: the
|
|
850
|
+
// reservation is flagged confirmed (and kept), so a retry of a settled id
|
|
851
|
+
// still returns 1; 0 only when the id is unknown / expired-and-pruned.
|
|
852
|
+
capacityConfirm(
|
|
853
|
+
ref: MemoryRef,
|
|
854
|
+
db: DbDevState,
|
|
855
|
+
handle: number,
|
|
856
|
+
keyPtr: number,
|
|
857
|
+
keyLen: number,
|
|
858
|
+
reservationId: number | bigint,
|
|
859
|
+
): number {
|
|
860
|
+
const coll = collOf(db, handle);
|
|
861
|
+
if (coll === null) return INVALID_HANDLE;
|
|
862
|
+
const l = this.capLedger(storeKey(coll, readKey(ref, keyPtr, keyLen)));
|
|
863
|
+
this.capPrune(l, Date.now());
|
|
864
|
+
const r = l.reservations.get(BigInt(reservationId));
|
|
865
|
+
if (r === undefined) return 0;
|
|
866
|
+
r.confirmed = true;
|
|
867
|
+
return 1;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Release a HELD reservation back to available. A confirmed sale cannot be
|
|
871
|
+
// cancelled (returns 0), nor an unknown id.
|
|
872
|
+
capacityCancel(
|
|
873
|
+
ref: MemoryRef,
|
|
874
|
+
db: DbDevState,
|
|
875
|
+
handle: number,
|
|
876
|
+
keyPtr: number,
|
|
877
|
+
keyLen: number,
|
|
878
|
+
reservationId: number | bigint,
|
|
879
|
+
): number {
|
|
880
|
+
const coll = collOf(db, handle);
|
|
881
|
+
if (coll === null) return INVALID_HANDLE;
|
|
882
|
+
const l = this.capLedger(storeKey(coll, readKey(ref, keyPtr, keyLen)));
|
|
883
|
+
this.capPrune(l, Date.now());
|
|
884
|
+
const id = BigInt(reservationId);
|
|
885
|
+
const r = l.reservations.get(id);
|
|
886
|
+
if (r === undefined || r.confirmed) return 0;
|
|
887
|
+
l.reservations.delete(id);
|
|
888
|
+
return 1;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// --- result pull + per-row metadata ---
|
|
892
|
+
|
|
893
|
+
// Drain the last stashed variable-length result into the caller buffer.
|
|
894
|
+
takeResult(ref: MemoryRef, db: DbDevState, outPtr: number, outCap: number): number {
|
|
895
|
+
const v = db.lastResult;
|
|
896
|
+
if (v === null) return 0;
|
|
897
|
+
if (v.length > outCap) return TOO_SMALL; // keep the stash for retry
|
|
898
|
+
const m = mem(ref);
|
|
899
|
+
if (outPtr < 0 || outPtr + v.length > m.length)
|
|
900
|
+
throw new Error('data: take_result out of bounds');
|
|
901
|
+
v.copy(m, outPtr);
|
|
902
|
+
db.lastResult = null;
|
|
903
|
+
return v.length;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// `data.result_schema_version() -> i64`: the schema_version the last
|
|
907
|
+
// value-returning read's row was written under, so the guest's woven decoder
|
|
908
|
+
// runs the right @migrate. With on-disk persistence + catalog version
|
|
909
|
+
// stamping, dev DOES surface historical versions: a row written under an old
|
|
910
|
+
// @data layout reports that old version after the type evolves, so the dev
|
|
911
|
+
// server exercises real cross-version decode. -1 means the last op returned
|
|
912
|
+
// no value. An i64 result returns a BigInt in Node's WASM ABI.
|
|
913
|
+
resultSchemaVersion(db: DbDevState): bigint {
|
|
914
|
+
return BigInt(db.lastResultVersion);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// --- test hooks ---
|
|
918
|
+
|
|
919
|
+
/** Test-only: clear the stores + catalog + persistence between unit tests. */
|
|
920
|
+
resetForTests(): void {
|
|
921
|
+
this.clear();
|
|
922
|
+
this.catalog = new Map();
|
|
923
|
+
this.persistPath = null;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/** Test-only: seed the catalog (collection -> current schema_version) directly. */
|
|
927
|
+
setCatalogForTests(entries: Record<string, number>): void {
|
|
928
|
+
this.catalog = new Map(Object.entries(entries));
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/** The single process-lifetime dev database (one store per dev/self-host process). */
|
|
933
|
+
export const devDb = new DevDatabase();
|
|
934
|
+
|
|
935
|
+
/** (Re)load the collection -> current schema_version map from a server wasm. */
|
|
936
|
+
export function setDbCatalog(wasm: Buffer): void {
|
|
937
|
+
devDb.setCatalog(wasm);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/** Point the dev DB at an on-disk file and load any existing snapshot. */
|
|
941
|
+
export function configureDbPersistence(filePath: string): void {
|
|
942
|
+
devDb.configurePersistence(filePath);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** Write the current store to disk (no-op if persistence is not configured). */
|
|
946
|
+
export function persistDb(): void {
|
|
947
|
+
devDb.persist();
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Build the `data.*` host imports for one instance, delegating to the shared
|
|
952
|
+
* {@link devDb}. `db` is the per-request state (handles + result stash); bind a
|
|
953
|
+
* fresh one per dispatch.
|
|
954
|
+
*/
|
|
955
|
+
export function buildDatabaseImports(
|
|
956
|
+
ref: MemoryRef,
|
|
957
|
+
db: DbDevState,
|
|
958
|
+
): Record<string, (...args: number[]) => number | bigint> {
|
|
959
|
+
return {
|
|
960
|
+
'data.resolve_collection': (
|
|
961
|
+
namePtr: number,
|
|
962
|
+
nameLen: number,
|
|
963
|
+
outHandlePtr: number,
|
|
964
|
+
): number => devDb.resolveCollection(ref, db, namePtr, nameLen, outHandlePtr),
|
|
965
|
+
|
|
966
|
+
'data.get': (handle: number, keyPtr: number, keyLen: number): number =>
|
|
967
|
+
devDb.get(ref, db, handle, keyPtr, keyLen),
|
|
968
|
+
|
|
969
|
+
'data.get_many': (handle: number, keysPtr: number, keysLen: number): number =>
|
|
970
|
+
devDb.getMany(ref, db, handle, keysPtr, keysLen),
|
|
971
|
+
|
|
972
|
+
'data.exists': (handle: number, keyPtr: number, keyLen: number): number =>
|
|
973
|
+
devDb.exists(ref, db, handle, keyPtr, keyLen),
|
|
974
|
+
|
|
975
|
+
'data.create': (
|
|
976
|
+
handle: number,
|
|
977
|
+
keyPtr: number,
|
|
978
|
+
keyLen: number,
|
|
979
|
+
valPtr: number,
|
|
980
|
+
valLen: number,
|
|
981
|
+
_idemPtr: number,
|
|
982
|
+
): number => devDb.create(ref, db, handle, keyPtr, keyLen, valPtr, valLen),
|
|
983
|
+
|
|
984
|
+
'data.patch': (
|
|
985
|
+
handle: number,
|
|
986
|
+
keyPtr: number,
|
|
987
|
+
keyLen: number,
|
|
988
|
+
patchPtr: number,
|
|
989
|
+
patchLen: number,
|
|
990
|
+
_idemPtr: number,
|
|
991
|
+
): number => devDb.patch(ref, db, handle, keyPtr, keyLen, patchPtr, patchLen),
|
|
992
|
+
|
|
993
|
+
'data.delete': (handle: number, keyPtr: number, keyLen: number, _idemPtr: number): number =>
|
|
994
|
+
devDb.delete(ref, db, handle, keyPtr, keyLen),
|
|
995
|
+
|
|
996
|
+
'data.get_delete': (
|
|
997
|
+
handle: number,
|
|
998
|
+
keyPtr: number,
|
|
999
|
+
keyLen: number,
|
|
1000
|
+
_idemPtr: number,
|
|
1001
|
+
): number => devDb.getDelete(ref, db, handle, keyPtr, keyLen),
|
|
1002
|
+
|
|
1003
|
+
'data.unique_lookup': (handle: number, keyPtr: number, keyLen: number): number =>
|
|
1004
|
+
devDb.uniqueLookup(ref, db, handle, keyPtr, keyLen),
|
|
1005
|
+
|
|
1006
|
+
'data.unique_claim': (
|
|
1007
|
+
handle: number,
|
|
1008
|
+
keyPtr: number,
|
|
1009
|
+
keyLen: number,
|
|
1010
|
+
valPtr: number,
|
|
1011
|
+
valLen: number,
|
|
1012
|
+
_idemPtr: number,
|
|
1013
|
+
): number => devDb.uniqueClaim(ref, db, handle, keyPtr, keyLen, valPtr, valLen),
|
|
1014
|
+
|
|
1015
|
+
'data.unique_release': (
|
|
1016
|
+
handle: number,
|
|
1017
|
+
keyPtr: number,
|
|
1018
|
+
keyLen: number,
|
|
1019
|
+
valPtr: number,
|
|
1020
|
+
valLen: number,
|
|
1021
|
+
_idemPtr: number,
|
|
1022
|
+
): number => devDb.uniqueRelease(ref, db, handle, keyPtr, keyLen, valPtr, valLen),
|
|
1023
|
+
|
|
1024
|
+
'data.membership_contains': (
|
|
1025
|
+
handle: number,
|
|
1026
|
+
setPtr: number,
|
|
1027
|
+
setLen: number,
|
|
1028
|
+
memberPtr: number,
|
|
1029
|
+
memberLen: number,
|
|
1030
|
+
): number =>
|
|
1031
|
+
devDb.membershipContains(ref, db, handle, setPtr, setLen, memberPtr, memberLen),
|
|
1032
|
+
|
|
1033
|
+
'data.membership_add': (
|
|
1034
|
+
handle: number,
|
|
1035
|
+
setPtr: number,
|
|
1036
|
+
setLen: number,
|
|
1037
|
+
memberPtr: number,
|
|
1038
|
+
memberLen: number,
|
|
1039
|
+
_idemPtr: number,
|
|
1040
|
+
): number => devDb.membershipAdd(ref, db, handle, setPtr, setLen, memberPtr, memberLen),
|
|
1041
|
+
|
|
1042
|
+
'data.membership_remove': (
|
|
1043
|
+
handle: number,
|
|
1044
|
+
setPtr: number,
|
|
1045
|
+
setLen: number,
|
|
1046
|
+
memberPtr: number,
|
|
1047
|
+
memberLen: number,
|
|
1048
|
+
_idemPtr: number,
|
|
1049
|
+
): number => devDb.membershipRemove(ref, db, handle, setPtr, setLen, memberPtr, memberLen),
|
|
1050
|
+
|
|
1051
|
+
'data.membership_list': (
|
|
1052
|
+
handle: number,
|
|
1053
|
+
setPtr: number,
|
|
1054
|
+
setLen: number,
|
|
1055
|
+
limit: number,
|
|
1056
|
+
): number => devDb.membershipList(ref, db, handle, setPtr, setLen, limit),
|
|
1057
|
+
|
|
1058
|
+
'data.view_get': (handle: number, keyPtr: number, keyLen: number): number =>
|
|
1059
|
+
devDb.viewGet(ref, db, handle, keyPtr, keyLen),
|
|
1060
|
+
|
|
1061
|
+
'data.view_publish': (
|
|
1062
|
+
handle: number,
|
|
1063
|
+
keyPtr: number,
|
|
1064
|
+
keyLen: number,
|
|
1065
|
+
valPtr: number,
|
|
1066
|
+
valLen: number,
|
|
1067
|
+
_idemPtr: number,
|
|
1068
|
+
): number => devDb.viewPublish(ref, db, handle, keyPtr, keyLen, valPtr, valLen),
|
|
1069
|
+
|
|
1070
|
+
'data.counter_get': (handle: number, keyPtr: number, keyLen: number): number =>
|
|
1071
|
+
devDb.counterGet(ref, db, handle, keyPtr, keyLen),
|
|
1072
|
+
|
|
1073
|
+
'data.counter_add': (
|
|
1074
|
+
handle: number,
|
|
1075
|
+
keyPtr: number,
|
|
1076
|
+
keyLen: number,
|
|
1077
|
+
delta: number | bigint,
|
|
1078
|
+
_idemPtr: number,
|
|
1079
|
+
): number => devDb.counterAdd(ref, db, handle, keyPtr, keyLen, delta),
|
|
1080
|
+
|
|
1081
|
+
'data.append': (
|
|
1082
|
+
handle: number,
|
|
1083
|
+
keyPtr: number,
|
|
1084
|
+
keyLen: number,
|
|
1085
|
+
evPtr: number,
|
|
1086
|
+
evLen: number,
|
|
1087
|
+
_idemPtr: number,
|
|
1088
|
+
): number => devDb.append(ref, db, handle, keyPtr, keyLen, evPtr, evLen),
|
|
1089
|
+
|
|
1090
|
+
'data.append_once': (
|
|
1091
|
+
handle: number,
|
|
1092
|
+
keyPtr: number,
|
|
1093
|
+
keyLen: number,
|
|
1094
|
+
evidPtr: number,
|
|
1095
|
+
evidLen: number,
|
|
1096
|
+
evPtr: number,
|
|
1097
|
+
evLen: number,
|
|
1098
|
+
): number =>
|
|
1099
|
+
devDb.appendOnce(ref, db, handle, keyPtr, keyLen, evidPtr, evidLen, evPtr, evLen),
|
|
1100
|
+
|
|
1101
|
+
'data.enqueue': (
|
|
1102
|
+
handle: number,
|
|
1103
|
+
keyPtr: number,
|
|
1104
|
+
keyLen: number,
|
|
1105
|
+
valPtr: number,
|
|
1106
|
+
valLen: number,
|
|
1107
|
+
_idemPtr: number,
|
|
1108
|
+
): number => devDb.enqueue(ref, db, handle, keyPtr, keyLen, valPtr, valLen),
|
|
1109
|
+
|
|
1110
|
+
'data.latest': (handle: number, keyPtr: number, keyLen: number, limit: number): number =>
|
|
1111
|
+
devDb.latest(ref, db, handle, keyPtr, keyLen, limit),
|
|
1112
|
+
|
|
1113
|
+
'data.capacity_set_total': (
|
|
1114
|
+
handle: number,
|
|
1115
|
+
keyPtr: number,
|
|
1116
|
+
keyLen: number,
|
|
1117
|
+
total: number | bigint,
|
|
1118
|
+
_idemPtr: number,
|
|
1119
|
+
): number => devDb.capacitySetTotal(ref, db, handle, keyPtr, keyLen, total),
|
|
1120
|
+
|
|
1121
|
+
'data.capacity_available': (handle: number, keyPtr: number, keyLen: number): number =>
|
|
1122
|
+
devDb.capacityAvailable(ref, db, handle, keyPtr, keyLen),
|
|
1123
|
+
|
|
1124
|
+
'data.capacity_reserve': (
|
|
1125
|
+
handle: number,
|
|
1126
|
+
keyPtr: number,
|
|
1127
|
+
keyLen: number,
|
|
1128
|
+
amount: number | bigint,
|
|
1129
|
+
ttlMs: number | bigint,
|
|
1130
|
+
_idemPtr: number,
|
|
1131
|
+
): number => devDb.capacityReserve(ref, db, handle, keyPtr, keyLen, amount, ttlMs),
|
|
1132
|
+
|
|
1133
|
+
'data.capacity_confirm': (
|
|
1134
|
+
handle: number,
|
|
1135
|
+
keyPtr: number,
|
|
1136
|
+
keyLen: number,
|
|
1137
|
+
reservationId: number | bigint,
|
|
1138
|
+
_idemPtr: number,
|
|
1139
|
+
): number => devDb.capacityConfirm(ref, db, handle, keyPtr, keyLen, reservationId),
|
|
1140
|
+
|
|
1141
|
+
'data.capacity_cancel': (
|
|
1142
|
+
handle: number,
|
|
1143
|
+
keyPtr: number,
|
|
1144
|
+
keyLen: number,
|
|
1145
|
+
reservationId: number | bigint,
|
|
1146
|
+
_idemPtr: number,
|
|
1147
|
+
): number => devDb.capacityCancel(ref, db, handle, keyPtr, keyLen, reservationId),
|
|
1148
|
+
|
|
1149
|
+
'data.take_result': (outPtr: number, outCap: number): number =>
|
|
1150
|
+
devDb.takeResult(ref, db, outPtr, outCap),
|
|
1151
|
+
|
|
1152
|
+
'data.result_schema_version': (): bigint => devDb.resultSchemaVersion(db),
|
|
1153
|
+
|
|
1154
|
+
// `data.write_allowed() -> i32`: 1 if the current call may write. Used by the
|
|
1155
|
+
// rewrite-on-read convergence after a lazy migration to persist the converged
|
|
1156
|
+
// row. The dev store permits writes, so return 1.
|
|
1157
|
+
'data.write_allowed': (): number => 1,
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/** Test-only: clear the stores + catalog + persistence between unit tests. */
|
|
1162
|
+
export function __resetDbForTests(): void {
|
|
1163
|
+
devDb.resetForTests();
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/** Test-only: seed the catalog (collection -> current schema_version) directly. */
|
|
1167
|
+
export function __setDbCatalogForTests(entries: Record<string, number>): void {
|
|
1168
|
+
devDb.setCatalogForTests(entries);
|
|
1169
|
+
}
|