toilscript 0.1.62 → 0.1.64

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/dist/web.js CHANGED
@@ -1,8 +1,8 @@
1
- var ASSEMBLYSCRIPT_VERSION = "0.1.62";
1
+ var ASSEMBLYSCRIPT_VERSION = "0.1.64";
2
2
  var ASSEMBLYSCRIPT_IMPORTMAP = {
3
3
  "imports": {
4
- "toilscript": "https://cdn.jsdelivr.net/npm/toilscript@0.1.62/dist/toilscript.js",
5
- "toilscript/cli": "https://cdn.jsdelivr.net/npm/toilscript@0.1.62/dist/cli.js",
4
+ "toilscript": "https://cdn.jsdelivr.net/npm/toilscript@0.1.64/dist/toilscript.js",
5
+ "toilscript/cli": "https://cdn.jsdelivr.net/npm/toilscript@0.1.64/dist/cli.js",
6
6
  "binaryen": "https://cdn.jsdelivr.net/npm/binaryen@130.0.0/index.js",
7
7
  "long": "https://cdn.jsdelivr.net/npm/long@5.3.2/index.js"
8
8
  }
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "toilscript",
9
9
  "wasm"
10
10
  ],
11
- "version": "0.1.62",
11
+ "version": "0.1.64",
12
12
  "author": "Daniel Wirtz <dcode+assemblyscript@dcode.io>",
13
13
  "license": "Apache-2.0",
14
14
  "homepage": "https://github.com/dacely-cloud/toilscript",
@@ -72,6 +72,7 @@
72
72
  "toilinit": "./bin/toilinit.js"
73
73
  },
74
74
  "scripts": {
75
+ "prepare": "node scripts/build",
75
76
  "check": "npm run check:config && npm run check:import && npm run lint",
76
77
  "check:config": "tsc --noEmit -p src --diagnostics --listFiles",
77
78
  "check:import": "tsc --noEmit --skipLibCheck --target ESNEXT --module nodenext --moduleResolution nodenext --experimentalDecorators tests/import/index",
@@ -80,7 +81,7 @@
80
81
  "prepublishOnly": "node scripts/build",
81
82
  "watch": "node scripts/build --watch",
82
83
  "coverage": "npx c8 -- npm test",
83
- "test": "npm run test:parser && npm run test:compiler -- --parallel && npm run test:browser && npm run test:toilconfig && npm run test:transform && npm run test:cli && npm run test:json && npm run test:data && npm run test:dbresolve && npm run test:dbstatic && npm run test:routekinds && npm run test:derive && npm run test:streams && npm run test:daemon && npm run test:dberror",
84
+ "test": "npm run test:parser && npm run test:compiler -- --parallel && npm run test:browser && npm run test:toilconfig && npm run test:transform && npm run test:cli && npm run test:json && npm run test:data && npm run test:dbresolve && npm run test:dbstatic && npm run test:dbtypes && npm run test:routekinds && npm run test:derive && npm run test:streams && npm run test:daemon && npm run test:dberror && npm run test:events && npm run test:uniquebatch && npm run test:membership && npm run test:event-redaction",
84
85
  "test:parser": "node --enable-source-maps tests/parser",
85
86
  "test:compiler": "node --enable-source-maps --no-warnings tests/compiler",
86
87
  "test:browser": "node --enable-source-maps tests/browser",
@@ -93,10 +94,15 @@
93
94
  "test:data": "node tests/data/run.mjs",
94
95
  "test:dbresolve": "node tests/dbresolve/run.mjs",
95
96
  "test:dbstatic": "node tests/dbstatic/run.mjs",
97
+ "test:dbtypes": "node tests/dbtypes/run.mjs",
96
98
  "test:routekinds": "node tests/routekinds/run.mjs",
97
99
  "test:derive": "node tests/derive/run.mjs",
98
100
  "test:streams": "node tests/streams/run.mjs && node tests/streams/catalog.mjs && node tests/streams/codegen.mjs",
101
+ "test:membership": "node tests/membership/run.mjs",
102
+ "test:event-redaction": "node tests/event-redaction/run.mjs",
99
103
  "test:daemon": "node tests/daemon/run.mjs",
104
+ "test:uniquebatch": "node tests/uniquebatch/run.mjs",
105
+ "test:events": "node tests/events/run.mjs",
100
106
  "test:dberror": "node tests/dberror/run.mjs",
101
107
  "asbuild": "npm run asbuild:debug && npm run asbuild:release",
102
108
  "asbuild:debug": "node bin/toilscript --config src/toilconfig.json --target debug",
@@ -71,6 +71,24 @@ export namespace toildbHost {
71
71
  idemPtr: usize
72
72
  ): i32;
73
73
 
74
+ // Caller-observed byte CAS: expectedLen=-1 expects absence. 1 applied, 0 mismatch.
75
+ // @ts-ignore: decorator
76
+ @external("env", "data.compare_exchange")
77
+ export declare function compareExchange(
78
+ handle: u32, keyPtr: usize, keyLen: i32,
79
+ expectedPtr: usize, expectedLen: i32, valPtr: usize, valLen: i32
80
+ ): i32;
81
+
82
+ // @ts-ignore: decorator
83
+ @external("env", "data.compare_delete")
84
+ export declare function compareDelete(
85
+ handle: u32, keyPtr: usize, keyLen: i32, expectedPtr: usize, expectedLen: i32
86
+ ): i32;
87
+
88
+ // @ts-ignore: decorator
89
+ @external("env", "data.get_current")
90
+ export declare function getCurrent(handle: u32, keyPtr: usize, keyLen: i32): i32;
91
+
74
92
  // record.upsert -> 0 created | 1 updated | 2 @unique-conflict (not written) |
75
93
  // negative error. A single blind last-writer-wins put: creates the row if
76
94
  // absent, overwrites it if present, in ONE op (no create-then-patch round trip).
@@ -141,6 +159,11 @@ export namespace toildbHost {
141
159
  idemPtr: usize
142
160
  ): i32;
143
161
 
162
+ // Remove a view key (Action/Derive/Job), idempotently.
163
+ // @ts-ignore: decorator
164
+ @external("env", "data.view_delete")
165
+ export declare function viewDelete(handle: u32, keyPtr: usize, keyLen: i32, idemPtr: usize): i32;
166
+
144
167
  // membership.contains -> 1 present | 0 absent | negative error.
145
168
  // @ts-ignore: decorator
146
169
  @external("env", "data.membership_contains")
@@ -168,6 +191,13 @@ export namespace toildbHost {
168
191
  @external("env", "data.membership_list")
169
192
  export declare function membershipList(handle: u32, setPtr: usize, setLen: i32, limit: i32): i32;
170
193
 
194
+ // Ordered membership page: list frame + optional length-prefixed raw cursor.
195
+ // @ts-ignore: decorator
196
+ @external("env", "data.membership_page")
197
+ export declare function membershipPage(
198
+ handle: u32, setPtr: usize, setLen: i32, cursorPtr: usize, cursorLen: i32, limit: i32
199
+ ): i32;
200
+
171
201
  // capacity.set_total(total: i64) -> 0 ok | negative error (job/derive only).
172
202
  // @ts-ignore: decorator
173
203
  @external("env", "data.capacity_set_total")
@@ -230,6 +260,26 @@ export namespace toildbHost {
230
260
  idemPtr: usize
231
261
  ): i32;
232
262
 
263
+ // Point reads: stashed value length | -2 absent | typed negative error.
264
+ // result_schema_version exposes the stored event's schema version.
265
+ // @ts-ignore: decorator
266
+ @external("env", "data.events_reference")
267
+ export declare function eventsReference(handle: u32, keyPtr: usize, keyLen: i32, idPtr: usize, idLen: i32): i32;
268
+ // @ts-ignore: decorator
269
+ @external("env", "data.events_redact")
270
+ export declare function eventsRedact(handle: u32, keyPtr: usize, keyLen: i32, sequence: u64, hlc: u64): i32;
271
+ // @ts-ignore: decorator
272
+ @external("env", "data.events_entries")
273
+ export declare function eventsEntries(handle: u32, keyPtr: usize, keyLen: i32, limit: i32, after: u64): i32;
274
+
275
+ // @ts-ignore: decorator
276
+ @external("env", "data.events_get")
277
+ export declare function eventsGet(handle: u32, keyPtr: usize, keyLen: i32, evidPtr: usize, evidLen: i32): i32;
278
+
279
+ // @ts-ignore: decorator
280
+ @external("env", "data.events_last")
281
+ export declare function eventsLast(handle: u32, keyPtr: usize, keyLen: i32): i32;
282
+
233
283
  // events.latest(limit) -> framed-list length (stashed) | negative error.
234
284
  // The blob is `u32 count` then per event `u32 schema_version + u32 len + bytes`,
235
285
  // newest first.
@@ -0,0 +1,14 @@
1
+ // Structured server logging, available to request, stream, and daemon code.
2
+ @external("env", "server_log")
3
+ declare function __serverLog(level: i32, ptr: usize, len: i32): i32;
4
+
5
+ @global
6
+ export class Log {
7
+ private static write(level: i32, message: string): bool {
8
+ const bytes = Uint8Array.wrap(String.UTF8.encode(message));
9
+ return __serverLog(level, bytes.dataStart, bytes.byteLength) == 1;
10
+ }
11
+ static info(message: string): bool { return Log.write(0, message); }
12
+ static warn(message: string): bool { return Log.write(1, message); }
13
+ static error(message: string): bool { return Log.write(2, message); }
14
+ }
@@ -186,7 +186,8 @@ function __toildbMiss(status: i32): void {
186
186
  /// }
187
187
  /// ```
188
188
  ///
189
- /// ONLY `Documents.get`, `Documents.getDelete`, `View.get` and `Unique.lookup`
189
+ /// `Documents.get`, `Documents.getDelete`, `View.get`, `Unique.lookup`,
190
+ /// `Events.get` and `Events.last`
190
191
  /// record into this. Every other ToilDB op either succeeds or traps. The `Analytics`
191
192
  /// reads also answer `null`, but on their own `-2 absent` / `-3 forbidden` statuses
192
193
  /// rather than a `TDLnnn`, so they leave `lastError()` untouched: do not consult it
@@ -232,6 +233,14 @@ export enum UpsertResult {
232
233
  Conflict = 2,
233
234
  }
234
235
 
236
+ /// An observed value plus its original storage bytes (before decoding/migration).
237
+ /// Keep the snapshot unchanged while constructing a replacement value.
238
+ @global
239
+ export class DocumentSnapshot<V> {
240
+ constructor(public value: V, public encoded: Uint8Array) {}
241
+ }
242
+
243
+ @global
235
244
  export class Documents<K, V> {
236
245
  private __handle: u32;
237
246
 
@@ -258,6 +267,47 @@ export class Documents<K, V> {
258
267
  return v;
259
268
  }
260
269
 
270
+ /// Read without rewrite-on-read. Retains the exact bytes for conditional writes,
271
+ /// including records whose codec or schema migrated during decoding.
272
+ getSnapshot(key: K): DocumentSnapshot<V> | null {
273
+ const kb = key.encode();
274
+ const status = toildbHost.get(this.__handle, kb.dataStart, kb.byteLength);
275
+ if (status < 0) { __toildbMiss(status); return null; }
276
+ const bytes = __toildbTake(status);
277
+ const value = instantiate<V>();
278
+ value.decodeInto(bytes);
279
+ return new DocumentSnapshot<V>(value, bytes);
280
+ }
281
+
282
+ /// Owner-authoritative snapshot, bypassing replica and request caches.
283
+ getCurrentSnapshot(key: K): DocumentSnapshot<V> | null {
284
+ const kb = key.encode();
285
+ const status = toildbHost.getCurrent(this.__handle, kb.dataStart, kb.byteLength);
286
+ if (status < 0) { __toildbMiss(status); return null; }
287
+ const bytes = __toildbTake(status);
288
+ const value = instantiate<V>();
289
+ value.decodeInto(bytes);
290
+ return new DocumentSnapshot<V>(value, bytes);
291
+ }
292
+
293
+ compareExchangeSnapshot(key: K, expected: DocumentSnapshot<V> | null, value: V): bool {
294
+ const kb = key.encode();
295
+ const vb = value.encode();
296
+ const eb = expected == null ? new Uint8Array(0) : expected.encoded;
297
+ const status = toildbHost.compareExchange(this.__handle, kb.dataStart, kb.byteLength,
298
+ eb.dataStart, expected == null ? -1 : eb.byteLength, vb.dataStart, vb.byteLength);
299
+ if (status < 0) abort("ToilDB compareExchange failed: status " + status.toString(), "toildb", 0, 0);
300
+ return status == 1;
301
+ }
302
+
303
+ compareDeleteSnapshot(key: K, expected: DocumentSnapshot<V>): bool {
304
+ const kb = key.encode();
305
+ const eb = expected.encoded;
306
+ const status = toildbHost.compareDelete(this.__handle, kb.dataStart, kb.byteLength, eb.dataStart, eb.byteLength);
307
+ if (status < 0) abort("ToilDB compareDelete failed: status " + status.toString(), "toildb", 0, 0);
308
+ return status == 1;
309
+ }
310
+
261
311
  /// Like `get`, but traps if the record is absent.
262
312
  require(key: K): V {
263
313
  const v = this.get(key);
@@ -275,7 +325,13 @@ export class Documents<K, V> {
275
325
  }
276
326
  const blob = w.toBytes();
277
327
  const status = toildbHost.getMany(this.__handle, blob.dataStart, blob.byteLength);
278
- if (status < 0) unreachable();
328
+ if (status < 0) {
329
+ __toildbMiss(status);
330
+ abort("ToilDB getMany failed: status " + status.toString() +
331
+ "; DbError=" + (<i32>Db.lastError()).toString() +
332
+ "; keys=" + keys.length.toString() +
333
+ ". Query accepts 32 keys, Action 64, background 1024; split larger batches.", "toildb", 0, 0);
334
+ }
279
335
  const out = __toildbTake(status);
280
336
  const results = new Array<V | null>();
281
337
  let off: i32 = 0;
@@ -325,8 +381,9 @@ export class Documents<K, V> {
325
381
  /// conflict or retry - the later one wins and overwrites the earlier. Correct
326
382
  /// for writing a whole value (a profile, a settings blob); wrong for a
327
383
  /// read-modify-write of accumulating state (a balance, a like count), where a
328
- /// concurrent write would be lost - use `counter.add` or version-checked
329
- /// `patch` there. A lost write is a data-integrity concern, not a boundary one:
384
+ /// concurrent write would be lost - use `counter.add` or a caller-observed
385
+ /// conditional write there. A lost write is a data-integrity concern, not a
386
+ /// boundary one:
330
387
  /// the key is tenant-scoped, so a race is only ever one tenant's own concurrent
331
388
  /// writes, and `@unique` stays race-safe (claim-once at the value's home).
332
389
  upsert(key: K, value: V): UpsertResult {
@@ -352,19 +409,40 @@ export class Documents<K, V> {
352
409
  return v;
353
410
  }
354
411
 
355
- /// Atomically replace an EXISTING record's value, version-checked: returns true
356
- /// if applied, false if a concurrent write changed the record first (optimistic
357
- /// concurrency - re-read and retry) or the record is absent.
412
+ /// Replace only if the stored bytes equal `expected`; null expects absence.
413
+ /// Returns false on a mismatch. Routed to the owner; @unique values are rejected.
414
+ compareExchange(key: K, expected: V | null, value: V): bool {
415
+ const kb = key.encode();
416
+ const vb = value.encode();
417
+ const eb = expected == null ? new Uint8Array(0) : expected.encode();
418
+ const status = toildbHost.compareExchange(
419
+ this.__handle, kb.dataStart, kb.byteLength,
420
+ eb.dataStart, expected == null ? -1 : eb.byteLength,
421
+ vb.dataStart, vb.byteLength
422
+ );
423
+ if (status < 0) abort("ToilDB compareExchange failed: status " + status.toString(), "toildb", 0, 0);
424
+ return status == 1;
425
+ }
426
+
427
+ /// Submit a replacement of an EXISTING record: true when accepted, false on
428
+ /// absence or rejection. The ABI carries no caller-observed version or value,
429
+ /// so this cannot protect a read-modify-write based on an earlier application read.
358
430
  enqueue(key: K, value: V): bool {
359
431
  const kb = key.encode();
360
432
  const vb = value.encode();
361
433
  return toildbHost.enqueue(this.__handle, kb.dataStart, kb.byteLength, vb.dataStart, vb.byteLength, 0) == 0;
362
434
  }
363
435
 
436
+ /// Delete only when the stored bytes still equal the caller's expectation.
437
+ compareDelete(key: K, expected: V): bool {
438
+ return this.compareDeleteSnapshot(key, new DocumentSnapshot<V>(expected, expected.encode()));
439
+ }
440
+
364
441
  /// Delete the record (idempotent).
365
442
  delete(key: K): void {
366
443
  const kb = key.encode();
367
- toildbHost.del(this.__handle, kb.dataStart, kb.byteLength, 0);
444
+ const status = toildbHost.del(this.__handle, kb.dataStart, kb.byteLength, 0);
445
+ if (status < 0) abort("ToilDB delete failed: status " + status.toString(), "toildb", 0, 0);
368
446
  }
369
447
 
370
448
  /// Atomic fetch-and-delete (consume-once); returns the prior value or `null`.
@@ -407,14 +485,22 @@ export class View<K, V> {
407
485
  return v!;
408
486
  }
409
487
 
488
+ /// Remove a projection key. Delete its source personal data before rebuilding.
489
+ delete(key: K): void {
490
+ const kb = key.encode();
491
+ const status = toildbHost.viewDelete(this.__handle, kb.dataStart, kb.byteLength, 0);
492
+ if (status < 0) abort("ToilDB view.delete failed: status " + status.toString(), "toildb", 0, 0);
493
+ }
494
+
410
495
  /// Publish (overwrite) the view for `key`. Derive/job only; the host assigns
411
496
  /// the version so a later publish always supersedes an earlier one.
412
497
  publish(key: K, value: V): void {
413
498
  const kb = key.encode();
414
499
  const vb = value.encode();
415
- toildbHost.viewPublish(
500
+ const status = toildbHost.viewPublish(
416
501
  this.__handle, kb.dataStart, kb.byteLength, vb.dataStart, vb.byteLength, 0
417
502
  );
503
+ if (status < 0) abort("ToilDB view.publish failed: status " + status.toString(), "toildb", 0, 0);
418
504
  }
419
505
  }
420
506
 
@@ -446,6 +532,12 @@ export class Unique<K, V> {
446
532
  return v;
447
533
  }
448
534
 
535
+ /// Bounded owner lookup in input order, with null for each unclaimed key.
536
+ /// Uses one read operation; the host enforces the per-request batch limit.
537
+ lookupMany(keys: K[]): Array<V | null> {
538
+ return new Documents<K, V>(this.__handle).getMany(keys);
539
+ }
540
+
449
541
  /// Claim `key` for `value`. Returns whether the caller owns it, and (when
450
542
  /// another owns it) who.
451
543
  claim(key: K, value: V): ClaimResult<V> {
@@ -475,6 +567,17 @@ export class Unique<K, V> {
475
567
  }
476
568
  }
477
569
 
570
+ /// A page in encoded-member order; pass cursor to listPage to continue.
571
+ @global
572
+ export class MembershipPage<M> {
573
+ members: M[];
574
+ cursor: Uint8Array | null;
575
+ constructor(members: M[], cursor: Uint8Array | null) {
576
+ this.members = members;
577
+ this.cursor = cursor;
578
+ }
579
+ }
580
+
478
581
  /// An unordered set (spec 7.3): followers, tags, ACLs, room members. `M` is the
479
582
  /// `@data` member type, `K` the `@data` set-key type.
480
583
  @global
@@ -512,6 +615,36 @@ export class Membership<K, M> {
512
615
  );
513
616
  }
514
617
 
618
+ /// Read members strictly after an opaque cursor. A full page may require an
619
+ /// empty final page to establish the end. Pagination is not a snapshot.
620
+ listPage(key: K, limit: i32, cursor: Uint8Array | null = null): MembershipPage<M> {
621
+ const kb = key.encode();
622
+ const cb = cursor == null ? new Uint8Array(0) : cursor;
623
+ const status = toildbHost.membershipPage(
624
+ this.__handle, kb.dataStart, kb.byteLength, cb.dataStart,
625
+ cursor == null ? -1 : cb.byteLength, limit
626
+ );
627
+ if (status < 0) abort("ToilDB membership.listPage failed: status " + status.toString(), "toildb", 0, 0);
628
+ const blob = __toildbTake(status);
629
+ const out = new Array<M>();
630
+ let off: i32 = 4;
631
+ const count = load<u32>(blob.dataStart);
632
+ for (let i: u32 = 0; i < count; i++) {
633
+ const version = <i64>load<u32>(blob.dataStart + off); off += 4;
634
+ const len = <i32>load<u32>(blob.dataStart + off); off += 4;
635
+ const member = instantiate<M>();
636
+ member.decodeIntoVersioned(blob.subarray(off, off + len), version);
637
+ out.push(member); off += len;
638
+ }
639
+ const hasCursor = load<u8>(blob.dataStart + off); off++;
640
+ let next: Uint8Array | null = null;
641
+ if (hasCursor != 0) {
642
+ const len = <i32>load<u32>(blob.dataStart + off); off += 4;
643
+ next = blob.slice(off, off + len);
644
+ }
645
+ return new MembershipPage<M>(out, next);
646
+ }
647
+
515
648
  /// Up to `limit` members of the set. Decodes each framed member into an `M`.
516
649
  list(key: K, limit: i32): M[] {
517
650
  const kb = key.encode();
@@ -936,6 +1069,21 @@ export class Analytics {
936
1069
  /// An append-only event log (spec 7.5): activity feeds, audit trails, the
937
1070
  /// fact stream a `@derive` consumes. `V` is the `@data` event type, `K` the
938
1071
  /// `@data` stream-key type.
1072
+ @global
1073
+ export class EventRef {
1074
+ constructor(public sequence: u64 = 0, public hlc: u64 = 0) {}
1075
+ }
1076
+
1077
+ @global
1078
+ export class EventEntry<V> {
1079
+ constructor(public reference: EventRef, public value: V) {}
1080
+ }
1081
+
1082
+ @global
1083
+ export class EventPage<V> {
1084
+ constructor(public entries: Array<EventEntry<V>>, public cursor: u64, public hasMore: bool) {}
1085
+ }
1086
+
939
1087
  @global
940
1088
  export class Events<K, V> {
941
1089
  private __handle: u32;
@@ -944,6 +1092,70 @@ export class Events<K, V> {
944
1092
  this.__handle = handle;
945
1093
  }
946
1094
 
1095
+ /// Read an appendOnce reference, including during pending cleanup. No payload is returned.
1096
+ reference(key: K, eventId: string): EventRef | null {
1097
+ const kb = key.encode();
1098
+ const id = Uint8Array.wrap(String.UTF8.encode(eventId));
1099
+ const status = toildbHost.eventsReference(this.__handle, kb.dataStart, kb.byteLength, id.dataStart, id.byteLength);
1100
+ if (status < 0) { __toildbMiss(status); return null; }
1101
+ const reader = new DataReader(__toildbTake(status));
1102
+ const reference = new EventRef(reader.readU64(), reader.readU64());
1103
+ if (!reader.ok) abort("Invalid event reference", "toildb", 0, 0);
1104
+ return reference;
1105
+ }
1106
+
1107
+ /// Erase this entry and one bounded page of recovery copies. Repeat in later
1108
+ /// background calls until true. The payload is hidden from the first successful call.
1109
+ redact(key: K, reference: EventRef): bool {
1110
+ const kb = key.encode();
1111
+ const status = toildbHost.eventsRedact(this.__handle, kb.dataStart, kb.byteLength, reference.sequence, reference.hlc);
1112
+ if (status < 0) abort("ToilDB events.redact failed: status " + status.toString(), "toildb", 0, 0);
1113
+ return status == 1;
1114
+ }
1115
+
1116
+ /// Bounded, oldest-first page with references for ordinary and appendOnce events.
1117
+ /// Keep the opaque cursor and use hasMore even if an erased page is empty.
1118
+ entries(key: K, limit: i32 = 128, after: u64 = 0): EventPage<V> {
1119
+ const kb = key.encode();
1120
+ const status = toildbHost.eventsEntries(this.__handle, kb.dataStart, kb.byteLength, limit, after);
1121
+ if (status < 0) abort("ToilDB events.entries failed: status " + status.toString(), "toildb", 0, 0);
1122
+ const reader = new DataReader(__toildbTake(status));
1123
+ const count = reader.readU32();
1124
+ const entries = new Array<EventEntry<V>>();
1125
+ for (let i: u32 = 0; i < count; i++) {
1126
+ const reference = new EventRef(reader.readU64(), reader.readU64());
1127
+ const version = <i64>reader.readU32();
1128
+ const bytes = reader.readBytes();
1129
+ const value = instantiate<V>(); value.decodeIntoVersioned(bytes, version);
1130
+ entries.push(new EventEntry<V>(reference, value));
1131
+ }
1132
+ const hasMore = reader.readU8() != 0;
1133
+ const cursor: u64 = hasMore ? reader.readU64() : 0;
1134
+ if (!reader.ok) abort("Invalid event page", "toildb", 0, 0);
1135
+ return new EventPage<V>(entries, cursor, hasMore);
1136
+ }
1137
+
1138
+ /// Read an event by its appendOnce eventId, or null if absent. Bounded request-path read.
1139
+ get(key: K, eventId: string): V | null {
1140
+ const kb = key.encode();
1141
+ const idb = Uint8Array.wrap(String.UTF8.encode(eventId));
1142
+ const status = toildbHost.eventsGet(this.__handle, kb.dataStart, kb.byteLength, idb.dataStart, idb.byteLength);
1143
+ if (status < 0) { __toildbMiss(status); return null; }
1144
+ const v = instantiate<V>();
1145
+ v.decodeInto(__toildbTake(status));
1146
+ return v;
1147
+ }
1148
+
1149
+ /// Read the newest appended event, or null for an empty stream. Does not scan.
1150
+ last(key: K): V | null {
1151
+ const kb = key.encode();
1152
+ const status = toildbHost.eventsLast(this.__handle, kb.dataStart, kb.byteLength);
1153
+ if (status < 0) { __toildbMiss(status); return null; }
1154
+ const v = instantiate<V>();
1155
+ v.decodeInto(__toildbTake(status));
1156
+ return v;
1157
+ }
1158
+
947
1159
  /// Append an event to the stream.
948
1160
  append(key: K, event: V): void {
949
1161
  const kb = key.encode();
@@ -240,6 +240,12 @@ declare const enum UpsertResult {
240
240
  }
241
241
 
242
242
  /** A mutable keyed-entity collection (spec 7.1): user profiles, items, sessions. */
243
+ declare class DocumentSnapshot<V> {
244
+ value: V;
245
+ encoded: Uint8Array;
246
+ constructor(value: V, encoded: Uint8Array);
247
+ }
248
+
243
249
  declare class Documents<K, V> {
244
250
  get(key: K): V | null;
245
251
  require(key: K): V;
@@ -247,13 +253,24 @@ declare class Documents<K, V> {
247
253
  getMany(keys: K[]): (V | null)[];
248
254
  create(key: K, value: V): bool;
249
255
  patch(key: K, value: V): V;
256
+ /** Replace if stored bytes equal expected; null expects absence. Owner-cell only,
257
+ * rejects @unique values. False on mismatch; other failures abort. */
258
+ compareExchange(key: K, expected: V | null, value: V): bool;
259
+ compareDelete(key: K, expected: V): bool;
260
+ getSnapshot(key: K): DocumentSnapshot<V> | null;
261
+ getCurrentSnapshot(key: K): DocumentSnapshot<V> | null;
262
+ compareExchangeSnapshot(key: K, expected: DocumentSnapshot<V> | null, value: V): bool;
263
+ compareDeleteSnapshot(key: K, expected: DocumentSnapshot<V>): bool;
250
264
  /** Create-or-overwrite in one op (blind last-writer-wins); replaces the two-op
251
265
  * `if (!create(k,v)) patch(k,v)` idiom. Right for writing a whole value; wrong
252
266
  * for read-modify-write of accumulating state (a balance/count), where a
253
- * concurrent write would be lost - use `counter.add` or version-checked
254
- * `patch` there. The key is tenant-scoped, so a race is only ever the same
255
- * tenant's own writes. See {@link UpsertResult}. */
267
+ * concurrent write would be lost - use `counter.add` or a caller-observed
268
+ * conditional write there. The key is tenant-scoped, so a race is only ever
269
+ * the same tenant's own writes. See {@link UpsertResult}. */
256
270
  upsert(key: K, value: V): UpsertResult;
271
+ /** Submit a replacement of an existing record; true when accepted, false on
272
+ * absence or rejection. Does not compare against an earlier application read. */
273
+ enqueue(key: K, value: V): bool;
257
274
  delete(key: K): void;
258
275
  getDelete(key: K): V | null;
259
276
  }
@@ -262,6 +279,7 @@ declare class Documents<K, V> {
262
279
  declare class View<K, V> {
263
280
  get(key: K): V | null;
264
281
  require(key: K): V;
282
+ delete(key: K): void;
265
283
  publish(key: K, value: V): void;
266
284
  }
267
285
 
@@ -275,16 +293,26 @@ declare class ClaimResult<V> {
275
293
  /** A globally-unique claim collection (spec 7.6): usernames, emails, slugs. */
276
294
  declare class Unique<K, V> {
277
295
  lookup(key: K): V | null;
296
+ /** Bounded owner lookup in input order; null for unclaimed keys. */
297
+ lookupMany(keys: K[]): Array<V | null>;
278
298
  claim(key: K, value: V): ClaimResult<V>;
279
299
  release(key: K, value: V): void;
280
300
  }
281
301
 
302
+ /** A page in encoded-member byte order, with an opaque continuation cursor. */
303
+ declare class MembershipPage<M> {
304
+ members: M[];
305
+ cursor: Uint8Array | null;
306
+ constructor(members: M[], cursor: Uint8Array | null);
307
+ }
308
+
282
309
  /** An unordered set (spec 7.3): followers, tags, ACLs, room members. */
283
310
  declare class Membership<K, M> {
284
311
  contains(key: K, member: M): bool;
285
312
  add(key: K, member: M): void;
286
313
  remove(key: K, member: M): void;
287
314
  list(key: K, limit: i32): M[];
315
+ listPage(key: K, limit: i32, cursor?: Uint8Array | null): MembershipPage<M>;
288
316
  }
289
317
 
290
318
  /** A finite, strongly-consistent resource via escrow (spec 7.7): stock, seats. */
@@ -303,7 +331,22 @@ declare class Counter<K> {
303
331
  }
304
332
 
305
333
  /** An append-only event log (spec 7.5): activity feeds, audit trails. */
334
+ declare class EventRef {
335
+ sequence: u64;
336
+ hlc: u64;
337
+ constructor(sequence?: u64, hlc?: u64);
338
+ }
339
+ declare class EventEntry<V> { reference: EventRef; value: V; constructor(reference: EventRef, value: V); }
340
+ declare class EventPage<V> { entries: EventEntry<V>[]; cursor: u64; hasMore: bool; constructor(entries: EventEntry<V>[], cursor: u64, hasMore: bool); }
306
341
  declare class Events<K, V> {
342
+ reference(key: K, eventId: string): EventRef | null;
343
+ redact(key: K, reference: EventRef): bool;
344
+ entries(key: K, limit?: i32, after?: u64): EventPage<V>;
345
+ /** Read an event by its appendOnce eventId, or null if absent. */
346
+ get(key: K, eventId: string): V | null;
347
+ /** Read the newest appended event, or null for an empty stream. */
348
+ last(key: K): V | null;
349
+ appendOnce(key: K, eventId: string, event: V): bool;
307
350
  append(key: K, event: V): void;
308
351
  latest(key: K, limit: i32): V[];
309
352
  since(key: K, limit: i32): V[];
@@ -862,3 +905,11 @@ declare class JSON {
862
905
  /** Serialize this value tree to a JSON string. */
863
906
  toString(): string;
864
907
  }
908
+
909
+ /** Server logging. At most 4096 UTF-8 bytes/message, 32 messages per call.
910
+ * Returns false when the message allowance is exhausted. */
911
+ declare class Log {
912
+ static info(message: string): bool;
913
+ static warn(message: string): bool;
914
+ static error(message: string): bool;
915
+ }