toiljs 0.0.60 → 0.0.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/.github/workflows/ci.yml +31 -0
  2. package/CHANGELOG.md +5 -0
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +2 -2
  5. package/build/client/.tsbuildinfo +1 -1
  6. package/build/client/index.d.ts +1 -1
  7. package/build/client/index.js +1 -1
  8. package/build/client/routing/mount.js +12 -1
  9. package/build/client/ssr/markers.d.ts +1 -0
  10. package/build/client/ssr/markers.js +3 -0
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/config.d.ts +21 -0
  13. package/build/compiler/config.js +35 -0
  14. package/build/compiler/docs.d.ts +2 -1
  15. package/build/compiler/docs.js +33 -304
  16. package/build/compiler/index.d.ts +13 -0
  17. package/build/compiler/index.js +113 -21
  18. package/build/compiler/template-build.d.ts +21 -1
  19. package/build/compiler/template-build.js +110 -26
  20. package/build/compiler/toil-docs.generated.d.ts +1 -0
  21. package/build/compiler/toil-docs.generated.js +20 -0
  22. package/build/devserver/.tsbuildinfo +1 -1
  23. package/build/devserver/daemon/catalog.d.ts +26 -0
  24. package/build/devserver/daemon/catalog.js +48 -0
  25. package/build/devserver/daemon/cron.d.ts +4 -0
  26. package/build/devserver/daemon/cron.js +50 -0
  27. package/build/devserver/daemon/host.d.ts +37 -0
  28. package/build/devserver/daemon/host.js +94 -0
  29. package/build/devserver/daemon/index.d.ts +34 -0
  30. package/build/devserver/daemon/index.js +241 -0
  31. package/build/devserver/db/catalog.d.ts +2 -1
  32. package/build/devserver/db/catalog.js +44 -44
  33. package/build/devserver/db/database.d.ts +27 -11
  34. package/build/devserver/db/database.js +539 -169
  35. package/build/devserver/db/index.d.ts +1 -1
  36. package/build/devserver/db/index.js +1 -1
  37. package/build/devserver/db/routeKinds.d.ts +8 -0
  38. package/build/devserver/db/routeKinds.js +139 -0
  39. package/build/devserver/db/types.d.ts +64 -1
  40. package/build/devserver/db/types.js +33 -1
  41. package/build/devserver/index.d.ts +10 -0
  42. package/build/devserver/index.js +7 -0
  43. package/build/devserver/mstore/store.d.ts +18 -0
  44. package/build/devserver/mstore/store.js +82 -0
  45. package/build/devserver/runtime/host.d.ts +6 -0
  46. package/build/devserver/runtime/host.js +45 -1
  47. package/build/devserver/runtime/module.d.ts +1 -0
  48. package/build/devserver/runtime/module.js +27 -1
  49. package/build/devserver/server.d.ts +6 -0
  50. package/build/devserver/server.js +59 -0
  51. package/build/devserver/ssr.d.ts +25 -0
  52. package/build/devserver/ssr.js +114 -0
  53. package/build/devserver/wasm/sections.d.ts +2 -0
  54. package/build/devserver/wasm/sections.js +42 -0
  55. package/build/devserver/wasm/surface.d.ts +18 -0
  56. package/build/devserver/wasm/surface.js +41 -0
  57. package/docs/README.md +4 -4
  58. package/docs/auth-todo.md +6 -6
  59. package/docs/caching.md +5 -5
  60. package/docs/cli.md +15 -0
  61. package/docs/client.md +40 -0
  62. package/docs/crypto.md +4 -4
  63. package/docs/data.md +6 -6
  64. package/docs/email.md +28 -28
  65. package/docs/environment.md +10 -10
  66. package/docs/index.md +26 -0
  67. package/docs/ratelimit.md +10 -10
  68. package/docs/routing.md +2 -2
  69. package/docs/server.md +61 -0
  70. package/docs/ssr.md +561 -113
  71. package/docs/styling.md +22 -0
  72. package/docs/time.md +1 -1
  73. package/eslint.config.js +10 -1
  74. package/examples/basic/client/components/Header.tsx +3 -0
  75. package/examples/basic/client/routes/features/actions.tsx +0 -2
  76. package/examples/basic/client/routes/hello.tsx +89 -19
  77. package/examples/basic/client/styles/main.css +48 -0
  78. package/examples/basic/server/SsrHelloRender.ts +97 -0
  79. package/examples/basic/server/main.ts +5 -0
  80. package/examples/basic/server/streams/Echo.ts +49 -0
  81. package/package.json +12 -10
  82. package/scripts/gen-toil-docs.mjs +96 -0
  83. package/src/cli/create.ts +2 -2
  84. package/src/client/index.ts +1 -1
  85. package/src/client/routing/mount.tsx +18 -2
  86. package/src/client/ssr/markers.tsx +22 -0
  87. package/src/compiler/config.ts +88 -2
  88. package/src/compiler/docs.ts +47 -308
  89. package/src/compiler/index.ts +236 -32
  90. package/src/compiler/ssr-codegen.ts +1 -1
  91. package/src/compiler/template-build.ts +247 -46
  92. package/src/compiler/toil-docs.generated.ts +26 -0
  93. package/src/devserver/daemon/catalog.ts +120 -0
  94. package/src/devserver/daemon/cron.ts +87 -0
  95. package/src/devserver/daemon/host.ts +224 -0
  96. package/src/devserver/daemon/index.ts +349 -0
  97. package/src/devserver/db/catalog.ts +61 -53
  98. package/src/devserver/db/database.ts +613 -149
  99. package/src/devserver/db/index.ts +1 -1
  100. package/src/devserver/db/routeKinds.ts +147 -0
  101. package/src/devserver/db/types.ts +65 -2
  102. package/src/devserver/index.ts +12 -0
  103. package/src/devserver/mstore/store.ts +121 -0
  104. package/src/devserver/runtime/host.ts +92 -1
  105. package/src/devserver/runtime/module.ts +35 -1
  106. package/src/devserver/server.ts +101 -0
  107. package/src/devserver/ssr.ts +166 -0
  108. package/src/devserver/wasm/sections.ts +59 -0
  109. package/src/devserver/wasm/surface.ts +88 -0
  110. package/test/daemon-build.test.ts +198 -0
  111. package/test/daemon-catalog.test.ts +265 -0
  112. package/test/daemon-emulation.test.ts +216 -0
  113. package/test/devserver-database.test.ts +396 -5
  114. package/test/email-preview.test.ts +6 -1
  115. package/test/fixtures/daemon-app.ts +56 -0
  116. package/test/global-setup.ts +17 -0
  117. package/test/ssr-render.test.ts +94 -27
  118. package/test/ssr-template.test.tsx +44 -1
  119. package/vitest.config.ts +3 -0
@@ -1,8 +1,9 @@
1
1
  import fs from 'node:fs';
2
+ import { createHash } from 'node:crypto';
2
3
  import path from 'node:path';
3
4
  import { DataReader, DataWriter } from 'toiljs/io';
4
5
  import { parseCatalog } from './catalog.js';
5
- import { ABSENT, ALREADY_EXISTS, CODEC_ERR, CONFLICT, INVALID_HANDLE, MAX_KEY, MAX_NAME, MAX_RESERVATION_TTL_MS, MAX_RESERVATIONS, MAX_VALUE, satI64, TOO_MANY_KEYS, TOO_SMALL, } from './types.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
7
  function mem(ref) {
7
8
  if (!ref.memory)
8
9
  throw new Error('data host import called before memory was bound');
@@ -22,27 +23,221 @@ function readKey(ref, ptr, len) {
22
23
  function storeKey(collection, key) {
23
24
  return collection + '\0' + key.toString('latin1');
24
25
  }
26
+ function readIdem(ref, ptr) {
27
+ if (ptr === 0)
28
+ return null;
29
+ return readCopy(ref, ptr, 16);
30
+ }
31
+ function u64le(n) {
32
+ const b = Buffer.allocUnsafe(8);
33
+ b.writeBigUInt64LE(BigInt(n));
34
+ return b;
35
+ }
36
+ const RECORD_OP_CREATE = 4;
37
+ const RECORD_OP_PATCH = 5;
38
+ const RECORD_OP_DELETE = 6;
39
+ const RECORD_OP_GET_DELETE = 7;
40
+ const RECORD_OP_ENQUEUE = 8;
41
+ function recordRequestHash(op, key, value) {
42
+ return createHash('sha256')
43
+ .update('toildb/record-idempotency/request/v1')
44
+ .update(Buffer.from([op]))
45
+ .update(u64le(key.length))
46
+ .update(key)
47
+ .update(u64le(value.length))
48
+ .update(value)
49
+ .digest('hex');
50
+ }
51
+ function idemKey(coll, key, op, idem) {
52
+ return `${storeKey(coll.name, key)}\0${op}\0${idem.toString('hex')}`;
53
+ }
54
+ function reservationIdFromIdem(coll, key, idem) {
55
+ const digest = createHash('sha256')
56
+ .update('toildb/capacity-reservation-id/v1')
57
+ .update(coll.name)
58
+ .update('\0')
59
+ .update(key)
60
+ .update(idem)
61
+ .digest();
62
+ return digest.readBigUInt64LE(0) | (1n << 63n);
63
+ }
64
+ function snapshotOutcome(outcome) {
65
+ switch (outcome.kind) {
66
+ case 'value':
67
+ return {
68
+ kind: 'value',
69
+ v: outcome.value.toString('base64'),
70
+ sv: outcome.schemaVersion,
71
+ };
72
+ default:
73
+ return { kind: outcome.kind };
74
+ }
75
+ }
76
+ function loadOutcome(outcome) {
77
+ if (outcome.kind === 'value') {
78
+ return {
79
+ kind: 'value',
80
+ value: Buffer.from(outcome.v, 'base64'),
81
+ schemaVersion: outcome.sv,
82
+ };
83
+ }
84
+ return { kind: outcome.kind };
85
+ }
25
86
  function collOf(db, handle) {
26
87
  return handle >= 0 && handle < db.handles.length ? db.handles[handle] : null;
27
88
  }
89
+ function collOfFamily(db, handle, ...families) {
90
+ const coll = collOf(db, handle);
91
+ if (coll === null)
92
+ return INVALID_HANDLE;
93
+ return families.includes(coll.family) ? coll : OP_NOT_ALLOWED_FOR_FAMILY;
94
+ }
95
+ var DbOp;
96
+ (function (DbOp) {
97
+ DbOp[DbOp["Get"] = 0] = "Get";
98
+ DbOp[DbOp["GetMany"] = 1] = "GetMany";
99
+ DbOp[DbOp["Exists"] = 2] = "Exists";
100
+ DbOp[DbOp["Create"] = 3] = "Create";
101
+ DbOp[DbOp["Patch"] = 4] = "Patch";
102
+ DbOp[DbOp["Delete"] = 5] = "Delete";
103
+ DbOp[DbOp["GetDelete"] = 6] = "GetDelete";
104
+ DbOp[DbOp["Enqueue"] = 7] = "Enqueue";
105
+ DbOp[DbOp["Append"] = 8] = "Append";
106
+ DbOp[DbOp["AppendOnce"] = 9] = "AppendOnce";
107
+ DbOp[DbOp["Latest"] = 10] = "Latest";
108
+ DbOp[DbOp["CounterGet"] = 11] = "CounterGet";
109
+ DbOp[DbOp["CounterAdd"] = 12] = "CounterAdd";
110
+ DbOp[DbOp["MembershipContains"] = 13] = "MembershipContains";
111
+ DbOp[DbOp["MembershipAdd"] = 14] = "MembershipAdd";
112
+ DbOp[DbOp["MembershipRemove"] = 15] = "MembershipRemove";
113
+ DbOp[DbOp["MembershipList"] = 16] = "MembershipList";
114
+ DbOp[DbOp["UniqueLookup"] = 17] = "UniqueLookup";
115
+ DbOp[DbOp["UniqueClaim"] = 18] = "UniqueClaim";
116
+ DbOp[DbOp["UniqueRelease"] = 19] = "UniqueRelease";
117
+ DbOp[DbOp["CapacityAvailable"] = 20] = "CapacityAvailable";
118
+ DbOp[DbOp["CapacityReserve"] = 21] = "CapacityReserve";
119
+ DbOp[DbOp["CapacityConfirm"] = 22] = "CapacityConfirm";
120
+ DbOp[DbOp["CapacityCancel"] = 23] = "CapacityCancel";
121
+ DbOp[DbOp["ViewGet"] = 24] = "ViewGet";
122
+ DbOp[DbOp["ViewPublish"] = 25] = "ViewPublish";
123
+ DbOp[DbOp["CapacitySetTotal"] = 26] = "CapacitySetTotal";
124
+ })(DbOp || (DbOp = {}));
125
+ function isReadOp(op) {
126
+ return (op === DbOp.Get ||
127
+ op === DbOp.GetMany ||
128
+ op === DbOp.Exists ||
129
+ op === DbOp.ViewGet ||
130
+ op === DbOp.CounterGet ||
131
+ op === DbOp.MembershipContains ||
132
+ op === DbOp.MembershipList ||
133
+ op === DbOp.UniqueLookup ||
134
+ op === DbOp.Latest ||
135
+ op === DbOp.CapacityAvailable);
136
+ }
137
+ function isScanOp(op) {
138
+ return op === DbOp.Latest || op === DbOp.MembershipList;
139
+ }
140
+ function kindAllows(kind, op) {
141
+ if (kind === DbFunctionKind.Query)
142
+ return isReadOp(op) && !isScanOp(op);
143
+ if (kind === DbFunctionKind.Action) {
144
+ return ((isReadOp(op) && !isScanOp(op)) ||
145
+ op === DbOp.Create ||
146
+ op === DbOp.Patch ||
147
+ op === DbOp.Delete ||
148
+ op === DbOp.GetDelete ||
149
+ op === DbOp.Enqueue ||
150
+ op === DbOp.Append ||
151
+ op === DbOp.AppendOnce ||
152
+ op === DbOp.CounterAdd ||
153
+ op === DbOp.MembershipAdd ||
154
+ op === DbOp.MembershipRemove ||
155
+ op === DbOp.UniqueClaim ||
156
+ op === DbOp.UniqueRelease ||
157
+ op === DbOp.CapacityReserve ||
158
+ op === DbOp.CapacityConfirm ||
159
+ op === DbOp.CapacityCancel);
160
+ }
161
+ if (kind === DbFunctionKind.Derive)
162
+ return (isReadOp(op) || op === DbOp.ViewPublish || op === DbOp.Append || op === DbOp.CounterAdd);
163
+ if (kind === DbFunctionKind.Job)
164
+ return true;
165
+ return false;
166
+ }
167
+ function collForOp(db, handle, op, ...families) {
168
+ const coll = collOfFamily(db, handle, ...families);
169
+ if (typeof coll === 'number')
170
+ return coll;
171
+ return kindAllows(db.functionKind, op) ? coll : OP_NOT_ALLOWED_IN_KIND;
172
+ }
28
173
  export class DevDatabase {
29
174
  store = new Map();
175
+ recordIdem = new Map();
176
+ uniqueIdem = new Map();
30
177
  views = new Map();
31
178
  members = new Map();
32
179
  counters = new Map();
180
+ counterIdem = new Map();
33
181
  events = new Map();
34
182
  eventDedup = new Map();
35
183
  capacity = new Map();
36
184
  versions = new Map();
37
185
  eventVersions = new Map();
38
186
  memberVersions = new Map();
39
- catalog = new Map();
187
+ catalog = { kind: 'no-section' };
40
188
  persistPath = null;
41
189
  setCatalog(wasm) {
42
190
  this.catalog = parseCatalog(wasm);
43
191
  }
192
+ currentSchemaVersion(coll) {
193
+ if (this.catalog.kind !== 'present')
194
+ return coll.schemaVersion;
195
+ return this.catalog.collections.get(coll.name)?.schemaVersion ?? coll.schemaVersion;
196
+ }
44
197
  stampVersion(coll, sk) {
45
- this.versions.set(sk, this.catalog.get(coll) ?? 0);
198
+ this.versions.set(sk, this.currentSchemaVersion(coll));
199
+ }
200
+ recordIdemStart(coll, key, op, idem, requestHash) {
201
+ if (idem === null)
202
+ return { fresh: true };
203
+ const ik = idemKey(coll, key, op, idem);
204
+ const row = this.recordIdem.get(ik);
205
+ if (row === undefined) {
206
+ this.recordIdem.set(ik, { requestHash, outcome: null });
207
+ return { fresh: true };
208
+ }
209
+ if (row.requestHash !== requestHash)
210
+ return { fresh: false, status: CONFLICT };
211
+ if (row.outcome === null)
212
+ return { fresh: false, status: UNAVAILABLE };
213
+ return { fresh: false, status: 0, outcome: row.outcome };
214
+ }
215
+ recordIdemFinish(coll, key, op, idem, requestHash, outcome) {
216
+ if (idem === null)
217
+ return;
218
+ const ik = idemKey(coll, key, op, idem);
219
+ const row = this.recordIdem.get(ik);
220
+ if (row === undefined || row.requestHash !== requestHash)
221
+ return;
222
+ if (row.outcome === null)
223
+ row.outcome = outcome;
224
+ }
225
+ replayRecordOutcome(db, outcome) {
226
+ switch (outcome.kind) {
227
+ case 'unit':
228
+ return 0;
229
+ case 'value':
230
+ db.lastResult = outcome.value;
231
+ db.lastResultVersion = outcome.schemaVersion;
232
+ return outcome.value.length;
233
+ case 'absent':
234
+ case 'not_found':
235
+ return ABSENT;
236
+ case 'already_exists':
237
+ return ALREADY_EXISTS;
238
+ case 'conflict':
239
+ return CONFLICT;
240
+ }
46
241
  }
47
242
  configurePersistence(filePath) {
48
243
  this.persistPath = filePath;
@@ -53,15 +248,26 @@ export class DevDatabase {
53
248
  return;
54
249
  const snap = {
55
250
  store: {},
251
+ recordIdem: {},
252
+ uniqueIdem: {},
56
253
  views: {},
57
254
  members: {},
58
255
  counters: {},
256
+ counterIdem: {},
59
257
  events: {},
60
258
  eventDedup: {},
61
259
  capacity: {},
62
260
  };
63
261
  for (const [k, v] of this.store)
64
262
  snap.store[k] = { v: v.toString('base64'), sv: this.versions.get(k) ?? 0 };
263
+ for (const [k, row] of this.recordIdem)
264
+ snap.recordIdem[k] = {
265
+ requestHash: row.requestHash,
266
+ state: row.outcome === null ? 'pending' : 'done',
267
+ outcome: row.outcome === null ? undefined : snapshotOutcome(row.outcome),
268
+ };
269
+ for (const [k, v] of this.uniqueIdem)
270
+ snap.uniqueIdem[k] = v;
65
271
  for (const [k, v] of this.views)
66
272
  snap.views[k] = { v: v.toString('base64'), sv: this.versions.get(k) ?? 0 };
67
273
  for (const [k, m] of this.members) {
@@ -73,6 +279,8 @@ export class DevDatabase {
73
279
  }
74
280
  for (const [k, v] of this.counters)
75
281
  snap.counters[k] = v.toString();
282
+ for (const [k, v] of this.counterIdem)
283
+ snap.counterIdem[k] = v.toString();
76
284
  for (const [k, log] of this.events) {
77
285
  const ver = this.eventVersions.get(k) ?? [];
78
286
  snap.events[k] = log.map((b, i) => ({ v: b.toString('base64'), sv: ver[i] ?? 0 }));
@@ -112,6 +320,16 @@ export class DevDatabase {
112
320
  this.store.set(k, Buffer.from(e.v, 'base64'));
113
321
  this.versions.set(k, e.sv);
114
322
  }
323
+ for (const [k, row] of Object.entries(snap.recordIdem ?? {})) {
324
+ this.recordIdem.set(k, {
325
+ requestHash: row.requestHash,
326
+ outcome: row.state === 'done' && row.outcome !== undefined
327
+ ? loadOutcome(row.outcome)
328
+ : null,
329
+ });
330
+ }
331
+ for (const [k, v] of Object.entries(snap.uniqueIdem ?? {}))
332
+ this.uniqueIdem.set(k, v);
115
333
  for (const [k, e] of Object.entries(snap.views ?? {})) {
116
334
  this.views.set(k, Buffer.from(e.v, 'base64'));
117
335
  this.versions.set(k, e.sv);
@@ -128,6 +346,8 @@ export class DevDatabase {
128
346
  }
129
347
  for (const [k, v] of Object.entries(snap.counters ?? {}))
130
348
  this.counters.set(k, BigInt(v));
349
+ for (const [k, v] of Object.entries(snap.counterIdem ?? {}))
350
+ this.counterIdem.set(k, BigInt(v));
131
351
  for (const [k, log] of Object.entries(snap.events ?? {})) {
132
352
  this.events.set(k, log.map((e) => Buffer.from(e.v, 'base64')));
133
353
  this.eventVersions.set(k, log.map((e) => e.sv));
@@ -151,11 +371,14 @@ export class DevDatabase {
151
371
  }
152
372
  clear() {
153
373
  this.store.clear();
374
+ this.recordIdem.clear();
375
+ this.uniqueIdem.clear();
154
376
  this.versions.clear();
155
377
  this.views.clear();
156
378
  this.members.clear();
157
379
  this.memberVersions.clear();
158
380
  this.counters.clear();
381
+ this.counterIdem.clear();
159
382
  this.eventVersions.clear();
160
383
  this.events.clear();
161
384
  this.eventDedup.clear();
@@ -184,8 +407,31 @@ export class DevDatabase {
184
407
  if (nameLen < 0 || nameLen > MAX_NAME)
185
408
  throw new Error('data: collection name too long');
186
409
  const name = readCopy(ref, namePtr, nameLen).toString('utf8');
410
+ let coll;
411
+ switch (this.catalog.kind) {
412
+ case 'present': {
413
+ const found = this.catalog.collections.get(name);
414
+ if (found === undefined)
415
+ return SCHEMA_UNAVAILABLE;
416
+ coll = { ...found };
417
+ break;
418
+ }
419
+ case 'malformed':
420
+ return SCHEMA_UNAVAILABLE;
421
+ case 'no-section':
422
+ coll = {
423
+ name,
424
+ family: CollectionFamily.Record,
425
+ schemaVersion: 0,
426
+ replication: 0,
427
+ placement: 0,
428
+ fillMaxWaitMs: DEFAULT_FILL_WAIT_MS,
429
+ fillAllowStale: true,
430
+ };
431
+ break;
432
+ }
187
433
  const handle = db.handles.length;
188
- db.handles.push(name);
434
+ db.handles.push(coll);
189
435
  const m = mem(ref);
190
436
  if (outHandlePtr < 0 || outHandlePtr + 4 > m.length)
191
437
  throw new Error('data: resolve out-handle out of bounds');
@@ -193,12 +439,12 @@ export class DevDatabase {
193
439
  return 0;
194
440
  }
195
441
  get(ref, db, handle, keyPtr, keyLen) {
196
- const coll = collOf(db, handle);
197
- if (coll === null)
198
- return INVALID_HANDLE;
442
+ const coll = collForOp(db, handle, DbOp.Get, CollectionFamily.Record);
443
+ if (typeof coll === 'number')
444
+ return coll;
199
445
  if (keyLen > MAX_KEY)
200
446
  throw new Error('data: key too long');
201
- const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
447
+ const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
202
448
  const v = this.store.get(sk);
203
449
  if (v === undefined)
204
450
  return ABSENT;
@@ -207,11 +453,12 @@ export class DevDatabase {
207
453
  return v.length;
208
454
  }
209
455
  getMany(ref, db, handle, keysPtr, keysLen) {
210
- const coll = collOf(db, handle);
211
- if (coll === null)
212
- return INVALID_HANDLE;
456
+ const coll = collForOp(db, handle, DbOp.GetMany, CollectionFamily.Record, CollectionFamily.View);
457
+ if (typeof coll === 'number')
458
+ return coll;
213
459
  if (keysLen > MAX_VALUE)
214
460
  throw new Error('data: keys blob too large');
461
+ const table = coll.family === CollectionFamily.View ? this.views : this.store;
215
462
  const r = new DataReader(readCopy(ref, keysPtr, keysLen));
216
463
  const count = r.readU32();
217
464
  if (count > 1024)
@@ -222,81 +469,122 @@ export class DevDatabase {
222
469
  const key = r.readBytes();
223
470
  if (key.length > MAX_KEY)
224
471
  throw new Error('data: key too long');
225
- const sk = storeKey(coll, Buffer.from(key));
226
- const v = this.store.get(sk);
472
+ const sk = storeKey(coll.name, Buffer.from(key));
473
+ const v = table.get(sk);
227
474
  if (v === undefined) {
228
475
  w.writeU8(0);
229
476
  }
230
477
  else {
231
- w.writeU8(1).writeU32(this.versions.get(sk) ?? 0).writeBytes(v);
478
+ w.writeU8(1)
479
+ .writeU32(this.versions.get(sk) ?? 0)
480
+ .writeBytes(v);
232
481
  }
233
482
  }
234
483
  db.lastResult = Buffer.from(w.toBytes());
235
484
  return db.lastResult.length;
236
485
  }
237
486
  exists(ref, db, handle, keyPtr, keyLen) {
238
- const coll = collOf(db, handle);
239
- if (coll === null)
240
- return INVALID_HANDLE;
241
- return this.store.has(storeKey(coll, readKey(ref, keyPtr, keyLen))) ? 1 : 0;
242
- }
243
- create(ref, db, handle, keyPtr, keyLen, valPtr, valLen) {
244
- const coll = collOf(db, handle);
245
- if (coll === null)
246
- return INVALID_HANDLE;
487
+ const coll = collForOp(db, handle, DbOp.Exists, CollectionFamily.Record);
488
+ if (typeof coll === 'number')
489
+ return coll;
490
+ return this.store.has(storeKey(coll.name, readKey(ref, keyPtr, keyLen))) ? 1 : 0;
491
+ }
492
+ create(ref, db, handle, keyPtr, keyLen, valPtr, valLen, idemPtr) {
493
+ const coll = collForOp(db, handle, DbOp.Create, CollectionFamily.Record);
494
+ if (typeof coll === 'number')
495
+ return coll;
247
496
  if (keyLen > MAX_KEY || valLen > MAX_VALUE)
248
497
  throw new Error('data: key/value too large');
249
- const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
250
- if (this.store.has(sk))
251
- return ALREADY_EXISTS;
252
- this.store.set(sk, readCopy(ref, valPtr, valLen));
253
- this.stampVersion(coll, sk);
254
- return 0;
498
+ const key = readKey(ref, keyPtr, keyLen);
499
+ const value = readCopy(ref, valPtr, valLen);
500
+ const idem = readIdem(ref, idemPtr);
501
+ const requestHash = recordRequestHash(RECORD_OP_CREATE, key, value);
502
+ const start = this.recordIdemStart(coll, key, 'C', idem, requestHash);
503
+ if (!start.fresh)
504
+ return start.outcome ? this.replayRecordOutcome(db, start.outcome) : start.status;
505
+ const sk = storeKey(coll.name, key);
506
+ const outcome = this.store.has(sk)
507
+ ? { kind: 'already_exists' }
508
+ : { kind: 'unit' };
509
+ if (outcome.kind === 'unit') {
510
+ this.store.set(sk, value);
511
+ this.stampVersion(coll, sk);
512
+ }
513
+ this.recordIdemFinish(coll, key, 'C', idem, requestHash, outcome);
514
+ return this.replayRecordOutcome(db, outcome);
255
515
  }
256
- patch(ref, db, handle, keyPtr, keyLen, patchPtr, patchLen) {
257
- const coll = collOf(db, handle);
258
- if (coll === null)
259
- return INVALID_HANDLE;
516
+ patch(ref, db, handle, keyPtr, keyLen, patchPtr, patchLen, idemPtr) {
517
+ const coll = collForOp(db, handle, DbOp.Patch, CollectionFamily.Record);
518
+ if (typeof coll === 'number')
519
+ return coll;
260
520
  if (keyLen > MAX_KEY || patchLen > MAX_VALUE)
261
521
  throw new Error('data: key/patch too large');
262
- const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
263
- if (!this.store.has(sk))
264
- return ABSENT;
522
+ const key = readKey(ref, keyPtr, keyLen);
265
523
  const v = readCopy(ref, patchPtr, patchLen);
266
- this.store.set(sk, v);
267
- this.stampVersion(coll, sk);
268
- db.lastResult = v;
269
- db.lastResultVersion = -1;
270
- return v.length;
524
+ const idem = readIdem(ref, idemPtr);
525
+ const requestHash = recordRequestHash(RECORD_OP_PATCH, key, v);
526
+ const start = this.recordIdemStart(coll, key, 'P', idem, requestHash);
527
+ if (!start.fresh)
528
+ return start.outcome ? this.replayRecordOutcome(db, start.outcome) : start.status;
529
+ const sk = storeKey(coll.name, key);
530
+ const outcome = this.store.has(sk)
531
+ ? { kind: 'value', value: v, schemaVersion: -1 }
532
+ : { kind: 'not_found' };
533
+ if (outcome.kind === 'value') {
534
+ this.store.set(sk, v);
535
+ this.stampVersion(coll, sk);
536
+ }
537
+ this.recordIdemFinish(coll, key, 'P', idem, requestHash, outcome);
538
+ return this.replayRecordOutcome(db, outcome);
271
539
  }
272
- delete(ref, db, handle, keyPtr, keyLen) {
273
- const coll = collOf(db, handle);
274
- if (coll === null)
275
- return INVALID_HANDLE;
276
- const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
540
+ delete(ref, db, handle, keyPtr, keyLen, idemPtr) {
541
+ const coll = collForOp(db, handle, DbOp.Delete, CollectionFamily.Record);
542
+ if (typeof coll === 'number')
543
+ return coll;
544
+ const key = readKey(ref, keyPtr, keyLen);
545
+ const idem = readIdem(ref, idemPtr);
546
+ const requestHash = recordRequestHash(RECORD_OP_DELETE, key, Buffer.alloc(0));
547
+ const start = this.recordIdemStart(coll, key, 'D', idem, requestHash);
548
+ if (!start.fresh)
549
+ return start.outcome ? this.replayRecordOutcome(db, start.outcome) : start.status;
550
+ const sk = storeKey(coll.name, key);
277
551
  this.store.delete(sk);
278
552
  this.versions.delete(sk);
279
- return 0;
553
+ const outcome = { kind: 'unit' };
554
+ this.recordIdemFinish(coll, key, 'D', idem, requestHash, outcome);
555
+ return this.replayRecordOutcome(db, outcome);
280
556
  }
281
- getDelete(ref, db, handle, keyPtr, keyLen) {
282
- const coll = collOf(db, handle);
283
- if (coll === null)
284
- return INVALID_HANDLE;
285
- const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
557
+ getDelete(ref, db, handle, keyPtr, keyLen, idemPtr) {
558
+ const coll = collForOp(db, handle, DbOp.GetDelete, CollectionFamily.Record);
559
+ if (typeof coll === 'number')
560
+ return coll;
561
+ const key = readKey(ref, keyPtr, keyLen);
562
+ const idem = readIdem(ref, idemPtr);
563
+ const requestHash = recordRequestHash(RECORD_OP_GET_DELETE, key, Buffer.alloc(0));
564
+ const start = this.recordIdemStart(coll, key, 'G', idem, requestHash);
565
+ if (!start.fresh)
566
+ return start.outcome ? this.replayRecordOutcome(db, start.outcome) : start.status;
567
+ const sk = storeKey(coll.name, key);
286
568
  const v = this.store.get(sk);
287
- if (v === undefined)
288
- return ABSENT;
289
- db.lastResultVersion = this.versions.get(sk) ?? 0;
290
- this.store.delete(sk);
291
- this.versions.delete(sk);
292
- db.lastResult = v;
293
- return v.length;
569
+ const outcome = v === undefined
570
+ ? { kind: 'absent' }
571
+ : {
572
+ kind: 'value',
573
+ value: v,
574
+ schemaVersion: this.versions.get(sk) ?? 0,
575
+ };
576
+ if (outcome.kind === 'value') {
577
+ this.store.delete(sk);
578
+ this.versions.delete(sk);
579
+ }
580
+ this.recordIdemFinish(coll, key, 'G', idem, requestHash, outcome);
581
+ return this.replayRecordOutcome(db, outcome);
294
582
  }
295
583
  uniqueLookup(ref, db, handle, keyPtr, keyLen) {
296
- const coll = collOf(db, handle);
297
- if (coll === null)
298
- return INVALID_HANDLE;
299
- const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
584
+ const coll = collForOp(db, handle, DbOp.UniqueLookup, CollectionFamily.Unique);
585
+ if (typeof coll === 'number')
586
+ return coll;
587
+ const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
300
588
  const v = this.store.get(sk);
301
589
  if (v === undefined)
302
590
  return ABSENT;
@@ -304,55 +592,59 @@ export class DevDatabase {
304
592
  db.lastResultVersion = this.versions.get(sk) ?? 0;
305
593
  return v.length;
306
594
  }
307
- uniqueClaim(ref, db, handle, keyPtr, keyLen, valPtr, valLen) {
308
- const coll = collOf(db, handle);
309
- if (coll === null)
310
- return INVALID_HANDLE;
595
+ uniqueClaim(ref, db, handle, keyPtr, keyLen, valPtr, valLen, idemPtr) {
596
+ const coll = collForOp(db, handle, DbOp.UniqueClaim, CollectionFamily.Unique);
597
+ if (typeof coll === 'number')
598
+ return coll;
311
599
  if (keyLen > MAX_KEY || valLen > MAX_VALUE)
312
600
  throw new Error('data: key/value too large');
313
- const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
601
+ const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
314
602
  const owner = readCopy(ref, valPtr, valLen);
603
+ const idem = readIdem(ref, idemPtr)?.toString('hex') ?? '';
315
604
  const existing = this.store.get(sk);
316
605
  if (existing === undefined) {
317
606
  this.store.set(sk, owner);
607
+ this.uniqueIdem.set(sk, idem);
318
608
  this.stampVersion(coll, sk);
319
609
  return 0;
320
610
  }
321
- if (existing.equals(owner))
322
- return 2;
611
+ if (existing.equals(owner)) {
612
+ return (this.uniqueIdem.get(sk) ?? '') === idem ? 2 : CONFLICT;
613
+ }
323
614
  db.lastResult = existing;
324
615
  return 1;
325
616
  }
326
617
  uniqueRelease(ref, db, handle, keyPtr, keyLen, valPtr, valLen) {
327
- const coll = collOf(db, handle);
328
- if (coll === null)
329
- return INVALID_HANDLE;
330
- const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
618
+ const coll = collForOp(db, handle, DbOp.UniqueRelease, CollectionFamily.Unique);
619
+ if (typeof coll === 'number')
620
+ return coll;
621
+ const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
331
622
  const existing = this.store.get(sk);
332
623
  if (existing === undefined)
333
624
  return 0;
334
625
  if (!existing.equals(readCopy(ref, valPtr, valLen)))
335
626
  return CONFLICT;
336
627
  this.store.delete(sk);
628
+ this.uniqueIdem.delete(sk);
337
629
  this.versions.delete(sk);
338
630
  return 0;
339
631
  }
340
632
  membershipContains(ref, db, handle, setPtr, setLen, memberPtr, memberLen) {
341
- const coll = collOf(db, handle);
342
- if (coll === null)
343
- return INVALID_HANDLE;
344
- const set = this.members.get(storeKey(coll, readKey(ref, setPtr, setLen)));
633
+ const coll = collForOp(db, handle, DbOp.MembershipContains, CollectionFamily.Membership);
634
+ if (typeof coll === 'number')
635
+ return coll;
636
+ const set = this.members.get(storeKey(coll.name, readKey(ref, setPtr, setLen)));
345
637
  if (set === undefined)
346
638
  return 0;
347
639
  return set.has(readCopy(ref, memberPtr, memberLen).toString('latin1')) ? 1 : 0;
348
640
  }
349
641
  membershipAdd(ref, db, handle, setPtr, setLen, memberPtr, memberLen) {
350
- const coll = collOf(db, handle);
351
- if (coll === null)
352
- return INVALID_HANDLE;
642
+ const coll = collForOp(db, handle, DbOp.MembershipAdd, CollectionFamily.Membership);
643
+ if (typeof coll === 'number')
644
+ return coll;
353
645
  if (setLen > MAX_KEY || memberLen > MAX_VALUE)
354
646
  throw new Error('data: set/member too large');
355
- const sk = storeKey(coll, readKey(ref, setPtr, setLen));
647
+ const sk = storeKey(coll.name, readKey(ref, setPtr, setLen));
356
648
  const member = readCopy(ref, memberPtr, memberLen);
357
649
  let set = this.members.get(sk);
358
650
  if (set === undefined) {
@@ -366,24 +658,24 @@ export class DevDatabase {
366
658
  mv = new Map();
367
659
  this.memberVersions.set(sk, mv);
368
660
  }
369
- mv.set(ml, this.catalog.get(coll) ?? 0);
661
+ mv.set(ml, this.currentSchemaVersion(coll));
370
662
  return 0;
371
663
  }
372
664
  membershipRemove(ref, db, handle, setPtr, setLen, memberPtr, memberLen) {
373
- const coll = collOf(db, handle);
374
- if (coll === null)
375
- return INVALID_HANDLE;
376
- const sk = storeKey(coll, readKey(ref, setPtr, setLen));
665
+ const coll = collForOp(db, handle, DbOp.MembershipRemove, CollectionFamily.Membership);
666
+ if (typeof coll === 'number')
667
+ return coll;
668
+ const sk = storeKey(coll.name, readKey(ref, setPtr, setLen));
377
669
  const ml = readCopy(ref, memberPtr, memberLen).toString('latin1');
378
670
  this.members.get(sk)?.delete(ml);
379
671
  this.memberVersions.get(sk)?.delete(ml);
380
672
  return 0;
381
673
  }
382
674
  membershipList(ref, db, handle, setPtr, setLen, limit) {
383
- const coll = collOf(db, handle);
384
- if (coll === null)
385
- return INVALID_HANDLE;
386
- const sk = storeKey(coll, readKey(ref, setPtr, setLen));
675
+ const coll = collForOp(db, handle, DbOp.MembershipList, CollectionFamily.Membership);
676
+ if (typeof coll === 'number')
677
+ return coll;
678
+ const sk = storeKey(coll.name, readKey(ref, setPtr, setLen));
387
679
  const set = this.members.get(sk);
388
680
  const mv = this.memberVersions.get(sk);
389
681
  const n = Math.max(0, Math.min(limit, 0xffff));
@@ -397,10 +689,10 @@ export class DevDatabase {
397
689
  return db.lastResult.length;
398
690
  }
399
691
  viewGet(ref, db, handle, keyPtr, keyLen) {
400
- const coll = collOf(db, handle);
401
- if (coll === null)
402
- return INVALID_HANDLE;
403
- const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
692
+ const coll = collForOp(db, handle, DbOp.ViewGet, CollectionFamily.View);
693
+ if (typeof coll === 'number')
694
+ return coll;
695
+ const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
404
696
  const v = this.views.get(sk);
405
697
  if (v === undefined)
406
698
  return ABSENT;
@@ -409,44 +701,67 @@ export class DevDatabase {
409
701
  return v.length;
410
702
  }
411
703
  viewPublish(ref, db, handle, keyPtr, keyLen, valPtr, valLen) {
412
- const coll = collOf(db, handle);
413
- if (coll === null)
414
- return INVALID_HANDLE;
704
+ const coll = collForOp(db, handle, DbOp.ViewPublish, CollectionFamily.View);
705
+ if (typeof coll === 'number')
706
+ return coll;
415
707
  if (keyLen > MAX_KEY || valLen > MAX_VALUE)
416
708
  throw new Error('data: key/view too large');
417
- const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
709
+ const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
418
710
  this.views.set(sk, readCopy(ref, valPtr, valLen));
419
711
  this.stampVersion(coll, sk);
420
712
  return 0;
421
713
  }
422
714
  counterGet(ref, db, handle, keyPtr, keyLen) {
423
- const coll = collOf(db, handle);
424
- if (coll === null)
425
- return INVALID_HANDLE;
426
- const sum = this.counters.get(storeKey(coll, readKey(ref, keyPtr, keyLen))) ?? 0n;
715
+ const coll = collForOp(db, handle, DbOp.CounterGet, CollectionFamily.Counter);
716
+ if (typeof coll === 'number')
717
+ return coll;
718
+ const sum = this.counters.get(storeKey(coll.name, readKey(ref, keyPtr, keyLen))) ?? 0n;
427
719
  const out = Buffer.alloc(8);
428
720
  out.writeBigInt64LE(sum);
429
721
  db.lastResult = out;
430
722
  return out.length;
431
723
  }
432
- counterAdd(ref, db, handle, keyPtr, keyLen, delta) {
433
- const coll = collOf(db, handle);
434
- if (coll === null)
435
- return INVALID_HANDLE;
436
- const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
437
- this.counters.set(sk, satI64((this.counters.get(sk) ?? 0n) + BigInt(delta)));
724
+ counterAdd(ref, db, handle, keyPtr, keyLen, delta, idemPtr) {
725
+ const coll = collForOp(db, handle, DbOp.CounterAdd, CollectionFamily.Counter);
726
+ if (typeof coll === 'number')
727
+ return coll;
728
+ const key = readKey(ref, keyPtr, keyLen);
729
+ const idem = readIdem(ref, idemPtr);
730
+ const d = BigInt(delta);
731
+ if (idem !== null) {
732
+ const ik = idemKey(coll, key, 'A', idem);
733
+ const seen = this.counterIdem.get(ik);
734
+ if (seen !== undefined)
735
+ return seen === d ? 0 : CONFLICT;
736
+ this.counterIdem.set(ik, d);
737
+ }
738
+ const sk = storeKey(coll.name, key);
739
+ this.counters.set(sk, satI64((this.counters.get(sk) ?? 0n) + d));
438
740
  return 0;
439
741
  }
440
- append(ref, db, handle, keyPtr, keyLen, evPtr, evLen) {
441
- const coll = collOf(db, handle);
442
- if (coll === null)
443
- return INVALID_HANDLE;
742
+ append(ref, db, handle, keyPtr, keyLen, evPtr, evLen, idemPtr) {
743
+ const coll = collForOp(db, handle, DbOp.Append, CollectionFamily.Events);
744
+ if (typeof coll === 'number')
745
+ return coll;
444
746
  if (keyLen > MAX_KEY || evLen > MAX_VALUE)
445
747
  throw new Error('data: key/event too large');
446
- const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
748
+ const key = readKey(ref, keyPtr, keyLen);
749
+ const sk = storeKey(coll.name, key);
750
+ const idem = readIdem(ref, idemPtr);
751
+ if (idem !== null) {
752
+ let seen = this.eventDedup.get(sk);
753
+ if (seen === undefined) {
754
+ seen = new Set();
755
+ this.eventDedup.set(sk, seen);
756
+ }
757
+ const eventId = idem.toString('latin1');
758
+ if (seen.has(eventId))
759
+ return 0;
760
+ seen.add(eventId);
761
+ }
447
762
  const log = this.events.get(sk);
448
763
  const ev = readCopy(ref, evPtr, evLen);
449
- const sv = this.catalog.get(coll) ?? 0;
764
+ const sv = this.currentSchemaVersion(coll);
450
765
  if (log === undefined) {
451
766
  this.events.set(sk, [ev]);
452
767
  this.eventVersions.set(sk, [sv]);
@@ -458,12 +773,12 @@ export class DevDatabase {
458
773
  return 0;
459
774
  }
460
775
  appendOnce(ref, db, handle, keyPtr, keyLen, evidPtr, evidLen, evPtr, evLen) {
461
- const coll = collOf(db, handle);
462
- if (coll === null)
463
- return INVALID_HANDLE;
776
+ const coll = collForOp(db, handle, DbOp.AppendOnce, CollectionFamily.Events);
777
+ if (typeof coll === 'number')
778
+ return coll;
464
779
  if (keyLen > MAX_KEY || evLen > MAX_VALUE)
465
780
  throw new Error('data: key/event too large');
466
- const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
781
+ const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
467
782
  const evid = readCopy(ref, evidPtr, evidLen).toString('latin1');
468
783
  let seen = this.eventDedup.get(sk);
469
784
  if (seen === undefined) {
@@ -473,7 +788,7 @@ export class DevDatabase {
473
788
  if (seen.has(evid))
474
789
  return 0;
475
790
  const ev = readCopy(ref, evPtr, evLen);
476
- const sv = this.catalog.get(coll) ?? 0;
791
+ const sv = this.currentSchemaVersion(coll);
477
792
  const log = this.events.get(sk);
478
793
  if (log === undefined) {
479
794
  this.events.set(sk, [ev]);
@@ -486,24 +801,35 @@ export class DevDatabase {
486
801
  seen.add(evid);
487
802
  return 1;
488
803
  }
489
- enqueue(ref, db, handle, keyPtr, keyLen, valPtr, valLen) {
490
- const coll = collOf(db, handle);
491
- if (coll === null)
492
- return INVALID_HANDLE;
804
+ enqueue(ref, db, handle, keyPtr, keyLen, valPtr, valLen, idemPtr) {
805
+ const coll = collForOp(db, handle, DbOp.Enqueue, CollectionFamily.Record);
806
+ if (typeof coll === 'number')
807
+ return coll;
493
808
  if (keyLen > MAX_KEY || valLen > MAX_VALUE)
494
809
  throw new Error('data: key/value too large');
495
- const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
496
- if (!this.store.has(sk))
497
- return ABSENT;
498
- this.store.set(sk, readCopy(ref, valPtr, valLen));
499
- this.stampVersion(coll, sk);
500
- return 0;
810
+ const key = readKey(ref, keyPtr, keyLen);
811
+ const value = readCopy(ref, valPtr, valLen);
812
+ const idem = readIdem(ref, idemPtr);
813
+ const requestHash = recordRequestHash(RECORD_OP_ENQUEUE, key, value);
814
+ const start = this.recordIdemStart(coll, key, 'E', idem, requestHash);
815
+ if (!start.fresh)
816
+ return start.outcome ? this.replayRecordOutcome(db, start.outcome) : start.status;
817
+ const sk = storeKey(coll.name, key);
818
+ const outcome = this.store.has(sk)
819
+ ? { kind: 'unit' }
820
+ : { kind: 'not_found' };
821
+ if (outcome.kind === 'unit') {
822
+ this.store.set(sk, value);
823
+ this.stampVersion(coll, sk);
824
+ }
825
+ this.recordIdemFinish(coll, key, 'E', idem, requestHash, outcome);
826
+ return this.replayRecordOutcome(db, outcome);
501
827
  }
502
828
  latest(ref, db, handle, keyPtr, keyLen, limit) {
503
- const coll = collOf(db, handle);
504
- if (coll === null)
505
- return INVALID_HANDLE;
506
- const sk = storeKey(coll, readKey(ref, keyPtr, keyLen));
829
+ const coll = collForOp(db, handle, DbOp.Latest, CollectionFamily.Events);
830
+ if (typeof coll === 'number')
831
+ return coll;
832
+ const sk = storeKey(coll.name, readKey(ref, keyPtr, keyLen));
507
833
  const log = this.events.get(sk) ?? [];
508
834
  const vers = this.eventVersions.get(sk) ?? [];
509
835
  const n = Math.max(0, Math.min(limit, 0xffff));
@@ -519,19 +845,19 @@ export class DevDatabase {
519
845
  return db.lastResult.length;
520
846
  }
521
847
  capacitySetTotal(ref, db, handle, keyPtr, keyLen, total) {
522
- const coll = collOf(db, handle);
523
- if (coll === null)
524
- return INVALID_HANDLE;
525
- const l = this.capLedger(storeKey(coll, readKey(ref, keyPtr, keyLen)));
848
+ const coll = collForOp(db, handle, DbOp.CapacitySetTotal, CollectionFamily.Capacity);
849
+ if (typeof coll === 'number')
850
+ return coll;
851
+ const l = this.capLedger(storeKey(coll.name, readKey(ref, keyPtr, keyLen)));
526
852
  const t = BigInt(total);
527
853
  l.total = satI64(t < 0n ? 0n : t);
528
854
  return 0;
529
855
  }
530
856
  capacityAvailable(ref, db, handle, keyPtr, keyLen) {
531
- const coll = collOf(db, handle);
532
- if (coll === null)
533
- return INVALID_HANDLE;
534
- const l = this.capLedger(storeKey(coll, readKey(ref, keyPtr, keyLen)));
857
+ const coll = collForOp(db, handle, DbOp.CapacityAvailable, CollectionFamily.Capacity);
858
+ if (typeof coll === 'number')
859
+ return coll;
860
+ const l = this.capLedger(storeKey(coll.name, readKey(ref, keyPtr, keyLen)));
535
861
  this.capPrune(l, Date.now());
536
862
  const avail = l.total - this.capReserved(l);
537
863
  const out = Buffer.alloc(8);
@@ -539,20 +865,34 @@ export class DevDatabase {
539
865
  db.lastResult = out;
540
866
  return out.length;
541
867
  }
542
- capacityReserve(ref, db, handle, keyPtr, keyLen, amount, ttlMs) {
543
- const coll = collOf(db, handle);
544
- if (coll === null)
545
- return INVALID_HANDLE;
868
+ capacityReserve(ref, db, handle, keyPtr, keyLen, amount, ttlMs, idemPtr) {
869
+ const coll = collForOp(db, handle, DbOp.CapacityReserve, CollectionFamily.Capacity);
870
+ if (typeof coll === 'number')
871
+ return coll;
546
872
  const want = BigInt(amount);
547
873
  if (want <= 0n)
548
874
  return CODEC_ERR;
549
- const l = this.capLedger(storeKey(coll, readKey(ref, keyPtr, keyLen)));
875
+ const key = readKey(ref, keyPtr, keyLen);
876
+ const idem = readIdem(ref, idemPtr);
877
+ const ttl = Math.min(Math.max(0, Number(ttlMs)), MAX_RESERVATION_TTL_MS);
878
+ const requestedId = idem === null ? null : reservationIdFromIdem(coll, key, idem);
879
+ const l = this.capLedger(storeKey(coll.name, key));
550
880
  const now = Date.now();
551
881
  this.capPrune(l, now);
882
+ if (requestedId !== null) {
883
+ const existing = l.reservations.get(requestedId);
884
+ if (existing !== undefined) {
885
+ if (existing.amount !== want)
886
+ return CONFLICT;
887
+ const out = Buffer.alloc(8);
888
+ out.writeBigUInt64LE(requestedId);
889
+ db.lastResult = out;
890
+ return out.length;
891
+ }
892
+ }
552
893
  if (l.total - this.capReserved(l) < want || l.reservations.size >= MAX_RESERVATIONS)
553
894
  return ABSENT;
554
- const ttl = Math.min(Math.max(0, Number(ttlMs)), MAX_RESERVATION_TTL_MS);
555
- const id = l.nextId++;
895
+ const id = requestedId ?? l.nextId++;
556
896
  l.reservations.set(id, { amount: want, expiresMs: now + ttl, confirmed: false });
557
897
  const out = Buffer.alloc(8);
558
898
  out.writeBigUInt64LE(id);
@@ -560,10 +900,10 @@ export class DevDatabase {
560
900
  return out.length;
561
901
  }
562
902
  capacityConfirm(ref, db, handle, keyPtr, keyLen, reservationId) {
563
- const coll = collOf(db, handle);
564
- if (coll === null)
565
- return INVALID_HANDLE;
566
- const l = this.capLedger(storeKey(coll, readKey(ref, keyPtr, keyLen)));
903
+ const coll = collForOp(db, handle, DbOp.CapacityConfirm, CollectionFamily.Capacity);
904
+ if (typeof coll === 'number')
905
+ return coll;
906
+ const l = this.capLedger(storeKey(coll.name, readKey(ref, keyPtr, keyLen)));
567
907
  this.capPrune(l, Date.now());
568
908
  const r = l.reservations.get(BigInt(reservationId));
569
909
  if (r === undefined)
@@ -572,10 +912,10 @@ export class DevDatabase {
572
912
  return 1;
573
913
  }
574
914
  capacityCancel(ref, db, handle, keyPtr, keyLen, reservationId) {
575
- const coll = collOf(db, handle);
576
- if (coll === null)
577
- return INVALID_HANDLE;
578
- const l = this.capLedger(storeKey(coll, readKey(ref, keyPtr, keyLen)));
915
+ const coll = collForOp(db, handle, DbOp.CapacityCancel, CollectionFamily.Capacity);
916
+ if (typeof coll === 'number')
917
+ return coll;
918
+ const l = this.capLedger(storeKey(coll.name, readKey(ref, keyPtr, keyLen)));
579
919
  this.capPrune(l, Date.now());
580
920
  const id = BigInt(reservationId);
581
921
  const r = l.reservations.get(id);
@@ -602,11 +942,41 @@ export class DevDatabase {
602
942
  }
603
943
  resetForTests() {
604
944
  this.clear();
605
- this.catalog = new Map();
945
+ this.catalog = { kind: 'no-section' };
606
946
  this.persistPath = null;
607
947
  }
608
948
  setCatalogForTests(entries) {
609
- this.catalog = new Map(Object.entries(entries));
949
+ const collections = new Map();
950
+ for (const [name, entry] of Object.entries(entries)) {
951
+ const schemaVersion = typeof entry === 'number' ? entry : (entry.schemaVersion ?? 0);
952
+ const family = typeof entry === 'number'
953
+ ? CollectionFamily.Record
954
+ : (entry.family ?? CollectionFamily.Record);
955
+ const replication = typeof entry === 'number' ? 0 : (entry.replication ?? 0);
956
+ const placement = typeof entry === 'number' ? 0 : (entry.placement ?? 0);
957
+ const fillMaxWaitMs = typeof entry === 'number'
958
+ ? DEFAULT_FILL_WAIT_MS
959
+ : (entry.fillMaxWaitMs ?? DEFAULT_FILL_WAIT_MS);
960
+ const fillAllowStale = typeof entry === 'number' ? true : (entry.fillAllowStale ?? true);
961
+ if (!isCollectionFamily(family)) {
962
+ this.catalog = { kind: 'malformed' };
963
+ return;
964
+ }
965
+ if (fillMaxWaitMs > MAX_FILL_WAIT_MS) {
966
+ this.catalog = { kind: 'malformed' };
967
+ return;
968
+ }
969
+ collections.set(name, {
970
+ name,
971
+ family,
972
+ schemaVersion: schemaVersion >>> 0,
973
+ replication,
974
+ placement,
975
+ fillMaxWaitMs,
976
+ fillAllowStale,
977
+ });
978
+ }
979
+ this.catalog = { kind: 'present', collections };
610
980
  }
611
981
  }
612
982
  export const devDb = new DevDatabase();
@@ -625,12 +995,12 @@ export function buildDatabaseImports(ref, db) {
625
995
  'data.get': (handle, keyPtr, keyLen) => devDb.get(ref, db, handle, keyPtr, keyLen),
626
996
  'data.get_many': (handle, keysPtr, keysLen) => devDb.getMany(ref, db, handle, keysPtr, keysLen),
627
997
  'data.exists': (handle, keyPtr, keyLen) => devDb.exists(ref, db, handle, keyPtr, keyLen),
628
- 'data.create': (handle, keyPtr, keyLen, valPtr, valLen, _idemPtr) => devDb.create(ref, db, handle, keyPtr, keyLen, valPtr, valLen),
629
- 'data.patch': (handle, keyPtr, keyLen, patchPtr, patchLen, _idemPtr) => devDb.patch(ref, db, handle, keyPtr, keyLen, patchPtr, patchLen),
630
- 'data.delete': (handle, keyPtr, keyLen, _idemPtr) => devDb.delete(ref, db, handle, keyPtr, keyLen),
631
- 'data.get_delete': (handle, keyPtr, keyLen, _idemPtr) => devDb.getDelete(ref, db, handle, keyPtr, keyLen),
998
+ 'data.create': (handle, keyPtr, keyLen, valPtr, valLen, idemPtr) => devDb.create(ref, db, handle, keyPtr, keyLen, valPtr, valLen, idemPtr),
999
+ 'data.patch': (handle, keyPtr, keyLen, patchPtr, patchLen, idemPtr) => devDb.patch(ref, db, handle, keyPtr, keyLen, patchPtr, patchLen, idemPtr),
1000
+ 'data.delete': (handle, keyPtr, keyLen, idemPtr) => devDb.delete(ref, db, handle, keyPtr, keyLen, idemPtr),
1001
+ 'data.get_delete': (handle, keyPtr, keyLen, idemPtr) => devDb.getDelete(ref, db, handle, keyPtr, keyLen, idemPtr),
632
1002
  'data.unique_lookup': (handle, keyPtr, keyLen) => devDb.uniqueLookup(ref, db, handle, keyPtr, keyLen),
633
- 'data.unique_claim': (handle, keyPtr, keyLen, valPtr, valLen, _idemPtr) => devDb.uniqueClaim(ref, db, handle, keyPtr, keyLen, valPtr, valLen),
1003
+ 'data.unique_claim': (handle, keyPtr, keyLen, valPtr, valLen, idemPtr) => devDb.uniqueClaim(ref, db, handle, keyPtr, keyLen, valPtr, valLen, idemPtr),
634
1004
  'data.unique_release': (handle, keyPtr, keyLen, valPtr, valLen, _idemPtr) => devDb.uniqueRelease(ref, db, handle, keyPtr, keyLen, valPtr, valLen),
635
1005
  'data.membership_contains': (handle, setPtr, setLen, memberPtr, memberLen) => devDb.membershipContains(ref, db, handle, setPtr, setLen, memberPtr, memberLen),
636
1006
  'data.membership_add': (handle, setPtr, setLen, memberPtr, memberLen, _idemPtr) => devDb.membershipAdd(ref, db, handle, setPtr, setLen, memberPtr, memberLen),
@@ -639,19 +1009,19 @@ export function buildDatabaseImports(ref, db) {
639
1009
  'data.view_get': (handle, keyPtr, keyLen) => devDb.viewGet(ref, db, handle, keyPtr, keyLen),
640
1010
  'data.view_publish': (handle, keyPtr, keyLen, valPtr, valLen, _idemPtr) => devDb.viewPublish(ref, db, handle, keyPtr, keyLen, valPtr, valLen),
641
1011
  'data.counter_get': (handle, keyPtr, keyLen) => devDb.counterGet(ref, db, handle, keyPtr, keyLen),
642
- 'data.counter_add': (handle, keyPtr, keyLen, delta, _idemPtr) => devDb.counterAdd(ref, db, handle, keyPtr, keyLen, delta),
643
- 'data.append': (handle, keyPtr, keyLen, evPtr, evLen, _idemPtr) => devDb.append(ref, db, handle, keyPtr, keyLen, evPtr, evLen),
1012
+ 'data.counter_add': (handle, keyPtr, keyLen, delta, idemPtr) => devDb.counterAdd(ref, db, handle, keyPtr, keyLen, delta, idemPtr),
1013
+ 'data.append': (handle, keyPtr, keyLen, evPtr, evLen, idemPtr) => devDb.append(ref, db, handle, keyPtr, keyLen, evPtr, evLen, idemPtr),
644
1014
  'data.append_once': (handle, keyPtr, keyLen, evidPtr, evidLen, evPtr, evLen) => devDb.appendOnce(ref, db, handle, keyPtr, keyLen, evidPtr, evidLen, evPtr, evLen),
645
- 'data.enqueue': (handle, keyPtr, keyLen, valPtr, valLen, _idemPtr) => devDb.enqueue(ref, db, handle, keyPtr, keyLen, valPtr, valLen),
1015
+ 'data.enqueue': (handle, keyPtr, keyLen, valPtr, valLen, idemPtr) => devDb.enqueue(ref, db, handle, keyPtr, keyLen, valPtr, valLen, idemPtr),
646
1016
  'data.latest': (handle, keyPtr, keyLen, limit) => devDb.latest(ref, db, handle, keyPtr, keyLen, limit),
647
1017
  'data.capacity_set_total': (handle, keyPtr, keyLen, total, _idemPtr) => devDb.capacitySetTotal(ref, db, handle, keyPtr, keyLen, total),
648
1018
  'data.capacity_available': (handle, keyPtr, keyLen) => devDb.capacityAvailable(ref, db, handle, keyPtr, keyLen),
649
- 'data.capacity_reserve': (handle, keyPtr, keyLen, amount, ttlMs, _idemPtr) => devDb.capacityReserve(ref, db, handle, keyPtr, keyLen, amount, ttlMs),
1019
+ 'data.capacity_reserve': (handle, keyPtr, keyLen, amount, ttlMs, idemPtr) => devDb.capacityReserve(ref, db, handle, keyPtr, keyLen, amount, ttlMs, idemPtr),
650
1020
  'data.capacity_confirm': (handle, keyPtr, keyLen, reservationId, _idemPtr) => devDb.capacityConfirm(ref, db, handle, keyPtr, keyLen, reservationId),
651
1021
  'data.capacity_cancel': (handle, keyPtr, keyLen, reservationId, _idemPtr) => devDb.capacityCancel(ref, db, handle, keyPtr, keyLen, reservationId),
652
1022
  'data.take_result': (outPtr, outCap) => devDb.takeResult(ref, db, outPtr, outCap),
653
1023
  'data.result_schema_version': () => devDb.resultSchemaVersion(db),
654
- 'data.write_allowed': () => 1,
1024
+ 'data.write_allowed': () => (kindAllows(db.functionKind, DbOp.Patch) ? 1 : 0),
655
1025
  };
656
1026
  }
657
1027
  export function __resetDbForTests() {