toiljs 0.0.124 → 0.0.125

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.
@@ -1,6 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import { performance } from 'node:perf_hooks';
3
3
  import pc from 'picocolors';
4
+ import { DbFunctionKind, persistDb, parseDerives, derivesForWrites, commitDeriveCheckpoints, setDbCatalog, } from '../db/index.js';
5
+ import { flushMaterializations, queueMaterialization } from '../db/materialization.js';
6
+ import { resetDbLimits } from '../db/limits.js';
4
7
  import { parseSurface } from '../wasm/surface.js';
5
8
  import { parseDaemonCatalog, } from './catalog.js';
6
9
  import { cronMatches, cronNeverFires, nextCronFireMs } from './cron.js';
@@ -35,8 +38,11 @@ export class DaemonHost {
35
38
  running = false;
36
39
  epochValue = 0n;
37
40
  timers = new Map();
41
+ deriveRetryTimer = null;
38
42
  nextFire = new Map();
39
43
  ticking = new Set();
44
+ derives = [];
45
+ pendingDerives = new Set();
40
46
  constructor(coldWasmPath, cfg, nodeMode, log = (s) => process.stdout.write(s)) {
41
47
  this.coldWasmPath = coldWasmPath;
42
48
  this.cfg = cfg;
@@ -115,6 +121,11 @@ export class DaemonHost {
115
121
  return false;
116
122
  }
117
123
  this.module = new WebAssembly.Module(bytes);
124
+ setDbCatalog(bytes, true);
125
+ this.derives = parseDerives(bytes);
126
+ this.pendingDerives.clear();
127
+ for (const derive of this.derives)
128
+ this.pendingDerives.add(derive.deriveId);
118
129
  this.catalog = catalog;
119
130
  this.loadedMtimeMs = mtimeMs;
120
131
  this.epochValue += 1n;
@@ -126,6 +137,7 @@ export class DaemonHost {
126
137
  return;
127
138
  const ref = { memory: null };
128
139
  this.state = freshDaemonState();
140
+ this.state.db.functionKind = DbFunctionKind.Action;
129
141
  const imports = buildDaemonImports(ref, this.state, this);
130
142
  const provided = imports;
131
143
  const missing = WebAssembly.Module.imports(this.module)
@@ -139,9 +151,24 @@ export class DaemonHost {
139
151
  this.instance = new WebAssembly.Instance(this.module, imports);
140
152
  this.exports = this.instance.exports;
141
153
  ref.memory = this.exports.memory;
154
+ if (this.derives.length > 0 && typeof this.exports.derive_run !== 'function') {
155
+ this.log(pc.red(' ✗ cold materializers require derive_run(i32) -> i64') + '\n');
156
+ this.instance = null;
157
+ this.exports = null;
158
+ return;
159
+ }
142
160
  try {
143
- if (typeof this.exports.init === 'function')
144
- this.exports.init();
161
+ if (typeof this.exports.init === 'function') {
162
+ const initRc = this.exports.init();
163
+ if (initRc !== 0) {
164
+ this.log(pc.red(` ✗ init() returned ${String(initRc)}; daemon not running`) + '\n');
165
+ this.instance = null;
166
+ this.exports = null;
167
+ return;
168
+ }
169
+ this.state.db.logMessages = 0;
170
+ resetDbLimits(this.state.db);
171
+ }
145
172
  const rc = this.exports.daemon_start();
146
173
  if (rc !== 0) {
147
174
  this.log(pc.red(` ✗ daemon_start() returned ${String(rc)}; daemon not running`) + '\n');
@@ -156,7 +183,15 @@ export class DaemonHost {
156
183
  this.exports = null;
157
184
  return;
158
185
  }
186
+ finally {
187
+ this.materializeWrites();
188
+ }
159
189
  this.running = true;
190
+ this.deriveRetryTimer = setInterval(() => {
191
+ if (this.running && this.pendingDerives.size > 0 && this.isLeader())
192
+ this.materializeWrites();
193
+ }, 1000);
194
+ this.deriveRetryTimer.unref?.();
160
195
  const limited = this.catalog.tasks.slice(0, this.cfg.maxTasks);
161
196
  for (const task of limited)
162
197
  this.registerTask(task);
@@ -165,6 +200,9 @@ export class DaemonHost {
165
200
  '\n');
166
201
  }
167
202
  stop() {
203
+ if (this.deriveRetryTimer !== null)
204
+ clearInterval(this.deriveRetryTimer);
205
+ this.deriveRetryTimer = null;
168
206
  for (const t of this.timers.values())
169
207
  clearTimeout(t);
170
208
  this.timers.clear();
@@ -230,6 +268,10 @@ export class DaemonHost {
230
268
  if (!this.isLeader())
231
269
  return;
232
270
  this.ticking.add(task.taskIndex);
271
+ this.state.db.functionKind = DbFunctionKind.Action;
272
+ this.state.db.logMessages = 0;
273
+ resetDbLimits(this.state.db);
274
+ this.state.db.writtenCollections.clear();
233
275
  const startedAt = performance.now();
234
276
  try {
235
277
  const ret = this.exports.scheduled_tick(task.taskIndex);
@@ -241,12 +283,52 @@ export class DaemonHost {
241
283
  this.log(pc.red(` ✗ @scheduled ${task.name} trapped: ${String(e)}`) + '\n');
242
284
  }
243
285
  finally {
286
+ this.materializeWrites();
244
287
  const took = performance.now() - startedAt;
245
288
  if (took > this.cfg.tickBudgetMs)
246
289
  this.log(pc.yellow(` ⏱ @scheduled ${task.name} took ${String(took)}ms (> tickBudgetMs ${String(this.cfg.tickBudgetMs)})`) + '\n');
247
290
  this.ticking.delete(task.taskIndex);
248
291
  }
249
292
  }
293
+ materializeWrites() {
294
+ if (this.derives.length > 0 && this.exports?.derive_run !== undefined) {
295
+ for (const derive of derivesForWrites(this.derives, this.state.db.writtenCollections))
296
+ this.pendingDerives.add(derive.deriveId);
297
+ const db = this.state.db;
298
+ const kind = db.functionKind;
299
+ try {
300
+ db.functionKind = DbFunctionKind.Derive;
301
+ for (const derive of this.derives.filter((d) => this.pendingDerives.has(d.deriveId))) {
302
+ db.deriveId = derive.deriveId;
303
+ db.logMessages = 0;
304
+ resetDbLimits(db);
305
+ db.pendingCheckpoints.clear();
306
+ try {
307
+ const rc = this.exports.derive_run(derive.deriveId);
308
+ if (rc !== 0n)
309
+ throw new Error(`derive_run returned ${String(rc)}`);
310
+ commitDeriveCheckpoints(db);
311
+ this.pendingDerives.delete(derive.deriveId);
312
+ }
313
+ catch (error) {
314
+ this.log(pc.red(` ✗ daemon derive ${derive.dbName}.${derive.methodName} failed; retry in one second: ${String(error)}`) + '\n');
315
+ break;
316
+ }
317
+ }
318
+ }
319
+ finally {
320
+ db.functionKind = kind;
321
+ db.pendingCheckpoints.clear();
322
+ }
323
+ }
324
+ else {
325
+ for (const collection of this.state.db.writtenCollections)
326
+ queueMaterialization(collection);
327
+ flushMaterializations();
328
+ }
329
+ persistDb();
330
+ this.state.db.writtenCollections.clear();
331
+ }
250
332
  close() {
251
333
  if (this.running)
252
334
  this.stop();
@@ -23,10 +23,13 @@ export declare class DevDatabase {
23
23
  private readonly capacity;
24
24
  private readonly versions;
25
25
  private readonly eventVersions;
26
+ private readonly eventHlcs;
27
+ private readonly erasedEvents;
28
+ private eventClock;
26
29
  private readonly memberVersions;
27
30
  private catalog;
28
31
  private persistPath;
29
- setCatalog(wasm: Buffer): void;
32
+ setCatalog(wasm: Buffer, merge?: boolean): void;
30
33
  private currentSchemaVersion;
31
34
  private stampVersion;
32
35
  private recordIdemStart;
@@ -57,8 +60,10 @@ export declare class DevDatabase {
57
60
  membershipAdd(ref: MemoryRef, db: DbDevState, handle: number, setPtr: number, setLen: number, memberPtr: number, memberLen: number): number;
58
61
  membershipRemove(ref: MemoryRef, db: DbDevState, handle: number, setPtr: number, setLen: number, memberPtr: number, memberLen: number): number;
59
62
  membershipList(ref: MemoryRef, db: DbDevState, handle: number, setPtr: number, setLen: number, limit: number): number;
63
+ membershipPage(ref: MemoryRef, db: DbDevState, handle: number, setPtr: number, setLen: number, cursorPtr: number, cursorLen: number, limit: number): number;
60
64
  viewGet(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number;
61
65
  viewPublish(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, valPtr: number, valLen: number): number;
66
+ viewDelete(ref: MemoryRef, db: DbDevState, handle: number, kp: number, kl: number): number;
62
67
  counterGet(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number;
63
68
  private recordWrite;
64
69
  counterAdd(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, delta: number | bigint, idemPtr: number): number;
@@ -67,7 +72,11 @@ export declare class DevDatabase {
67
72
  eventsGet(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, evidPtr: number, evidLen: number): number;
68
73
  eventsLast(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number): number;
69
74
  private eventResult;
75
+ eventsReference(ref: MemoryRef, db: DbDevState, handle: number, kp: number, kl: number, ip: number, il: number): number;
76
+ eventsRedact(ref: MemoryRef, db: DbDevState, handle: number, kp: number, kl: number, seq: bigint, hlc: bigint): number;
77
+ eventsEntries(ref: MemoryRef, db: DbDevState, handle: number, kp: number, kl: number, limit: number, after: bigint): number;
70
78
  compareExchange(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, expectedPtr: number, expectedLen: number, valuePtr: number, valueLen: number): number;
79
+ compareDelete(ref: MemoryRef, db: DbDevState, handle: number, kp: number, kl: number, ep: number, el: number): number;
71
80
  enqueue(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, valPtr: number, valLen: number, idemPtr: number): number;
72
81
  latest(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, limit: number): number;
73
82
  since(ref: MemoryRef, db: DbDevState, handle: number, keyPtr: number, keyLen: number, limit: number): number;
@@ -83,7 +92,7 @@ export declare class DevDatabase {
83
92
  setCatalogForTests(entries: Record<string, CatalogSeedEntry>): void;
84
93
  }
85
94
  export declare const devDb: DevDatabase;
86
- export declare function setDbCatalog(wasm: Buffer): void;
95
+ export declare function setDbCatalog(wasm: Buffer, merge?: boolean): void;
87
96
  export declare function configureDbPersistence(filePath: string): void;
88
97
  export declare function persistDb(): void;
89
98
  export declare function commitDeriveCheckpoints(db: DbDevState): void;
@@ -3,7 +3,9 @@ import { createHash } from 'node:crypto';
3
3
  import path from 'node:path';
4
4
  import { DataReader, DataWriter } from 'toiljs/io';
5
5
  import { parseCatalog } from './catalog.js';
6
- import { ABSENT, ALREADY_EXISTS, CODEC_ERR, CollectionFamily, CONFLICT, DbFunctionKind, DEFAULT_FILL_WAIT_MS, INVALID_HANDLE, MAX_KEY, MAX_FILL_WAIT_MS, MAX_NAME, MAX_RESERVATION_TTL_MS, MAX_RESERVATIONS, MAX_VALUE, OP_NOT_ALLOWED_FOR_FAMILY, OP_NOT_ALLOWED_IN_KIND, satI64, SCHEMA_UNAVAILABLE, TOO_MANY_KEYS, TOO_SMALL, UNAVAILABLE, isCollectionFamily, } from './types.js';
6
+ import { queueMaterialization } from './materialization.js';
7
+ import { checkDbResultSize, withDbLimits } from './limits.js';
8
+ import { ABSENT, ALREADY_EXISTS, CODEC_ERR, CollectionFamily, CONFLICT, DbFunctionKind, DEFAULT_FILL_WAIT_MS, INVALID_HANDLE, MAX_KEY, MAX_FILL_WAIT_MS, MAX_NAME, MAX_RESERVATION_TTL_MS, MAX_RESERVATIONS, MAX_VALUE, OP_NOT_ALLOWED_FOR_FAMILY, OP_NOT_ALLOWED_IN_KIND, satI64, SCHEMA_UNAVAILABLE, TOO_SMALL, UNAVAILABLE, isCollectionFamily, } from './types.js';
7
9
  function mem(ref) {
8
10
  if (!ref.memory)
9
11
  throw new Error('data host import called before memory was bound');
@@ -131,10 +133,14 @@ var DbOp;
131
133
  DbOp[DbOp["CapacityCancel"] = 24] = "CapacityCancel";
132
134
  DbOp[DbOp["ViewGet"] = 25] = "ViewGet";
133
135
  DbOp[DbOp["ViewPublish"] = 26] = "ViewPublish";
134
- DbOp[DbOp["CapacitySetTotal"] = 27] = "CapacitySetTotal";
135
- DbOp[DbOp["EventsSince"] = 28] = "EventsSince";
136
- DbOp[DbOp["EventsGet"] = 29] = "EventsGet";
137
- DbOp[DbOp["EventsLast"] = 30] = "EventsLast";
136
+ DbOp[DbOp["ViewDelete"] = 27] = "ViewDelete";
137
+ DbOp[DbOp["CapacitySetTotal"] = 28] = "CapacitySetTotal";
138
+ DbOp[DbOp["EventsSince"] = 29] = "EventsSince";
139
+ DbOp[DbOp["EventsGet"] = 30] = "EventsGet";
140
+ DbOp[DbOp["EventsLast"] = 31] = "EventsLast";
141
+ DbOp[DbOp["EventsEntries"] = 32] = "EventsEntries";
142
+ DbOp[DbOp["EventsReference"] = 33] = "EventsReference";
143
+ DbOp[DbOp["EventsRedact"] = 34] = "EventsRedact";
138
144
  })(DbOp || (DbOp = {}));
139
145
  var UpsertResult;
140
146
  (function (UpsertResult) {
@@ -155,10 +161,12 @@ function isReadOp(op) {
155
161
  op === DbOp.EventsSince ||
156
162
  op === DbOp.EventsGet ||
157
163
  op === DbOp.EventsLast ||
164
+ op === DbOp.EventsEntries ||
165
+ op === DbOp.EventsReference ||
158
166
  op === DbOp.CapacityAvailable);
159
167
  }
160
168
  function isScanOp(op) {
161
- return op === DbOp.Latest || op === DbOp.MembershipList || op === DbOp.EventsSince;
169
+ return op === DbOp.Latest || op === DbOp.MembershipList || op === DbOp.EventsSince || op === DbOp.EventsEntries;
162
170
  }
163
171
  function kindAllows(kind, op) {
164
172
  if (kind === DbFunctionKind.Query)
@@ -169,10 +177,12 @@ function kindAllows(kind, op) {
169
177
  op === DbOp.Patch ||
170
178
  op === DbOp.Upsert ||
171
179
  op === DbOp.Delete ||
180
+ op === DbOp.ViewDelete ||
172
181
  op === DbOp.GetDelete ||
173
182
  op === DbOp.Enqueue ||
174
183
  op === DbOp.Append ||
175
184
  op === DbOp.AppendOnce ||
185
+ op === DbOp.EventsRedact ||
176
186
  op === DbOp.CounterAdd ||
177
187
  op === DbOp.MembershipAdd ||
178
188
  op === DbOp.MembershipRemove ||
@@ -183,7 +193,11 @@ function kindAllows(kind, op) {
183
193
  op === DbOp.CapacityCancel);
184
194
  }
185
195
  if (kind === DbFunctionKind.Derive)
186
- return (isReadOp(op) || op === DbOp.ViewPublish || op === DbOp.Append || op === DbOp.CounterAdd);
196
+ return (isReadOp(op) ||
197
+ op === DbOp.ViewPublish ||
198
+ op === DbOp.ViewDelete ||
199
+ op === DbOp.Append ||
200
+ op === DbOp.CounterAdd);
187
201
  if (kind === DbFunctionKind.Job)
188
202
  return true;
189
203
  return false;
@@ -209,11 +223,20 @@ export class DevDatabase {
209
223
  capacity = new Map();
210
224
  versions = new Map();
211
225
  eventVersions = new Map();
226
+ eventHlcs = new Map();
227
+ erasedEvents = new Map();
228
+ eventClock = 0n;
212
229
  memberVersions = new Map();
213
230
  catalog = { kind: 'no-section' };
214
231
  persistPath = null;
215
- setCatalog(wasm) {
216
- this.catalog = parseCatalog(wasm);
232
+ setCatalog(wasm, merge = false) {
233
+ const incoming = parseCatalog(wasm);
234
+ if (merge && this.catalog.kind === 'present' && incoming.kind === 'present') {
235
+ for (const [name, collection] of incoming.collections)
236
+ this.catalog.collections.set(name, collection);
237
+ }
238
+ else if (!merge || incoming.kind !== 'no-section')
239
+ this.catalog = incoming;
217
240
  }
218
241
  currentSchemaVersion(coll) {
219
242
  if (this.catalog.kind !== 'present')
@@ -283,6 +306,7 @@ export class DevDatabase {
283
306
  events: {},
284
307
  eventDedup: {},
285
308
  eventPositions: {},
309
+ erasedEvents: {},
286
310
  deriveCheckpoints: {},
287
311
  capacity: {},
288
312
  };
@@ -311,10 +335,12 @@ export class DevDatabase {
311
335
  snap.counterIdem[k] = v.toString();
312
336
  for (const [k, log] of this.events) {
313
337
  const ver = this.eventVersions.get(k) ?? [];
314
- snap.events[k] = log.map((b, i) => ({ v: b.toString('base64'), sv: ver[i] ?? 0 }));
338
+ snap.events[k] = log.map((b, i) => ({ v: b.toString('base64'), sv: ver[i] ?? 0, hlc: (this.eventHlcs.get(k)?.[i] ?? BigInt(i + 1)).toString() }));
315
339
  }
316
340
  for (const [k, positions] of this.eventPositions)
317
341
  snap.eventPositions[k] = [...positions];
342
+ for (const [k, erased] of this.erasedEvents)
343
+ snap.erasedEvents[k] = [...erased];
318
344
  for (const [k, s] of this.eventDedup)
319
345
  snap.eventDedup[k] = [...s];
320
346
  for (const [k, v] of this.deriveCheckpoints)
@@ -383,6 +409,21 @@ export class DevDatabase {
383
409
  for (const [k, log] of Object.entries(snap.events ?? {})) {
384
410
  this.events.set(k, log.map((e) => Buffer.from(e.v, 'base64')));
385
411
  this.eventVersions.set(k, log.map((e) => e.sv));
412
+ const hlcs = log.map((e, i) => BigInt(e.hlc ?? String(i + 1)));
413
+ this.eventHlcs.set(k, hlcs);
414
+ for (const hlc of hlcs)
415
+ if (hlc > this.eventClock)
416
+ this.eventClock = hlc;
417
+ }
418
+ for (const [k, values] of Object.entries(snap.erasedEvents ?? {})) {
419
+ const erased = new Set(values);
420
+ this.erasedEvents.set(k, erased);
421
+ const log = this.events.get(k) ?? [];
422
+ const hlcs = this.eventHlcs.get(k) ?? [];
423
+ for (let i = 0; i < log.length; i++) {
424
+ if (erased.has(String(i + 1) + ':' + String(hlcs[i] ?? BigInt(i + 1))))
425
+ log[i] = Buffer.alloc(0);
426
+ }
386
427
  }
387
428
  for (const [k, positions] of Object.entries(snap.eventPositions ?? {}))
388
429
  this.eventPositions.set(k, new Map(positions));
@@ -416,6 +457,9 @@ export class DevDatabase {
416
457
  this.counters.clear();
417
458
  this.counterIdem.clear();
418
459
  this.eventVersions.clear();
460
+ this.eventHlcs.clear();
461
+ this.erasedEvents.clear();
462
+ this.eventClock = 0n;
419
463
  this.events.clear();
420
464
  this.eventDedup.clear();
421
465
  this.eventPositions.clear();
@@ -505,15 +549,18 @@ export class DevDatabase {
505
549
  ? 64
506
550
  : 1024;
507
551
  if (count > maxKeys)
508
- return TOO_MANY_KEYS;
552
+ throw new Error(`data.get_many failed: TDL020 batch-key budget exceeded; collection=${coll.name} kind=${db.functionKind} batch_keys=${String(count)} max_batch_keys=${String(maxKeys)}. Split the keys into smaller batches.`);
509
553
  const w = new DataWriter();
510
554
  w.writeU32(count);
555
+ let resultBytes = 4;
511
556
  for (let i = 0; i < count; i++) {
512
557
  const key = r.readBytes();
513
558
  if (key.length > MAX_KEY)
514
559
  throw new Error('data: key too long');
515
560
  const sk = storeKey(coll.name, Buffer.from(key));
516
561
  const v = table.get(sk);
562
+ resultBytes += v === undefined ? 1 : 9 + v.length;
563
+ checkDbResultSize(db, resultBytes);
517
564
  if (v === undefined) {
518
565
  w.writeU8(0);
519
566
  }
@@ -816,6 +863,32 @@ export class DevDatabase {
816
863
  db.lastResult = Buffer.from(w.toBytes());
817
864
  return db.lastResult.length;
818
865
  }
866
+ membershipPage(ref, db, handle, setPtr, setLen, cursorPtr, cursorLen, limit) {
867
+ if (cursorLen < -1 || limit < 1 || limit > 0xffff)
868
+ throw new Error('data.membership_page: invalid cursor length or limit');
869
+ checkValueSize(cursorLen, 'cursor');
870
+ const coll = collForOp(db, handle, DbOp.MembershipList, CollectionFamily.Membership);
871
+ if (typeof coll === 'number')
872
+ return coll;
873
+ if (limit > 1024)
874
+ throw new Error('data.membership_page: TDL020 scan limit exceeds 1024');
875
+ const cursor = cursorLen === -1 ? null : readCopy(ref, cursorPtr, cursorLen);
876
+ const sk = storeKey(coll.name, readKey(ref, setPtr, setLen));
877
+ const members = Array.from(this.members.get(sk)?.values() ?? [])
878
+ .sort(Buffer.compare)
879
+ .filter((m) => cursor === null || Buffer.compare(m, cursor) > 0)
880
+ .slice(0, limit);
881
+ const versions = this.memberVersions.get(sk);
882
+ const w = new DataWriter().writeU32(members.length);
883
+ for (const member of members)
884
+ w.writeU32(versions?.get(member.toString('latin1')) ?? 0).writeBytes(member);
885
+ const next = members.length === limit ? members[members.length - 1] : null;
886
+ w.writeU8(next === null ? 0 : 1);
887
+ if (next !== null)
888
+ w.writeBytes(next);
889
+ db.lastResult = Buffer.from(w.toBytes());
890
+ return db.lastResult.length;
891
+ }
819
892
  viewGet(ref, db, handle, keyPtr, keyLen) {
820
893
  const coll = collForOp(db, handle, DbOp.ViewGet, CollectionFamily.View);
821
894
  if (typeof coll === 'number')
@@ -838,6 +911,15 @@ export class DevDatabase {
838
911
  this.stampVersion(coll, sk);
839
912
  return 0;
840
913
  }
914
+ viewDelete(ref, db, handle, kp, kl) {
915
+ const coll = collForOp(db, handle, DbOp.ViewDelete, CollectionFamily.View);
916
+ if (typeof coll === 'number')
917
+ return coll;
918
+ const key = storeKey(coll.name, readKey(ref, kp, kl));
919
+ this.views.delete(key);
920
+ this.versions.delete(key);
921
+ return 0;
922
+ }
841
923
  counterGet(ref, db, handle, keyPtr, keyLen) {
842
924
  const coll = collForOp(db, handle, DbOp.CounterGet, CollectionFamily.Counter);
843
925
  if (typeof coll === 'number')
@@ -852,6 +934,8 @@ export class DevDatabase {
852
934
  if (db.functionKind === DbFunctionKind.Derive)
853
935
  return;
854
936
  db.writtenCollections.add(coll.name);
937
+ if (db.functionKind === DbFunctionKind.Job)
938
+ queueMaterialization(coll.name);
855
939
  }
856
940
  counterAdd(ref, db, handle, keyPtr, keyLen, delta, idemPtr) {
857
941
  const coll = collForOp(db, handle, DbOp.CounterAdd, CollectionFamily.Counter);
@@ -904,6 +988,11 @@ export class DevDatabase {
904
988
  versions.push(sv);
905
989
  this.eventVersions.set(sk, versions);
906
990
  }
991
+ const hlcs = this.eventHlcs.get(sk) ?? [];
992
+ const now = BigInt(Date.now()) << 16n;
993
+ this.eventClock = now > this.eventClock ? now : this.eventClock + 1n;
994
+ hlcs.push(this.eventClock);
995
+ this.eventHlcs.set(sk, hlcs);
907
996
  this.recordWrite(db, coll);
908
997
  return 0;
909
998
  }
@@ -942,6 +1031,11 @@ export class DevDatabase {
942
1031
  }
943
1032
  positions.set(evid, position);
944
1033
  seen.add(evid);
1034
+ const hlcs = this.eventHlcs.get(sk) ?? [];
1035
+ const now = BigInt(Date.now()) << 16n;
1036
+ this.eventClock = now > this.eventClock ? now : this.eventClock + 1n;
1037
+ hlcs.push(this.eventClock);
1038
+ this.eventHlcs.set(sk, hlcs);
945
1039
  this.recordWrite(db, coll);
946
1040
  return 1;
947
1041
  }
@@ -963,10 +1057,15 @@ export class DevDatabase {
963
1057
  if (typeof coll === 'number')
964
1058
  return coll;
965
1059
  const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
966
- return this.eventResult(db, sk, (this.events.get(sk)?.length ?? 0) - 1);
1060
+ const log = this.events.get(sk) ?? [];
1061
+ let position = log.length - 1;
1062
+ while (position >= 0 && log[position].length === 0)
1063
+ position--;
1064
+ return this.eventResult(db, sk, position);
967
1065
  }
968
1066
  eventResult(db, sk, position) {
969
- const value = position === undefined ? undefined : this.events.get(sk)?.[position];
1067
+ const stored = position === undefined ? undefined : this.events.get(sk)?.[position];
1068
+ const value = stored?.length === 0 ? undefined : stored;
970
1069
  db.lastResult = value === undefined ? null : Buffer.from(value);
971
1070
  db.lastResultVersion =
972
1071
  value === undefined || position === undefined
@@ -974,6 +1073,79 @@ export class DevDatabase {
974
1073
  : (this.eventVersions.get(sk)?.[position] ?? 0);
975
1074
  return value === undefined ? ABSENT : value.length;
976
1075
  }
1076
+ eventsReference(ref, db, handle, kp, kl, ip, il) {
1077
+ const coll = collForOp(db, handle, DbOp.EventsReference, CollectionFamily.Events);
1078
+ if (typeof coll === 'number')
1079
+ return coll;
1080
+ if (il < 0 || il > MAX_KEY)
1081
+ throw new Error('Invalid event ID length');
1082
+ const sk = storeKey(coll.name, readKey(ref, kp, kl));
1083
+ const id = readCopy(ref, ip, il).toString('latin1');
1084
+ const position = this.eventPositions.get(sk)?.get(id);
1085
+ if (position === undefined)
1086
+ return this.eventDedup.get(sk)?.has(id) ? UNAVAILABLE : ABSENT;
1087
+ const bytes = Buffer.alloc(16);
1088
+ bytes.writeBigUInt64LE(BigInt(position + 1));
1089
+ bytes.writeBigUInt64LE(this.eventHlcs.get(sk)?.[position] ?? BigInt(position + 1), 8);
1090
+ db.lastResult = bytes;
1091
+ db.lastResultVersion = -1;
1092
+ return bytes.length;
1093
+ }
1094
+ eventsRedact(ref, db, handle, kp, kl, seq, hlc) {
1095
+ const coll = collForOp(db, handle, DbOp.EventsRedact, CollectionFamily.Events);
1096
+ if (typeof coll === 'number')
1097
+ return coll;
1098
+ if (seq <= 0n || hlc < 0n)
1099
+ throw new Error('Invalid event reference');
1100
+ const sk = storeKey(coll.name, readKey(ref, kp, kl));
1101
+ let erased = this.erasedEvents.get(sk);
1102
+ if (erased === undefined) {
1103
+ erased = new Set();
1104
+ this.erasedEvents.set(sk, erased);
1105
+ }
1106
+ erased.add(seq.toString() + ':' + hlc.toString());
1107
+ const position = Number(seq - 1n);
1108
+ const log = this.events.get(sk);
1109
+ if (Number.isSafeInteger(position) && position >= 0 && log !== undefined && position < log.length &&
1110
+ (this.eventHlcs.get(sk)?.[position] ?? seq) === hlc)
1111
+ log[position] = Buffer.alloc(0);
1112
+ for (const key of this.deriveCheckpoints.keys())
1113
+ if (key.endsWith('\0' + sk))
1114
+ this.deriveCheckpoints.delete(key);
1115
+ for (const key of db.pendingCheckpoints.keys())
1116
+ if (key.endsWith('\0' + sk))
1117
+ db.pendingCheckpoints.delete(key);
1118
+ this.recordWrite(db, coll);
1119
+ return 1;
1120
+ }
1121
+ eventsEntries(ref, db, handle, kp, kl, limit, after) {
1122
+ const coll = collForOp(db, handle, DbOp.EventsEntries, CollectionFamily.Events);
1123
+ if (typeof coll === 'number')
1124
+ return coll;
1125
+ if (!Number.isInteger(limit) || limit < 1 || limit > 1024 || after < 0n || after > BigInt(Number.MAX_SAFE_INTEGER))
1126
+ throw new Error('Invalid event page cursor or limit');
1127
+ const sk = storeKey(coll.name, readKey(ref, kp, kl));
1128
+ const log = this.events.get(sk) ?? [];
1129
+ const positions = [];
1130
+ let cursor = Number(after);
1131
+ while (cursor < log.length && positions.length < limit) {
1132
+ if (log[cursor].length > 0)
1133
+ positions.push(cursor);
1134
+ cursor++;
1135
+ }
1136
+ const writer = new DataWriter().writeU32(positions.length);
1137
+ for (const position of positions)
1138
+ writer.writeU64(BigInt(position + 1))
1139
+ .writeU64(this.eventHlcs.get(sk)?.[position] ?? BigInt(position + 1))
1140
+ .writeU32(this.eventVersions.get(sk)?.[position] ?? 0).writeBytes(log[position]);
1141
+ const hasMore = positions.length === limit;
1142
+ writer.writeU8(hasMore ? 1 : 0);
1143
+ if (hasMore)
1144
+ writer.writeU64(BigInt(cursor));
1145
+ db.lastResult = Buffer.from(writer.toBytes());
1146
+ db.lastResultVersion = -1;
1147
+ return db.lastResult.length;
1148
+ }
977
1149
  compareExchange(ref, db, handle, keyPtr, keyLen, expectedPtr, expectedLen, valuePtr, valueLen) {
978
1150
  const coll = collForOp(db, handle, DbOp.Patch, CollectionFamily.Record);
979
1151
  if (typeof coll === 'number')
@@ -997,6 +1169,26 @@ export class DevDatabase {
997
1169
  this.recordWrite(db, coll);
998
1170
  return 1;
999
1171
  }
1172
+ compareDelete(ref, db, handle, kp, kl, ep, el) {
1173
+ const coll = collForOp(db, handle, DbOp.Delete, CollectionFamily.Record);
1174
+ if (typeof coll === 'number')
1175
+ return coll;
1176
+ checkKeyValueSize(kl, el, 'expected value');
1177
+ checkValueSize(el, 'expected value');
1178
+ if (el < 0)
1179
+ throw new Error('data: conditional delete requires an expected value');
1180
+ const expected = readCopy(ref, ep, el);
1181
+ if (this.uniqueValuesOf(coll, expected).length > 0)
1182
+ throw new Error('Conditional delete does not support @unique fields');
1183
+ const sk = storeKey(coll.name, readKey(ref, kp, kl));
1184
+ const prior = this.store.get(sk);
1185
+ if (prior === undefined || !prior.equals(expected))
1186
+ return 0;
1187
+ this.store.delete(sk);
1188
+ this.versions.delete(sk);
1189
+ this.recordWrite(db, coll);
1190
+ return 1;
1191
+ }
1000
1192
  enqueue(ref, db, handle, keyPtr, keyLen, valPtr, valLen, idemPtr) {
1001
1193
  const coll = collForOp(db, handle, DbOp.Enqueue, CollectionFamily.Record);
1002
1194
  if (typeof coll === 'number')
@@ -1028,9 +1220,9 @@ export class DevDatabase {
1028
1220
  const log = this.events.get(sk) ?? [];
1029
1221
  const vers = this.eventVersions.get(sk) ?? [];
1030
1222
  const n = Math.max(0, Math.min(limit, 0xffff));
1031
- const start = Math.max(0, log.length - n);
1032
- const newest = log.slice(start).reverse();
1033
- const newestVers = vers.slice(start).reverse();
1223
+ const positions = n === 0 ? [] : log.map((_, i) => i).filter(i => log[i].length > 0).slice(-n).reverse();
1224
+ const newest = positions.map(i => log[i]);
1225
+ const newestVers = positions.map(i => vers[i]);
1034
1226
  const w = new DataWriter();
1035
1227
  w.writeU32(newest.length);
1036
1228
  for (let i = 0; i < newest.length; i++) {
@@ -1049,9 +1241,15 @@ export class DevDatabase {
1049
1241
  const vers = this.eventVersions.get(sk) ?? [];
1050
1242
  const cursor = db.pendingCheckpoints.get(ckKey) ?? this.deriveCheckpoints.get(ckKey) ?? 0;
1051
1243
  const n = Math.max(0, Math.min(limit, 0xffff));
1052
- const end = Math.min(log.length, cursor + n);
1053
- const batch = log.slice(cursor, end);
1054
- const batchVers = vers.slice(cursor, end);
1244
+ const positions = [];
1245
+ let end = cursor;
1246
+ while (end < log.length && positions.length < n) {
1247
+ if (log[end].length > 0)
1248
+ positions.push(end);
1249
+ end++;
1250
+ }
1251
+ const batch = positions.map(i => log[i]);
1252
+ const batchVers = positions.map(i => vers[i]);
1055
1253
  db.pendingCheckpoints.set(ckKey, end);
1056
1254
  const w = new DataWriter();
1057
1255
  w.writeU32(batch.length);
@@ -1204,8 +1402,8 @@ export class DevDatabase {
1204
1402
  }
1205
1403
  }
1206
1404
  export const devDb = new DevDatabase();
1207
- export function setDbCatalog(wasm) {
1208
- devDb.setCatalog(wasm);
1405
+ export function setDbCatalog(wasm, merge = false) {
1406
+ devDb.setCatalog(wasm, merge);
1209
1407
  }
1210
1408
  export function configureDbPersistence(filePath) {
1211
1409
  devDb.configurePersistence(filePath);
@@ -1217,7 +1415,7 @@ export function commitDeriveCheckpoints(db) {
1217
1415
  devDb.commitDeriveCheckpoints(db);
1218
1416
  }
1219
1417
  export function buildDatabaseImports(ref, db) {
1220
- return {
1418
+ const imports = {
1221
1419
  'data.resolve_collection': (namePtr, nameLen, outHandlePtr) => devDb.resolveCollection(ref, db, namePtr, nameLen, outHandlePtr),
1222
1420
  'data.get': (handle, keyPtr, keyLen) => devDb.get(ref, db, handle, keyPtr, keyLen),
1223
1421
  'data.get_many': (handle, keysPtr, keysLen) => devDb.getMany(ref, db, handle, keysPtr, keysLen),
@@ -1233,7 +1431,9 @@ export function buildDatabaseImports(ref, db) {
1233
1431
  'data.membership_contains': (handle, setPtr, setLen, memberPtr, memberLen) => devDb.membershipContains(ref, db, handle, setPtr, setLen, memberPtr, memberLen),
1234
1432
  'data.membership_add': (handle, setPtr, setLen, memberPtr, memberLen, _idemPtr) => devDb.membershipAdd(ref, db, handle, setPtr, setLen, memberPtr, memberLen),
1235
1433
  'data.membership_remove': (handle, setPtr, setLen, memberPtr, memberLen, _idemPtr) => devDb.membershipRemove(ref, db, handle, setPtr, setLen, memberPtr, memberLen),
1434
+ 'data.membership_page': (h, sp, sl, cp, cl, limit) => devDb.membershipPage(ref, db, h, sp, sl, cp, cl, limit),
1236
1435
  'data.membership_list': (handle, setPtr, setLen, limit) => devDb.membershipList(ref, db, handle, setPtr, setLen, limit),
1436
+ 'data.view_delete': (h, kp, kl, _idem) => devDb.viewDelete(ref, db, h, kp, kl),
1237
1437
  'data.view_get': (handle, keyPtr, keyLen) => devDb.viewGet(ref, db, handle, keyPtr, keyLen),
1238
1438
  'data.view_publish': (handle, keyPtr, keyLen, valPtr, valLen, _idemPtr) => devDb.viewPublish(ref, db, handle, keyPtr, keyLen, valPtr, valLen),
1239
1439
  'data.counter_get': (handle, keyPtr, keyLen) => devDb.counterGet(ref, db, handle, keyPtr, keyLen),
@@ -1241,9 +1441,13 @@ export function buildDatabaseImports(ref, db) {
1241
1441
  'data.append': (handle, keyPtr, keyLen, evPtr, evLen, idemPtr) => devDb.append(ref, db, handle, keyPtr, keyLen, evPtr, evLen, idemPtr),
1242
1442
  'data.append_once': (handle, keyPtr, keyLen, evidPtr, evidLen, evPtr, evLen) => devDb.appendOnce(ref, db, handle, keyPtr, keyLen, evidPtr, evidLen, evPtr, evLen),
1243
1443
  'data.get_current': (handle, kp, kl) => devDb.get(ref, db, handle, kp, kl),
1444
+ 'data.compare_delete': (h, kp, kl, ep, el) => devDb.compareDelete(ref, db, h, kp, kl, ep, el),
1244
1445
  'data.compare_exchange': (handle, kp, kl, ep, el, vp, vl) => devDb.compareExchange(ref, db, handle, kp, kl, ep, el, vp, vl),
1245
1446
  'data.enqueue': (handle, keyPtr, keyLen, valPtr, valLen, idemPtr) => devDb.enqueue(ref, db, handle, keyPtr, keyLen, valPtr, valLen, idemPtr),
1246
1447
  'data.latest': (handle, keyPtr, keyLen, limit) => devDb.latest(ref, db, handle, keyPtr, keyLen, limit),
1448
+ 'data.events_reference': (h, kp, kl, ip, il) => devDb.eventsReference(ref, db, h, kp, kl, ip, il),
1449
+ 'data.events_redact': (h, kp, kl, seq, hlc) => devDb.eventsRedact(ref, db, h, kp, kl, BigInt(seq), BigInt(hlc)),
1450
+ 'data.events_entries': (h, kp, kl, limit, after) => devDb.eventsEntries(ref, db, h, kp, kl, limit, BigInt(after)),
1247
1451
  'data.events_get': (handle, keyPtr, keyLen, evidPtr, evidLen) => devDb.eventsGet(ref, db, handle, keyPtr, keyLen, evidPtr, evidLen),
1248
1452
  'data.events_last': (handle, keyPtr, keyLen) => devDb.eventsLast(ref, db, handle, keyPtr, keyLen),
1249
1453
  'data.events_since': (handle, keyPtr, keyLen, limit) => devDb.since(ref, db, handle, keyPtr, keyLen, limit),
@@ -1256,6 +1460,7 @@ export function buildDatabaseImports(ref, db) {
1256
1460
  'data.result_schema_version': () => devDb.resultSchemaVersion(db),
1257
1461
  'data.write_allowed': () => (kindAllows(db.functionKind, DbOp.Patch) ? 1 : 0),
1258
1462
  };
1463
+ return withDbLimits(imports, db);
1259
1464
  }
1260
1465
  export function __resetDbForTests() {
1261
1466
  devDb.resetForTests();
@@ -0,0 +1,6 @@
1
+ import { type DbDevState } from './types.js';
2
+ type Imports = Record<string, (...args: number[]) => number | bigint>;
3
+ export declare function resetDbLimits(db: DbDevState): void;
4
+ export declare function checkDbResultSize(db: DbDevState, bytes: number): void;
5
+ export declare function withDbLimits(imports: Imports, db: DbDevState): Imports;
6
+ export {};