@gaganref/confect-test 10.3.1-fork.91 → 10.3.1-fork.93

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 (51) hide show
  1. package/dist/TestConfect.d.ts +26 -0
  2. package/dist/TestConfect.d.ts.map +1 -1
  3. package/dist/TestConfect.js +9 -1
  4. package/dist/TestConfect.js.map +1 -1
  5. package/dist/internal/Attempt.d.ts +14 -1
  6. package/dist/internal/Attempt.d.ts.map +1 -1
  7. package/dist/internal/Attempt.js +18 -1
  8. package/dist/internal/Attempt.js.map +1 -1
  9. package/dist/internal/ConvexTest.d.ts +26 -1
  10. package/dist/internal/ConvexTest.d.ts.map +1 -1
  11. package/dist/internal/ConvexTest.js +55 -27
  12. package/dist/internal/ConvexTest.js.map +1 -1
  13. package/dist/internal/Execution.d.ts +151 -1
  14. package/dist/internal/Execution.d.ts.map +1 -1
  15. package/dist/internal/Execution.js +731 -214
  16. package/dist/internal/Execution.js.map +1 -1
  17. package/dist/internal/KernelQuery.d.ts +8 -3
  18. package/dist/internal/KernelQuery.d.ts.map +1 -1
  19. package/dist/internal/KernelQuery.js +10 -3
  20. package/dist/internal/KernelQuery.js.map +1 -1
  21. package/dist/internal/KernelRefusal.d.ts +134 -0
  22. package/dist/internal/KernelRefusal.d.ts.map +1 -1
  23. package/dist/internal/KernelRefusal.js +134 -1
  24. package/dist/internal/KernelRefusal.js.map +1 -1
  25. package/dist/internal/Metering.d.ts +186 -0
  26. package/dist/internal/Metering.d.ts.map +1 -0
  27. package/dist/internal/Metering.js +401 -0
  28. package/dist/internal/Metering.js.map +1 -0
  29. package/dist/internal/Redaction.d.ts +142 -0
  30. package/dist/internal/Redaction.d.ts.map +1 -0
  31. package/dist/internal/Redaction.js +448 -0
  32. package/dist/internal/Redaction.js.map +1 -0
  33. package/dist/internal/Snapshot.d.ts +298 -0
  34. package/dist/internal/Snapshot.d.ts.map +1 -0
  35. package/dist/internal/Snapshot.js +532 -0
  36. package/dist/internal/Snapshot.js.map +1 -0
  37. package/dist/internal/Subscriptions.d.ts +13 -0
  38. package/dist/internal/Subscriptions.d.ts.map +1 -1
  39. package/dist/internal/Subscriptions.js +27 -3
  40. package/dist/internal/Subscriptions.js.map +1 -1
  41. package/package.json +3 -3
  42. package/src/TestConfect.ts +63 -4
  43. package/src/internal/Attempt.ts +24 -0
  44. package/src/internal/ConvexTest.ts +135 -38
  45. package/src/internal/Execution.ts +1141 -324
  46. package/src/internal/KernelQuery.ts +17 -6
  47. package/src/internal/KernelRefusal.ts +160 -0
  48. package/src/internal/Metering.ts +520 -0
  49. package/src/internal/Redaction.ts +730 -0
  50. package/src/internal/Snapshot.ts +945 -0
  51. package/src/internal/Subscriptions.ts +34 -4
@@ -1,15 +1,19 @@
1
1
  import { own } from "./Globals.js";
2
2
  import { install } from "./Clock.js";
3
- import { createFunctionHandle, createTestJwt, deserializeConvexErrorData, evaluateValue, hasPendingValues, parseArgs, parseFunctionHandle, resolveCommittedValues, simpleHash, tableNameFromId, validateFunctionArgs, validateReturnValue } from "./KernelValues.js";
3
+ import { createFunctionHandle, createTestJwt, deserializeConvexErrorData, evaluateValue, hasPendingValues, isSimpleObject, parseArgs, parseFunctionHandle, resolveCommittedValues, simpleHash, tableNameFromId, validateFunctionArgs, validateReturnValue } from "./KernelValues.js";
4
4
  import { apply, conflict, empty, wallClock } from "./Kernel.js";
5
+ import { defaults, everything, examined, fromBudget, point } from "./Metering.js";
5
6
  import { KernelAttempt } from "./Attempt.js";
6
7
  import { bind, current as current$1, fresh, unbound } from "./Invocation.js";
7
- import { ClosedAttempt, CommitTsNotIncreasing, DrainBudgetExceeded, OptimisticConcurrencyControlFailure, RegisteredAfterOpen, StaleAttempt, UnknownStandingSource, requiresBinary, rowPending } from "./KernelRefusal.js";
8
+ import { ClosedAttempt, CommitTsNotIncreasing, DrainBudgetExceeded, OptimisticConcurrencyControlFailure, RegisteredAfterOpen, SnapshotComponentMismatch, SnapshotJobInconsistent, SnapshotSubscribed, SnapshotWorldBusy, SnapshotWorldRunning, SnapshotWorldUnresolved, StaleAttempt, UnknownStandingSource, requiresBinary, rowPending } from "./KernelRefusal.js";
8
9
  import { exportSchema, paginate, stream, validateDocument } from "./KernelQuery.js";
10
+ import { Secrets, failure, keep, mark, markValue, marker, none, sensitiveAt, unmark } from "./Redaction.js";
9
11
  import { functions, pending, row, stateOf, table } from "./Scheduling.js";
12
+ import { check, contract, decode, encode } from "./Snapshot.js";
10
13
  import { actionGeneric, getFunctionAddress, mutationGeneric, queryGeneric } from "convex/server";
11
14
  import { AsyncLocalStorage } from "node:async_hooks";
12
- import { convexToJson, jsonToConvex } from "convex/values";
15
+ import { CommitTsPlaceholder, compareValues, convexToJson, jsonToConvex } from "convex/values";
16
+ import * as Result from "effect/Result";
13
17
  //#region src/internal/Execution.ts
14
18
  const ROOT = "";
15
19
  const TYPICAL_CONVEX_ID_LENGTH = 32;
@@ -97,6 +101,23 @@ const pinnedTime = () => {
97
101
  * `finishAllScheduledFunctions` defaults to.
98
102
  */
99
103
  const defaultBudget = 100;
104
+ const unresolvedWorld = (name, reason) => new SnapshotWorldUnresolved({
105
+ name,
106
+ reason,
107
+ message: reason === "missing" ? `A snapshot job runs under the test world "${name}", which no world passed to restore is named. Pass that world.` : `Two different test worlds are named "${name}", so a snapshot cannot tell their jobs apart. Name each world uniquely.`
108
+ });
109
+ /**
110
+ * `shape` placed at `at`, or no placement when there is no shape.
111
+ */
112
+ const placed = (shape, at = []) => shape === void 0 ? [] : [{
113
+ at,
114
+ shape
115
+ }];
116
+ const running = (registry, count, what) => new SnapshotWorldRunning({
117
+ registry,
118
+ count,
119
+ message: `The test backend is running ${count} ${what}, whose effects a snapshot would split. Await them first.`
120
+ });
100
121
  const current = () => {
101
122
  const execution = executions.getStore();
102
123
  if (execution === void 0) throw new Error("A Convex syscall ran outside any function the test kernel started.");
@@ -190,6 +211,7 @@ const kindCheck = {
190
211
  */
191
212
  var World = class {
192
213
  runtime;
214
+ redaction;
193
215
  components = /* @__PURE__ */ new Map();
194
216
  state = empty;
195
217
  opened;
@@ -205,8 +227,31 @@ var World = class {
205
227
  */
206
228
  floor = Number.NEGATIVE_INFINITY;
207
229
  targets = /* @__PURE__ */ new Map();
208
- constructor(schema, modules, runtime = wallClock()) {
230
+ /**
231
+ * The raw secrets this world has seen under a secret schema node. Replay
232
+ * state, kept in memory and never written to an artifact.
233
+ */
234
+ secrets = new Secrets();
235
+ /**
236
+ * The live registries `Snapshot.contract.live` names, each a count: calls
237
+ * from the test, scheduler walks, function executions (nested and detached
238
+ * ones included) and registered subscribers. Held attempts are `holding`.
239
+ */
240
+ live = {
241
+ running: 0,
242
+ walks: 0,
243
+ functions: 0,
244
+ subscribers: 0
245
+ };
246
+ /**
247
+ * The latest identity version a subscriber's `authenticate` reached. Each
248
+ * subscriber starts at the epoch, and each identity change advances it, so a
249
+ * restored world never hands out a version its source already used.
250
+ */
251
+ identityEpoch = 0;
252
+ constructor(schema, modules, runtime = wallClock(), redaction = none) {
209
253
  this.runtime = runtime;
254
+ this.redaction = redaction;
210
255
  installConvexGlobal();
211
256
  install(pinnedTime, () => executions.getStore() !== void 0);
212
257
  this.components.set(ROOT, makeComponent(ROOT, schema, modules));
@@ -272,66 +317,74 @@ var World = class {
272
317
  };
273
318
  }
274
319
  query(caller, reference, args) {
275
- return this.open().then(() => {
276
- const from = {
277
- caller,
278
- request: newRequest(caller)
279
- };
280
- return typeof reference === "function" ? this.runQuery({
281
- componentPath: ROOT,
282
- udfPath: "inline"
283
- }, {
284
- handler: reference,
285
- args: { type: "any" },
286
- returns: null
287
- }, args ?? {}, from) : this.queryAt(pathOfReference(reference, ROOT), args, from);
320
+ return this.during(() => {
321
+ return this.open().then(() => {
322
+ const from = {
323
+ caller,
324
+ request: newRequest(caller)
325
+ };
326
+ return typeof reference === "function" ? this.runQuery({
327
+ componentPath: ROOT,
328
+ udfPath: "inline"
329
+ }, {
330
+ handler: reference,
331
+ args: { type: "any" },
332
+ returns: null
333
+ }, args ?? {}, from) : this.queryAt(pathOfReference(reference, ROOT), args, from);
334
+ });
288
335
  });
289
336
  }
290
337
  mutation(caller, reference, args) {
291
- return this.open().then(() => {
292
- const from = {
293
- caller,
294
- request: newRequest(caller)
295
- };
296
- return typeof reference === "function" ? this.runMutation({
297
- componentPath: ROOT,
298
- udfPath: "inline"
299
- }, {
300
- handler: reference,
301
- args: { type: "any" },
302
- returns: null
303
- }, args ?? {}, from) : this.mutationAt(pathOfReference(reference, ROOT), args, from);
338
+ return this.during(() => {
339
+ return this.open().then(() => {
340
+ const from = {
341
+ caller,
342
+ request: newRequest(caller)
343
+ };
344
+ return typeof reference === "function" ? this.runMutation({
345
+ componentPath: ROOT,
346
+ udfPath: "inline"
347
+ }, {
348
+ handler: reference,
349
+ args: { type: "any" },
350
+ returns: null
351
+ }, args ?? {}, from) : this.mutationAt(pathOfReference(reference, ROOT), args, from);
352
+ });
304
353
  });
305
354
  }
306
355
  action(caller, reference, args) {
307
- return this.open().then(() => {
308
- const from = {
309
- caller,
310
- request: newRequest(caller)
311
- };
312
- return typeof reference === "function" ? this.runAction({
313
- componentPath: ROOT,
314
- udfPath: "inline"
315
- }, {
316
- handler: reference,
317
- args: { type: "any" },
318
- returns: null
319
- }, args ?? {}, from) : this.actionAt(pathOfReference(reference, ROOT), args, from);
356
+ return this.during(() => {
357
+ return this.open().then(() => {
358
+ const from = {
359
+ caller,
360
+ request: newRequest(caller)
361
+ };
362
+ return typeof reference === "function" ? this.runAction({
363
+ componentPath: ROOT,
364
+ udfPath: "inline"
365
+ }, {
366
+ handler: reference,
367
+ args: { type: "any" },
368
+ returns: null
369
+ }, args ?? {}, from) : this.actionAt(pathOfReference(reference, ROOT), args, from);
370
+ });
320
371
  });
321
372
  }
322
373
  run(caller, componentPath, handler) {
323
- return this.open().then(() => {
324
- this.component(componentPath);
325
- return this.runMutation({
326
- componentPath,
327
- udfPath: "custom"
328
- }, {
329
- handler,
330
- args: { type: "any" },
331
- returns: null
332
- }, {}, {
333
- caller,
334
- request: newRequest(caller)
374
+ return this.during(() => {
375
+ return this.open().then(() => {
376
+ this.component(componentPath);
377
+ return this.runMutation({
378
+ componentPath,
379
+ udfPath: "custom"
380
+ }, {
381
+ handler,
382
+ args: { type: "any" },
383
+ returns: null
384
+ }, {}, {
385
+ caller,
386
+ request: newRequest(caller)
387
+ });
335
388
  });
336
389
  });
337
390
  }
@@ -339,22 +392,27 @@ var World = class {
339
392
  * Run a mutation to its commit point and hold it as attempt 1.
340
393
  */
341
394
  prepare(caller, reference, args) {
342
- return this.open().then(() => {
343
- const functionPath = pathOfReference(reference, ROOT);
344
- const resolved = this.resolve(functionPath, "mutation");
345
- validateFunctionArgs(resolved.args, args ?? {}, functionPath, false);
346
- return this.prepareMutation(functionPath, resolved, args, {
347
- caller,
348
- request: newRequest(caller)
349
- }, 1, current$1()).then((held) => held.view);
395
+ return this.during(() => {
396
+ return this.open().then(() => {
397
+ const functionPath = pathOfReference(reference, ROOT);
398
+ const resolved = this.resolve(functionPath, "mutation");
399
+ this.keepArgs(functionPath, args);
400
+ validateFunctionArgs(resolved.args, args ?? {}, functionPath, false);
401
+ return this.prepareMutation(functionPath, resolved, args, {
402
+ caller,
403
+ request: newRequest(caller)
404
+ }, 1, current$1()).then((held) => held.view);
405
+ });
350
406
  });
351
407
  }
352
408
  apply(id) {
353
- const held = this.holding.get(id);
354
- return held === void 0 ? Promise.reject(new StaleAttempt({
355
- id,
356
- message: `The test kernel holds no attempt "${id}". It was already applied, or never prepared.`
357
- })) : this.applyHeld(held);
409
+ return this.during(() => {
410
+ const held = this.holding.get(id);
411
+ return held === void 0 ? Promise.reject(new StaleAttempt({
412
+ id,
413
+ message: `The test kernel holds no attempt "${id}". It was already applied, or never prepared.`
414
+ })) : this.applyHeld(held);
415
+ });
358
416
  }
359
417
  /**
360
418
  * The held attempts, oldest first.
@@ -369,37 +427,226 @@ var World = class {
369
427
  * read where an earlier run's ended.
370
428
  */
371
429
  subscribed(caller, reference, args, pinned) {
372
- return this.open().then(() => {
373
- const functionPath = pathOfReference(reference, ROOT);
374
- const resolved = this.resolve(functionPath, "query");
375
- validateFunctionArgs(resolved.args, args ?? {}, functionPath, false);
376
- const top = {
377
- attempt: new KernelAttempt(this.state),
378
- pagination: {
379
- calls: 0,
380
- journal: pinned.journal,
381
- end: null
382
- },
383
- startTime: pinned.time
384
- };
385
- const run = (outcome) => ({
386
- outcome,
387
- snapshot: top.attempt.snapshot.version,
388
- reads: top.attempt.reads,
389
- observedIdentity: top.attempt.observedIdentity,
390
- time: top.attempt.observedTime,
391
- journal: pinned.journal ?? top.pagination.end
430
+ return this.during(() => {
431
+ return this.open().then(() => {
432
+ const functionPath = pathOfReference(reference, ROOT);
433
+ const resolved = this.resolve(functionPath, "query");
434
+ this.keepArgs(functionPath, args);
435
+ validateFunctionArgs(resolved.args, args ?? {}, functionPath, false);
436
+ const top = {
437
+ attempt: new KernelAttempt(this.state),
438
+ pagination: {
439
+ calls: 0,
440
+ journal: pinned.journal,
441
+ end: null
442
+ },
443
+ startTime: pinned.time
444
+ };
445
+ const run = (outcome) => ({
446
+ outcome,
447
+ snapshot: top.attempt.snapshot.version,
448
+ reads: top.attempt.reads,
449
+ observedIdentity: top.attempt.observedIdentity,
450
+ time: top.attempt.observedTime,
451
+ journal: pinned.journal ?? top.pagination.end
452
+ });
453
+ return this.runQuery(functionPath, resolved, args, {
454
+ caller,
455
+ request: newRequest(caller)
456
+ }, false, top).then((value) => run({
457
+ _tag: "returned",
458
+ value
459
+ }), (error) => run({
460
+ _tag: "failed",
461
+ error
462
+ }));
392
463
  });
393
- return this.runQuery(functionPath, resolved, args, {
394
- caller,
395
- request: newRequest(caller)
396
- }, false, top).then((value) => run({
397
- _tag: "returned",
398
- value
399
- }), (error) => run({
400
- _tag: "failed",
401
- error
402
- }));
464
+ });
465
+ }
466
+ /**
467
+ * Run `call`, a call from the test, counted as running until it settles.
468
+ */
469
+ during(call) {
470
+ return this.track("running", call);
471
+ }
472
+ /**
473
+ * Run `work` counted in the live registry `registry` until it settles.
474
+ */
475
+ track(registry, work) {
476
+ this.live[registry]++;
477
+ const settle = () => {
478
+ this.live[registry]--;
479
+ };
480
+ try {
481
+ return work().finally(settle);
482
+ } catch (error) {
483
+ settle();
484
+ throw error;
485
+ }
486
+ }
487
+ /**
488
+ * How many entries each live registry holds now.
489
+ */
490
+ liveCounts() {
491
+ return {
492
+ running: this.live.running,
493
+ walks: this.live.walks,
494
+ functions: this.live.functions,
495
+ held: this.holding.size,
496
+ subscribers: this.live.subscribers
497
+ };
498
+ }
499
+ /**
500
+ * Count a subscriber the shim's registry created. Subscribers live as long as
501
+ * their test, so the count never falls.
502
+ */
503
+ registerSubscriber() {
504
+ this.live.subscribers++;
505
+ }
506
+ /**
507
+ * The identity version a new subscriber starts at.
508
+ */
509
+ currentIdentityEpoch() {
510
+ return this.identityEpoch;
511
+ }
512
+ /**
513
+ * The next identity version, for a subscriber whose identity changed.
514
+ */
515
+ nextIdentityEpoch() {
516
+ return ++this.identityEpoch;
517
+ }
518
+ /**
519
+ * The committed state as snapshot bytes: every component's rows and the
520
+ * version that wrote each, the write log, the jobs and their targets, the
521
+ * clock's floor and the identity counters. Each job's world is recorded by
522
+ * name. Two exports of the same state are the same bytes. A world running a
523
+ * call or holding attempts refuses, since a snapshot carries neither.
524
+ */
525
+ exportSnapshot() {
526
+ this.refuseBusy(0, "export");
527
+ const names = /* @__PURE__ */ new Map();
528
+ const jobs = /* @__PURE__ */ new Map();
529
+ for (const byTable of this.state.tables.values()) for (const id of byTable.get("_scheduled_functions")?.keys() ?? []) {
530
+ const target = this.targets.get(id);
531
+ if (target === void 0) continue;
532
+ const world = target.world;
533
+ if (world !== void 0) {
534
+ if ((names.get(world.name) ?? world) !== world) throw unresolvedWorld(world.name, "ambiguous");
535
+ names.set(world.name, world);
536
+ }
537
+ jobs.set(id, {
538
+ functionPath: target.functionPath,
539
+ standing: target.standing,
540
+ world: world?.name ?? null
541
+ });
542
+ }
543
+ return encode({
544
+ components: [...this.components.keys()],
545
+ state: this.state,
546
+ floor: this.floor,
547
+ identity: {
548
+ nextDocumentId: this.nextDocId,
549
+ lastCreationTime: this.lastCreationTime,
550
+ nextAttemptId: this.nextAttemptId,
551
+ identityEpoch: this.identityEpoch
552
+ },
553
+ jobs
554
+ }, (componentPath, table, json) => mark(json, this.documentPlacements(componentPath, table, json), this.secrets));
555
+ }
556
+ /**
557
+ * Replace this world's state with a snapshot's. The world must register the
558
+ * snapshot's components, run nothing, and have committed nothing and hold
559
+ * nothing. Each job's world resolves by name among `worlds`. Bytes whose
560
+ * digest does not match, and state the kernel could not have reached, refuse
561
+ * before anything changes.
562
+ */
563
+ restoreSnapshot(bytes, options = {}) {
564
+ return this.open().then(() => this.restore(bytes, options.worlds ?? [], options.secrets ?? new Secrets()));
565
+ }
566
+ restore(bytes, worlds, secrets) {
567
+ this.refuseBusy(this.state.version, "restore");
568
+ const decoded = decode(bytes, (json) => unmark(json, secrets));
569
+ if (Result.isFailure(decoded)) throw decoded.failure;
570
+ const image = decoded.success;
571
+ const checked = check(image, table);
572
+ if (Result.isFailure(checked)) throw checked.failure;
573
+ const registered = [...this.components.keys()].sort();
574
+ const snapshot = [...image.components].sort();
575
+ if (registered.join("\0") !== snapshot.join("\0")) throw new SnapshotComponentMismatch({
576
+ snapshot,
577
+ registered,
578
+ message: `The snapshot holds components ${snapshot.map((p) => `"${p}"`).join(", ")}, but this test backend registers ${registered.map((p) => `"${p}"`).join(", ")}. Register the same components before restoring.`
579
+ });
580
+ for (const [id, { functionPath }] of image.jobs) if (!this.exports(functionPath)) throw new SnapshotJobInconsistent({
581
+ id,
582
+ reason: "function",
583
+ message: `The snapshot's job ${id} targets ${functionPath.udfPath} in "${functionPath.componentPath}", which that component does not export.`
584
+ });
585
+ const byName = /* @__PURE__ */ new Map();
586
+ for (const world of worlds) {
587
+ if ((byName.get(world.name) ?? world) !== world) throw unresolvedWorld(world.name, "ambiguous");
588
+ byName.set(world.name, world);
589
+ }
590
+ const targets = Array.from(image.jobs, ([id, { functionPath, standing, world }]) => {
591
+ const resolved = world === null ? void 0 : byName.get(world);
592
+ if (world !== null && resolved === void 0) throw unresolvedWorld(world, "missing");
593
+ return [id, {
594
+ functionPath,
595
+ standing,
596
+ world: resolved
597
+ }];
598
+ });
599
+ this.state = image.state;
600
+ this.floor = image.floor;
601
+ this.nextDocId = image.identity.nextDocumentId;
602
+ this.lastCreationTime = image.identity.lastCreationTime;
603
+ this.nextAttemptId = image.identity.nextAttemptId;
604
+ this.identityEpoch = image.identity.identityEpoch;
605
+ this.targets.clear();
606
+ for (const [id, target] of targets) this.targets.set(id, target);
607
+ this.secrets = secrets.copy();
608
+ }
609
+ /**
610
+ * Whether `functionPath`'s component exports a Convex function there.
611
+ */
612
+ exports({ componentPath, udfPath }) {
613
+ const [modulePath, exportName = "default"] = udfPath.split(":");
614
+ const func = this.components.get(componentPath)?.loaded.get(modulePath)?.[exportName];
615
+ return func?.isQuery === true || func?.isMutation === true || func?.isAction === true;
616
+ }
617
+ /**
618
+ * Refuse a snapshot while any live registry `operation` waits on is
619
+ * non-empty, or, restoring, once `version` shows the world committed
620
+ * something of its own.
621
+ */
622
+ refuseBusy(version, operation) {
623
+ const counts = this.liveCounts();
624
+ const refusals = {
625
+ running: (count) => running("running", count, "calls from the test"),
626
+ walks: (count) => running("walks", count, "scheduler walks"),
627
+ functions: (count) => running("functions", count, "functions, nested and detached ones included"),
628
+ held: () => {
629
+ const held = [...this.holding.keys()];
630
+ return new SnapshotWorldBusy({
631
+ version,
632
+ held,
633
+ message: `The test backend holds attempts ${held.join(", ")}, which a snapshot does not carry. Apply them first.`
634
+ });
635
+ },
636
+ subscribers: (count) => new SnapshotSubscribed({
637
+ subscribers: count,
638
+ message: `The test backend has ${count} subscribers, whose identity versions a restore would rewind. Restore into a test backend no client has subscribed to.`
639
+ })
640
+ };
641
+ for (const key of Object.keys(refusals)) {
642
+ const { blocks } = contract.live[key];
643
+ if (operation === "export" && blocks === "restore") continue;
644
+ if (counts[key] > 0) throw refusals[key](counts[key]);
645
+ }
646
+ if (operation === "restore" && version !== 0) throw new SnapshotWorldBusy({
647
+ version,
648
+ held: [],
649
+ message: `The test backend has committed ${version} versions of its own. Restore a snapshot into a fresh test backend.`
403
650
  });
404
651
  }
405
652
  /**
@@ -421,9 +668,11 @@ var World = class {
421
668
  * crosses no future deadline.
422
669
  */
423
670
  advance(ms, budget = defaultBudget) {
424
- return this.open().then(() => {
425
- const until = this.now() + ms;
426
- return this.walk((pending) => pending.filter((job) => job.scheduledTime <= until), budget).then(() => this.raiseFloor(until));
671
+ return this.track("walks", () => {
672
+ return this.open().then(() => {
673
+ const until = this.now() + ms;
674
+ return this.walk((pending) => pending.filter((job) => job.scheduledTime <= until), budget).then(() => this.raiseFloor(until));
675
+ });
427
676
  });
428
677
  }
429
678
  /**
@@ -431,7 +680,9 @@ var World = class {
431
680
  * standing jobs are pending. Every job run counts against `budget`.
432
681
  */
433
682
  drain(budget = defaultBudget) {
434
- return this.open().then(() => this.walk((pending) => pending.filter((job) => !job.standing), budget));
683
+ return this.track("walks", () => {
684
+ return this.open().then(() => this.walk((pending) => pending.filter((job) => !job.standing), budget));
685
+ });
435
686
  }
436
687
  /**
437
688
  * Run the first of `select(pending)` until it is empty, moving the clock to
@@ -532,8 +783,14 @@ var World = class {
532
783
  const target = functionPathOf(args, execution.componentPath);
533
784
  this.component(target.componentPath);
534
785
  const jobArgs = jsonToConvex(args.args);
786
+ this.keepArgs(target, jobArgs);
535
787
  if (hasPendingValues(jobArgs)) throw new Error(`Invalid arguments for "${target.udfPath}": a scheduled function cannot take an unresolved placeholder value.`);
788
+ refuse(attempt.meter.schedule(jobArgs, execution.limits));
789
+ const jobsIndex = indexKey(execution.componentPath, table, "by_id");
790
+ const parentJob = execution.request.scheduledFunctionId;
791
+ if (parentJob !== null) refuse(attempt.meter.readInterval(jobsIndex, point([parentJob]), execution.limits));
536
792
  const id = this.generateId(table);
793
+ attempt.meter.deriveInterval(jobsIndex, point([id]));
537
794
  attempt.write(execution.componentPath, id, row(id, this.creationTime(execution.startTime), target.udfPath, jobArgs, args.ts * 1e3));
538
795
  this.targets.set(id, {
539
796
  functionPath: target,
@@ -548,10 +805,10 @@ var World = class {
548
805
  /**
549
806
  * Cancel a job that has not finished. A finished job stays as it ended.
550
807
  */
551
- cancelJob(attempt, componentPath, id) {
808
+ cancelJob(attempt, execution, id) {
809
+ const { componentPath } = execution;
552
810
  validateId(table, id);
553
- attempt.readDocument(componentPath, id);
554
- const doc = attempt.get(componentPath, id);
811
+ const doc = this.readById(attempt, execution, id);
555
812
  if (doc === null) throw new Error(`Patch on non-existent document with ID "${id}"`);
556
813
  const { kind } = stateOf(doc);
557
814
  if (kind === "pending" || kind === "inProgress") attempt.write(componentPath, id, {
@@ -564,21 +821,26 @@ var World = class {
564
821
  }
565
822
  queryAt(functionPath, args, from, snapshotOnly = false) {
566
823
  const resolved = this.resolve(functionPath, "query");
824
+ this.keepArgs(functionPath, args);
567
825
  validateFunctionArgs(resolved.args, args ?? {}, functionPath, from.parent?.attempt !== void 0);
568
826
  return this.runQuery(functionPath, resolved, args, from, snapshotOnly);
569
827
  }
570
828
  mutationAt(functionPath, args, from) {
571
829
  const resolved = this.resolve(functionPath, "mutation");
830
+ this.keepArgs(functionPath, args);
572
831
  validateFunctionArgs(resolved.args, args ?? {}, functionPath, from.parent?.attempt !== void 0);
573
832
  return this.runMutation(functionPath, resolved, args, from);
574
833
  }
575
834
  actionAt(functionPath, args, from) {
576
835
  const resolved = this.resolve(functionPath, "action");
836
+ this.keepArgs(functionPath, args);
577
837
  validateFunctionArgs(resolved.args, args ?? {}, functionPath, false);
578
838
  return this.runAction(functionPath, resolved, args, from);
579
839
  }
580
840
  execution(functionPath, udfType, attempt, from, pagination, startTime) {
581
841
  const identity = functionPath.componentPath !== ROOT ? null : from.parent !== void 0 ? from.parent.identity : from.caller.identity;
842
+ const inherited = attempt === void 0 || from.parent?.attempt === void 0 ? defaults : from.parent.limits;
843
+ const limits = attempt === void 0 || from.budget === void 0 ? inherited : fromBudget(from.budget, attempt.meter.used(), inherited);
582
844
  return {
583
845
  world: this,
584
846
  componentPath: functionPath.componentPath,
@@ -589,29 +851,33 @@ var World = class {
589
851
  request: from.request,
590
852
  startTime: startTime ?? (from.parent !== void 0 && from.parent.udfType !== "action" ? from.parent.startTime : this.now()),
591
853
  pagination,
592
- turn: new Turn()
854
+ turn: new Turn(),
855
+ limits
593
856
  };
594
857
  }
595
858
  runQuery(functionPath, resolved, args, from, snapshotOnly = false, top) {
596
- const parentAttempt = from.parent?.attempt;
597
- const isNested = parentAttempt !== void 0;
598
- const attempt = top?.attempt ?? (parentAttempt === void 0 ? new KernelAttempt(this.state) : snapshotOnly ? new KernelAttempt(parentAttempt.snapshot) : parentAttempt);
599
- const execution = this.execution(functionPath, "query", attempt, from, top?.pagination ?? (isNested && from.parent?.pagination || {
600
- calls: 0,
601
- journal: null,
602
- end: null
603
- }), top?.startTime);
604
- const q = queryGeneric({ handler: (ctx, a) => resolved.handler({
605
- ...ctx,
606
- auth: queryAuth(execution)
607
- }, a) });
608
- return enter(execution, () => q.invokeQuery(JSON.stringify(convexToJson([parseArgs(args)])))).then((raw) => settled(execution, raw)).then((raw) => {
609
- const result = jsonToConvex(JSON.parse(raw));
610
- if (!isNested && hasPendingValues(result)) throw new Error(`Function ${functionPath.udfPath} return value invalid: queries cannot return an unresolved placeholder value.`);
611
- validateReturnValue(resolved.returns, result, "query", functionPath);
612
- return result;
613
- }).catch((error) => {
614
- throw deserializeConvexErrorData(error);
859
+ return this.track("functions", () => {
860
+ const parentAttempt = from.parent?.attempt;
861
+ const isNested = parentAttempt !== void 0;
862
+ const attempt = top?.attempt ?? (parentAttempt === void 0 ? new KernelAttempt(this.state) : snapshotOnly ? parentAttempt.staleSnapshot() : parentAttempt);
863
+ const execution = this.execution(functionPath, "query", attempt, from, top?.pagination ?? (isNested && from.parent?.pagination || {
864
+ calls: 0,
865
+ journal: null,
866
+ end: null
867
+ }), top?.startTime);
868
+ const q = queryGeneric({ handler: (ctx, a) => resolved.handler({
869
+ ...ctx,
870
+ auth: queryAuth(execution)
871
+ }, a) });
872
+ return enter(execution, () => q.invokeQuery(JSON.stringify(convexToJson([parseArgs(args)])))).then((raw) => settled(execution, raw)).then((raw) => {
873
+ const result = jsonToConvex(JSON.parse(raw));
874
+ if (!isNested && hasPendingValues(result)) throw new Error(`Function ${functionPath.udfPath} return value invalid: queries cannot return an unresolved placeholder value.`);
875
+ this.keepReturns(functionPath, result);
876
+ validateReturnValue(resolved.returns, result, "query", functionPath);
877
+ return result;
878
+ }).catch((error) => {
879
+ throw deserializeConvexErrorData(error);
880
+ });
615
881
  });
616
882
  }
617
883
  invokeMutation(execution, resolved, args) {
@@ -621,28 +887,32 @@ var World = class {
621
887
  }, a) });
622
888
  return enter(execution, () => m.invokeMutation(JSON.stringify(convexToJson([parseArgs(args)])))).then((raw) => settled(execution, raw)).then((raw) => {
623
889
  const result = jsonToConvex(JSON.parse(raw));
624
- validateReturnValue(resolved.returns, result, "mutation", {
890
+ const functionPath = {
625
891
  componentPath: execution.componentPath,
626
892
  udfPath: execution.udfPath
627
- });
893
+ };
894
+ this.keepReturns(functionPath, result);
895
+ validateReturnValue(resolved.returns, result, "mutation", functionPath);
628
896
  return result;
629
897
  });
630
898
  }
631
899
  runMutation(functionPath, resolved, args, from) {
632
- const parentAttempt = from.parent?.attempt;
633
- if (parentAttempt !== void 0) {
634
- const frame = parentAttempt.child();
635
- const execution = this.execution(functionPath, "mutation", frame, from, void 0);
636
- return this.invokeMutation(execution, resolved, args).then((result) => frame.drained().then(() => {
637
- frame.close();
638
- frame.merge();
639
- return result;
640
- }), (error) => {
641
- frame.close();
642
- throw deserializeConvexErrorData(error);
643
- });
644
- }
645
- return this.prepareMutation(functionPath, resolved, args, from, 1, current$1()).then((held) => this.publish(held));
900
+ return this.track("functions", () => {
901
+ const parentAttempt = from.parent?.attempt;
902
+ if (parentAttempt !== void 0) {
903
+ const frame = parentAttempt.child();
904
+ const execution = this.execution(functionPath, "mutation", frame, from, void 0);
905
+ return this.invokeMutation(execution, resolved, args).then((result) => frame.drained().then(() => {
906
+ frame.close();
907
+ frame.merge();
908
+ return result;
909
+ }), (error) => {
910
+ frame.close();
911
+ throw deserializeConvexErrorData(error);
912
+ });
913
+ }
914
+ return this.prepareMutation(functionPath, resolved, args, from, 1, current$1()).then((held) => this.publish(held));
915
+ });
646
916
  }
647
917
  /**
648
918
  * Run one attempt of a top-level mutation against the latest commit, then
@@ -652,34 +922,37 @@ var World = class {
652
922
  * number.
653
923
  */
654
924
  prepareMutation(functionPath, resolved, args, from, number, bound) {
655
- const attempt = new KernelAttempt(this.state, number);
656
- const execution = this.execution(functionPath, "mutation", attempt, from, void 0);
657
- const run = () => this.invokeMutation(execution, resolved, args);
658
- return (bound === void 0 ? run() : bind({
659
- ...bound,
660
- attempt: number
661
- }, run)).then((value) => attempt.drained().then(() => ({
662
- _tag: "returned",
663
- value
664
- })), (error) => ({
665
- _tag: "failed",
666
- error: deserializeConvexErrorData(error)
667
- })).then((outcome) => {
668
- attempt.close();
669
- const held = {
670
- view: {
671
- id: `attempt-${this.nextAttemptId++}`,
672
- request: from.request.requestId,
673
- number,
674
- snapshot: attempt.snapshot.version,
675
- reads: attempt.reads,
676
- outcome
677
- },
678
- attempt,
679
- again: (next) => this.prepareMutation(functionPath, resolved, args, from, next, bound)
680
- };
681
- this.holding.set(held.view.id, held);
682
- return held;
925
+ return this.track("functions", () => {
926
+ const attempt = new KernelAttempt(this.state, number);
927
+ const execution = this.execution(functionPath, "mutation", attempt, from, void 0);
928
+ const run = () => this.invokeMutation(execution, resolved, args);
929
+ return (bound === void 0 ? run() : bind({
930
+ ...bound,
931
+ attempt: number
932
+ }, run)).then((value) => attempt.drained().then(() => ({
933
+ _tag: "returned",
934
+ value
935
+ })), (error) => ({
936
+ _tag: "failed",
937
+ error: deserializeConvexErrorData(error)
938
+ })).then((outcome) => {
939
+ attempt.close();
940
+ const held = {
941
+ view: {
942
+ id: `attempt-${this.nextAttemptId++}`,
943
+ functionPath,
944
+ request: from.request.requestId,
945
+ number,
946
+ snapshot: attempt.snapshot.version,
947
+ reads: attempt.reads,
948
+ outcome
949
+ },
950
+ attempt,
951
+ again: (next) => this.prepareMutation(functionPath, resolved, args, from, next, bound)
952
+ };
953
+ this.holding.set(held.view.id, held);
954
+ return held;
955
+ });
683
956
  });
684
957
  }
685
958
  /**
@@ -744,43 +1017,223 @@ var World = class {
744
1017
  });
745
1018
  }
746
1019
  runAction(functionPath, resolved, args, from) {
747
- const execution = this.execution(functionPath, "action", void 0, from, void 0);
748
- const inner = {
749
- request: from.request,
750
- parent: execution
1020
+ return this.track("functions", () => {
1021
+ const execution = this.execution(functionPath, "action", void 0, from, void 0);
1022
+ const inner = {
1023
+ request: from.request,
1024
+ parent: execution
1025
+ };
1026
+ const a = actionGeneric({ handler: (ctx, innerArgs) => resolved.handler({
1027
+ ...ctx,
1028
+ runQuery: (reference, queryArgs) => this.queryAt(pathOfReference(reference, functionPath.componentPath), queryArgs, inner),
1029
+ runMutation: (reference, mutationArgs) => this.mutationAt(pathOfReference(reference, functionPath.componentPath), mutationArgs, inner),
1030
+ runAction: (reference, actionArgs) => this.actionAt(pathOfReference(reference, functionPath.componentPath), actionArgs, inner),
1031
+ auth: queryAuth(execution)
1032
+ }, innerArgs) });
1033
+ const run = () => enter(execution, () => a.invokeAction(from.request.requestId, JSON.stringify(convexToJson([parseArgs(args)]))));
1034
+ const bound = from.parent === void 0 ? void 0 : current$1();
1035
+ return (bound === void 0 ? run() : bind(fresh(bound.world, functionPath.componentPath, from.request), run)).then((raw) => {
1036
+ const result = jsonToConvex(JSON.parse(raw));
1037
+ this.keepReturns(functionPath, result);
1038
+ validateReturnValue(resolved.returns, result, "action", functionPath);
1039
+ return result;
1040
+ }).catch((error) => {
1041
+ throw deserializeConvexErrorData(error);
1042
+ });
1043
+ });
1044
+ }
1045
+ keepArgs(functionPath, args) {
1046
+ keep(args ?? {}, placed(this.redaction.args(functionPath)), this.secrets);
1047
+ }
1048
+ keepReturns(functionPath, result) {
1049
+ keep(result, placed(this.redaction.returns(functionPath)), this.secrets);
1050
+ }
1051
+ keepRow(componentPath, table, value) {
1052
+ keep(value, placed(this.redaction.table(componentPath, table)), this.secrets);
1053
+ }
1054
+ /**
1055
+ * Where a stored document holds secrets: its table's shape at its root, and
1056
+ * for a job's row, its target's argument shape at `args.0`.
1057
+ */
1058
+ documentPlacements(componentPath, table$1, doc) {
1059
+ const own = placed(this.redaction.table(componentPath, table$1));
1060
+ if (table$1 !== "_scheduled_functions") return own;
1061
+ const target = this.targets.get(doc._id)?.functionPath ?? {
1062
+ componentPath,
1063
+ udfPath: String(doc.name)
751
1064
  };
752
- const a = actionGeneric({ handler: (ctx, innerArgs) => resolved.handler({
753
- ...ctx,
754
- runQuery: (reference, queryArgs) => this.queryAt(pathOfReference(reference, functionPath.componentPath), queryArgs, inner),
755
- runMutation: (reference, mutationArgs) => this.mutationAt(pathOfReference(reference, functionPath.componentPath), mutationArgs, inner),
756
- runAction: (reference, actionArgs) => this.actionAt(pathOfReference(reference, functionPath.componentPath), actionArgs, inner),
757
- auth: queryAuth(execution)
758
- }, innerArgs) });
759
- const run = () => enter(execution, () => a.invokeAction(from.request.requestId, JSON.stringify(convexToJson([parseArgs(args)]))));
760
- const bound = from.parent === void 0 ? void 0 : current$1();
761
- return (bound === void 0 ? run() : bind(fresh(bound.world, functionPath.componentPath, from.request), run)).then((raw) => {
762
- const result = jsonToConvex(JSON.parse(raw));
763
- validateReturnValue(resolved.returns, result, "action", functionPath);
764
- return result;
765
- }).catch((error) => {
766
- throw deserializeConvexErrorData(error);
1065
+ return own.concat(placed(this.redaction.args(target), ["args", "0"]));
1066
+ }
1067
+ /**
1068
+ * A copy of the raw secrets this world has kept, for a restore of its
1069
+ * snapshots. It lives in memory only.
1070
+ */
1071
+ keptSecrets() {
1072
+ return this.secrets.copy();
1073
+ }
1074
+ /**
1075
+ * The failure report for `thrown`, whatever was thrown, with every kept
1076
+ * secret marked. `thrown` itself is never changed.
1077
+ */
1078
+ redactFailure(thrown) {
1079
+ return failure(thrown, this.secrets);
1080
+ }
1081
+ /**
1082
+ * `outcome` as an artifact writes it: a value marked by the shape of what
1083
+ * `functionPath` returns, a failure as its report.
1084
+ */
1085
+ redactOutcome(outcome, functionPath) {
1086
+ return outcome._tag === "returned" ? {
1087
+ _tag: "returned",
1088
+ value: markValue(outcome.value, placed(functionPath === void 0 ? void 0 : this.redaction.returns(functionPath)), this.secrets)
1089
+ } : {
1090
+ _tag: "failed",
1091
+ error: this.redactFailure(outcome.error)
1092
+ };
1093
+ }
1094
+ /**
1095
+ * A changed document as an artifact writes it, marked by its table's shape.
1096
+ */
1097
+ redactChange(change) {
1098
+ const document = (doc) => doc === null ? null : markValue(doc, this.documentPlacements(change.componentPath, tableNameFromId(doc._id) ?? "", doc), this.secrets);
1099
+ return {
1100
+ ...change,
1101
+ before: document(change.before),
1102
+ after: document(change.after)
1103
+ };
1104
+ }
1105
+ /**
1106
+ * The function a subscription's or call's reference names, if it names one.
1107
+ */
1108
+ functionPathOf(reference) {
1109
+ try {
1110
+ return typeof reference === "function" ? void 0 : pathOfReference(reference, ROOT);
1111
+ } catch {
1112
+ return;
1113
+ }
1114
+ }
1115
+ /**
1116
+ * Read records as an artifact writes them. A key or range bound on a field
1117
+ * its table's shape holds secret is the marker, and so is any string holding
1118
+ * a kept secret.
1119
+ */
1120
+ projectReads(reads) {
1121
+ return reads.map((read) => {
1122
+ if (read.kind === "document") return read;
1123
+ const shape = this.redaction.table(read.componentPath, read.table);
1124
+ const sensitive = (fieldPath) => shape !== void 0 && sensitiveAt(shape, fieldPath.split("."));
1125
+ const value = (fieldPath, v) => sensitive(fieldPath) ? marker : markValue(v, [], this.secrets);
1126
+ const key = (k) => k === null ? null : k.map((v, i) => v === void 0 ? void 0 : value(read.fields[i] ?? "", v));
1127
+ return {
1128
+ ...read,
1129
+ range: read.range.map((bound) => ({
1130
+ ...bound,
1131
+ value: sensitive(bound.fieldPath) ? marker : convexToJson(markValue(jsonToConvex(bound.value), [], this.secrets))
1132
+ })),
1133
+ after: key(read.after),
1134
+ through: read.through === "end" || read.through === null ? read.through : key(read.through)
1135
+ };
767
1136
  });
768
1137
  }
1138
+ /**
1139
+ * A held attempt as an artifact writes it: its read records projected and its
1140
+ * outcome marked.
1141
+ */
1142
+ projectHeld(view) {
1143
+ return {
1144
+ ...view,
1145
+ reads: this.projectReads(view.reads),
1146
+ outcome: this.redactOutcome(view.outcome, view.functionPath)
1147
+ };
1148
+ }
1149
+ /**
1150
+ * What `apply` answered as an artifact writes it: an applied result marked by
1151
+ * the shape of what `functionPath` returns, a conflict's next attempt
1152
+ * projected, a refusal as its report.
1153
+ */
1154
+ projectApplied(applied, functionPath) {
1155
+ switch (applied.kind) {
1156
+ case "applied": {
1157
+ const outcome = this.redactOutcome({
1158
+ _tag: "returned",
1159
+ value: applied.result
1160
+ }, functionPath);
1161
+ return outcome._tag === "returned" ? {
1162
+ ...applied,
1163
+ result: outcome.value
1164
+ } : applied;
1165
+ }
1166
+ case "conflict": return {
1167
+ ...applied,
1168
+ next: this.projectHeld(applied.next)
1169
+ };
1170
+ case "refused": return {
1171
+ ...applied,
1172
+ error: this.redactFailure(applied.error)
1173
+ };
1174
+ case "exhausted": return {
1175
+ ...applied,
1176
+ error: this.redactFailure(applied.error)
1177
+ };
1178
+ }
1179
+ }
1180
+ /**
1181
+ * A function's arguments as an artifact writes them, marked by the shape of
1182
+ * its arguments. A command log of provider calls (#137's `commands()`) writes
1183
+ * each command's input through this.
1184
+ */
1185
+ projectArguments(functionPath, args) {
1186
+ return markValue(args, placed(this.redaction.args(functionPath)), this.secrets);
1187
+ }
769
1188
  schema(componentPath) {
770
1189
  return this.component(componentPath).schema;
771
1190
  }
772
1191
  view(execution) {
773
1192
  const attempt = transaction(execution);
1193
+ const { componentPath, limits } = execution;
774
1194
  return {
775
- schema: this.schema(execution.componentPath),
776
- scan: (tableName) => attempt.scan(execution.componentPath, tableName),
777
- read: (interval, after) => attempt.readInterval({
778
- ...interval,
779
- componentPath: execution.componentPath,
780
- after
781
- })
1195
+ schema: this.schema(componentPath),
1196
+ scan: (tableName) => attempt.scan(componentPath, tableName),
1197
+ read: ({ index, ...interval }, after) => {
1198
+ const advance = attempt.readInterval({
1199
+ ...interval,
1200
+ componentPath,
1201
+ after
1202
+ });
1203
+ return (through) => {
1204
+ advance(through);
1205
+ const examined$1 = examined(interval.range, interval.order, after, through);
1206
+ if (examined$1 !== null) refuse(attempt.meter.readInterval(indexKey(componentPath, interval.table, index), examined$1, limits));
1207
+ };
1208
+ },
1209
+ examine: (table, doc) => refuse(attempt.meter.readDocument(doc, isSystemTable(table), limits))
782
1210
  };
783
1211
  }
1212
+ /**
1213
+ * Read one document by ID, as the binary's `get_inner` does for a get and for
1214
+ * the document a patch, replace, delete or cancel changes: it reads the ID's
1215
+ * interval of the table's `by_id` index, then charges the document if there
1216
+ * is one (`crates/database/src/transaction.rs:1188-1250`).
1217
+ */
1218
+ readById(attempt, execution, id) {
1219
+ const { componentPath, limits } = execution;
1220
+ const table = tableNameFromId(id);
1221
+ attempt.readDocument(componentPath, id);
1222
+ refuse(attempt.meter.readInterval(indexKey(componentPath, table, "by_id"), point([id]), limits));
1223
+ const doc = attempt.get(componentPath, id);
1224
+ if (doc !== null) refuse(attempt.meter.readDocument(doc, isSystemTable(table), limits));
1225
+ return doc;
1226
+ }
1227
+ /**
1228
+ * Stage a write to a user table, counted against the execution's limits
1229
+ * first. A write over a limit is not staged (`Writes::update`,
1230
+ * `crates/database/src/writes.rs:241-310`).
1231
+ */
1232
+ stage(execution, id, doc) {
1233
+ const attempt = transaction(execution);
1234
+ refuse(attempt.meter.write(doc, execution.limits));
1235
+ attempt.write(execution.componentPath, id, doc);
1236
+ }
784
1237
  generateId(table) {
785
1238
  return String(this.nextDocId++).padStart(TYPICAL_CONVEX_ID_LENGTH - table.length, "0") + table;
786
1239
  }
@@ -869,6 +1322,7 @@ var World = class {
869
1322
  return JSON.stringify({});
870
1323
  case "1.0/count": {
871
1324
  const attempt = transaction(execution);
1325
+ refuse(attempt.meter.readInterval(indexKey(execution.componentPath, args.table, "by_id"), everything, execution.limits));
872
1326
  attempt.readInterval({
873
1327
  kind: "interval",
874
1328
  componentPath: execution.componentPath,
@@ -903,10 +1357,11 @@ var World = class {
903
1357
  return (op === "1.0/actions/query" ? this.queryAt(functionPath, args.args, from) : op === "1.0/actions/mutation" ? this.mutationAt(functionPath, args.args, from) : this.actionAt(functionPath, args.args, from)).then((value) => JSON.stringify(convexToJson(value)));
904
1358
  }
905
1359
  case "1.0/runUdf": {
906
- const { udfType, args: udfArgs } = args;
1360
+ const { udfType, args: udfArgs, transactionLimits } = args;
907
1361
  const from = {
908
1362
  request: execution.request,
909
- parent: execution
1363
+ parent: execution,
1364
+ budget: budgetOf(transactionLimits)
910
1365
  };
911
1366
  const functionPath = functionPathOf(args, execution.componentPath);
912
1367
  const value = jsonToConvex(udfArgs);
@@ -921,15 +1376,17 @@ var World = class {
921
1376
  case "1.0/schedule": return JSON.stringify(convexToJson(this.scheduleJob(transaction(execution), execution, args)));
922
1377
  case "1.0/actions/schedule": return JSON.stringify(convexToJson(this.commitNow((attempt) => this.scheduleJob(attempt, execution, args))));
923
1378
  case "1.0/cancel_job":
924
- this.cancelJob(transaction(execution), execution.componentPath, args.id);
1379
+ this.cancelJob(transaction(execution), execution, args.id);
925
1380
  return JSON.stringify({});
926
1381
  case "1.0/actions/cancel_job":
927
- this.commitNow((attempt) => this.cancelJob(attempt, execution.componentPath, args.id));
1382
+ this.commitNow((attempt) => this.cancelJob(attempt, execution, args.id));
928
1383
  return JSON.stringify({});
929
1384
  case "1.0/storageDelete":
930
1385
  case "1.0/storageGetUrl":
931
1386
  case "1.0/storageGenerateUploadUrl": throw rowPending("storage", 6);
932
- case "1.0/getTransactionMetrics": throw rowPending("transaction limits", 6);
1387
+ case "1.0/getTransactionMetrics":
1388
+ if (execution.attempt === void 0) throw new Error(`Unknown async operation ${op}`);
1389
+ return JSON.stringify(execution.attempt.meter.metrics(execution.limits));
933
1390
  case "1.0/actions/vectorSearch": throw requiresBinary("vectorSearch");
934
1391
  default: throw new Error(`The test kernel does not support async syscall: "${op}"`);
935
1392
  }
@@ -939,15 +1396,15 @@ var World = class {
939
1396
  }
940
1397
  get(execution, tableName, id) {
941
1398
  validateId(tableName, id);
942
- const attempt = transaction(execution);
943
- attempt.readDocument(execution.componentPath, id);
944
- return attempt.get(execution.componentPath, id);
1399
+ return this.readById(transaction(execution), execution, id);
945
1400
  }
946
1401
  insert(execution, table, value) {
947
1402
  const attempt = transaction(execution);
1403
+ this.keepRow(execution.componentPath, table, value);
948
1404
  validateDocument(this.schema(execution.componentPath), table, value);
949
1405
  const _id = this.generateId(table);
950
- attempt.write(execution.componentPath, _id, {
1406
+ attempt.meter.deriveInterval(indexKey(execution.componentPath, table, "by_id"), point([_id]));
1407
+ this.stage(execution, _id, {
951
1408
  ...value,
952
1409
  _id,
953
1410
  _creationTime: this.creationTime(execution.startTime)
@@ -966,9 +1423,7 @@ var World = class {
966
1423
  existing(execution, tableName, id, write, value) {
967
1424
  validateId(tableName, id);
968
1425
  if (value !== void 0 && typeof value !== "object") throw new Error(`Invalid argument \`value\` in \`db.${write}\`, expected object but got '${typeof value}': ${value}`);
969
- const attempt = transaction(execution);
970
- attempt.readDocument(execution.componentPath, id);
971
- const document = attempt.get(execution.componentPath, id);
1426
+ const document = this.readById(transaction(execution), execution, id);
972
1427
  if (document === null) throw new Error(write === "delete" ? "Delete on non-existent doc" : `${write === "patch" ? "Patch" : "Replace"} on non-existent document with ID "${id}"`);
973
1428
  return document;
974
1429
  }
@@ -986,27 +1441,89 @@ var World = class {
986
1441
  ...fields,
987
1442
  ...this.fields(document, value)
988
1443
  };
989
- validateDocument(this.schema(execution.componentPath), tableNameFromId(_id), merged);
990
- transaction(execution).write(execution.componentPath, _id, {
1444
+ this.keepRow(execution.componentPath, tableNameFromId(_id), merged);
1445
+ const next = {
991
1446
  _id,
992
1447
  _creationTime,
993
1448
  ...merged
994
- });
1449
+ };
1450
+ if (sameDocument(document, next)) return;
1451
+ validateDocument(this.schema(execution.componentPath), tableNameFromId(_id), merged);
1452
+ this.stage(execution, _id, next);
995
1453
  }
996
1454
  replace(execution, tableName, id, value) {
997
1455
  const document = this.existing(execution, tableName, id, "replace", value);
998
1456
  const fields = this.fields(document, value);
999
- validateDocument(this.schema(execution.componentPath), tableNameFromId(document._id), fields);
1000
- transaction(execution).write(execution.componentPath, document._id, {
1457
+ this.keepRow(execution.componentPath, tableNameFromId(document._id), fields);
1458
+ const next = {
1001
1459
  ...fields,
1002
1460
  _id: document._id,
1003
1461
  _creationTime: document._creationTime
1004
- });
1462
+ };
1463
+ if (sameDocument(document, next)) return;
1464
+ validateDocument(this.schema(execution.componentPath), tableNameFromId(document._id), fields);
1465
+ this.stage(execution, document._id, next);
1005
1466
  }
1006
1467
  remove(execution, tableName, id) {
1007
1468
  const document = this.existing(execution, tableName, id, "delete", void 0);
1008
- transaction(execution).write(execution.componentPath, document._id, null);
1469
+ this.stage(execution, document._id, null);
1470
+ }
1471
+ };
1472
+ const refuse = (error) => {
1473
+ if (error !== void 0) throw error;
1474
+ };
1475
+ /**
1476
+ * One index of one table in one component, as the binary keys a read set.
1477
+ */
1478
+ const indexKey = (componentPath, table, index) => `${componentPath}\u0000${table}\u0000${index}`;
1479
+ const isSystemTable = (table) => table.startsWith("_");
1480
+ const isFieldMap = (value) => typeof value === "object" && value !== null && isSimpleObject(value);
1481
+ /**
1482
+ * Whether two stored values are the same value, an unresolved commit timestamp
1483
+ * included. The binary compares pending values structurally, so a pending
1484
+ * timestamp equals only another pending timestamp, at any depth (`PendingValue`
1485
+ * derives `PartialEq`, `crates/value/src/pending.rs:56-75`, as
1486
+ * `PendingDocument` does, `crates/common/src/document.rs:624-632`). Concrete
1487
+ * values compare by `ConvexValue`'s total order
1488
+ * (`crates/value/src/sorting.rs:468-472`), which `compareValues` follows.
1489
+ * `compareValues` alone ranks a pending timestamp as the largest int64, so it
1490
+ * cannot tell the two apart. An object field set to `undefined` is absent.
1491
+ */
1492
+ const sameValue = (a, b) => {
1493
+ const aPending = a instanceof CommitTsPlaceholder;
1494
+ const bPending = b instanceof CommitTsPlaceholder;
1495
+ if (aPending || bPending) return aPending && bPending;
1496
+ if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((value, i) => sameValue(value, b[i]));
1497
+ if (isFieldMap(a) || isFieldMap(b)) {
1498
+ if (!isFieldMap(a) || !isFieldMap(b)) return false;
1499
+ const fields = (value) => Object.entries(value).filter(([, field]) => field !== void 0);
1500
+ const aFields = fields(a);
1501
+ const bFields = new Map(fields(b));
1502
+ return aFields.length === bFields.size && aFields.every(([key, field]) => bFields.has(key) && sameValue(field, bFields.get(key)));
1503
+ }
1504
+ return compareValues(a, b) === 0;
1505
+ };
1506
+ /**
1507
+ * Whether a patch or replace leaves a document as it was. The binary then
1508
+ * writes nothing (`patch_inner` and `replace_inner`,
1509
+ * `crates/database/src/transaction.rs:713-790`).
1510
+ */
1511
+ const sameDocument = (before, after) => sameValue(before, after);
1512
+ /**
1513
+ * A nested call's `transactionLimits`. The binary reads each field as an
1514
+ * unsigned integer and ignores fields it does not know (`TransactionLimits`,
1515
+ * `crates/database/src/execution_size.rs:62-95`).
1516
+ */
1517
+ const budgetOf = (limits) => {
1518
+ if (limits === void 0 || limits === null) return void 0;
1519
+ const budget = {};
1520
+ for (const metric of Object.keys(defaults)) {
1521
+ const value = limits[metric];
1522
+ if (value === void 0) continue;
1523
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throw new Error(`Invalid arguments for \`runUdf\`: transactionLimits.${metric} must be a non-negative integer, got ${JSON.stringify(value)}`);
1524
+ budget[metric] = value;
1009
1525
  }
1526
+ return budget;
1010
1527
  };
1011
1528
  const closed = (execution, operation) => new ClosedAttempt({
1012
1529
  operation,