@gaganref/confect-test 10.3.1-fork.92 → 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 (36) 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/ConvexTest.d.ts +26 -1
  6. package/dist/internal/ConvexTest.d.ts.map +1 -1
  7. package/dist/internal/ConvexTest.js +55 -27
  8. package/dist/internal/ConvexTest.js.map +1 -1
  9. package/dist/internal/Execution.d.ts +138 -1
  10. package/dist/internal/Execution.d.ts.map +1 -1
  11. package/dist/internal/Execution.js +595 -182
  12. package/dist/internal/Execution.js.map +1 -1
  13. package/dist/internal/KernelRefusal.d.ts +134 -0
  14. package/dist/internal/KernelRefusal.d.ts.map +1 -1
  15. package/dist/internal/KernelRefusal.js +134 -1
  16. package/dist/internal/KernelRefusal.js.map +1 -1
  17. package/dist/internal/Redaction.d.ts +142 -0
  18. package/dist/internal/Redaction.d.ts.map +1 -0
  19. package/dist/internal/Redaction.js +448 -0
  20. package/dist/internal/Redaction.js.map +1 -0
  21. package/dist/internal/Snapshot.d.ts +298 -0
  22. package/dist/internal/Snapshot.d.ts.map +1 -0
  23. package/dist/internal/Snapshot.js +532 -0
  24. package/dist/internal/Snapshot.js.map +1 -0
  25. package/dist/internal/Subscriptions.d.ts +13 -0
  26. package/dist/internal/Subscriptions.d.ts.map +1 -1
  27. package/dist/internal/Subscriptions.js +27 -3
  28. package/dist/internal/Subscriptions.js.map +1 -1
  29. package/package.json +3 -3
  30. package/src/TestConfect.ts +63 -4
  31. package/src/internal/ConvexTest.ts +135 -38
  32. package/src/internal/Execution.ts +886 -287
  33. package/src/internal/KernelRefusal.ts +160 -0
  34. package/src/internal/Redaction.ts +730 -0
  35. package/src/internal/Snapshot.ts +945 -0
  36. package/src/internal/Subscriptions.ts +34 -4
@@ -5,12 +5,15 @@ import { apply, conflict, empty, wallClock } from "./Kernel.js";
5
5
  import { defaults, everything, examined, fromBudget, point } from "./Metering.js";
6
6
  import { KernelAttempt } from "./Attempt.js";
7
7
  import { bind, current as current$1, fresh, unbound } from "./Invocation.js";
8
- 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";
9
9
  import { exportSchema, paginate, stream, validateDocument } from "./KernelQuery.js";
10
+ import { Secrets, failure, keep, mark, markValue, marker, none, sensitiveAt, unmark } from "./Redaction.js";
10
11
  import { functions, pending, row, stateOf, table } from "./Scheduling.js";
12
+ import { check, contract, decode, encode } from "./Snapshot.js";
11
13
  import { actionGeneric, getFunctionAddress, mutationGeneric, queryGeneric } from "convex/server";
12
14
  import { AsyncLocalStorage } from "node:async_hooks";
13
15
  import { CommitTsPlaceholder, compareValues, convexToJson, jsonToConvex } from "convex/values";
16
+ import * as Result from "effect/Result";
14
17
  //#region src/internal/Execution.ts
15
18
  const ROOT = "";
16
19
  const TYPICAL_CONVEX_ID_LENGTH = 32;
@@ -98,6 +101,23 @@ const pinnedTime = () => {
98
101
  * `finishAllScheduledFunctions` defaults to.
99
102
  */
100
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
+ });
101
121
  const current = () => {
102
122
  const execution = executions.getStore();
103
123
  if (execution === void 0) throw new Error("A Convex syscall ran outside any function the test kernel started.");
@@ -191,6 +211,7 @@ const kindCheck = {
191
211
  */
192
212
  var World = class {
193
213
  runtime;
214
+ redaction;
194
215
  components = /* @__PURE__ */ new Map();
195
216
  state = empty;
196
217
  opened;
@@ -206,8 +227,31 @@ var World = class {
206
227
  */
207
228
  floor = Number.NEGATIVE_INFINITY;
208
229
  targets = /* @__PURE__ */ new Map();
209
- 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) {
210
253
  this.runtime = runtime;
254
+ this.redaction = redaction;
211
255
  installConvexGlobal();
212
256
  install(pinnedTime, () => executions.getStore() !== void 0);
213
257
  this.components.set(ROOT, makeComponent(ROOT, schema, modules));
@@ -273,66 +317,74 @@ var World = class {
273
317
  };
274
318
  }
275
319
  query(caller, reference, args) {
276
- return this.open().then(() => {
277
- const from = {
278
- caller,
279
- request: newRequest(caller)
280
- };
281
- return typeof reference === "function" ? this.runQuery({
282
- componentPath: ROOT,
283
- udfPath: "inline"
284
- }, {
285
- handler: reference,
286
- args: { type: "any" },
287
- returns: null
288
- }, 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
+ });
289
335
  });
290
336
  }
291
337
  mutation(caller, reference, args) {
292
- return this.open().then(() => {
293
- const from = {
294
- caller,
295
- request: newRequest(caller)
296
- };
297
- return typeof reference === "function" ? this.runMutation({
298
- componentPath: ROOT,
299
- udfPath: "inline"
300
- }, {
301
- handler: reference,
302
- args: { type: "any" },
303
- returns: null
304
- }, 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
+ });
305
353
  });
306
354
  }
307
355
  action(caller, reference, args) {
308
- return this.open().then(() => {
309
- const from = {
310
- caller,
311
- request: newRequest(caller)
312
- };
313
- return typeof reference === "function" ? this.runAction({
314
- componentPath: ROOT,
315
- udfPath: "inline"
316
- }, {
317
- handler: reference,
318
- args: { type: "any" },
319
- returns: null
320
- }, 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
+ });
321
371
  });
322
372
  }
323
373
  run(caller, componentPath, handler) {
324
- return this.open().then(() => {
325
- this.component(componentPath);
326
- return this.runMutation({
327
- componentPath,
328
- udfPath: "custom"
329
- }, {
330
- handler,
331
- args: { type: "any" },
332
- returns: null
333
- }, {}, {
334
- caller,
335
- 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
+ });
336
388
  });
337
389
  });
338
390
  }
@@ -340,22 +392,27 @@ var World = class {
340
392
  * Run a mutation to its commit point and hold it as attempt 1.
341
393
  */
342
394
  prepare(caller, reference, args) {
343
- return this.open().then(() => {
344
- const functionPath = pathOfReference(reference, ROOT);
345
- const resolved = this.resolve(functionPath, "mutation");
346
- validateFunctionArgs(resolved.args, args ?? {}, functionPath, false);
347
- return this.prepareMutation(functionPath, resolved, args, {
348
- caller,
349
- request: newRequest(caller)
350
- }, 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
+ });
351
406
  });
352
407
  }
353
408
  apply(id) {
354
- const held = this.holding.get(id);
355
- return held === void 0 ? Promise.reject(new StaleAttempt({
356
- id,
357
- message: `The test kernel holds no attempt "${id}". It was already applied, or never prepared.`
358
- })) : 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
+ });
359
416
  }
360
417
  /**
361
418
  * The held attempts, oldest first.
@@ -370,37 +427,226 @@ var World = class {
370
427
  * read where an earlier run's ended.
371
428
  */
372
429
  subscribed(caller, reference, args, pinned) {
373
- return this.open().then(() => {
374
- const functionPath = pathOfReference(reference, ROOT);
375
- const resolved = this.resolve(functionPath, "query");
376
- validateFunctionArgs(resolved.args, args ?? {}, functionPath, false);
377
- const top = {
378
- attempt: new KernelAttempt(this.state),
379
- pagination: {
380
- calls: 0,
381
- journal: pinned.journal,
382
- end: null
383
- },
384
- startTime: pinned.time
385
- };
386
- const run = (outcome) => ({
387
- outcome,
388
- snapshot: top.attempt.snapshot.version,
389
- reads: top.attempt.reads,
390
- observedIdentity: top.attempt.observedIdentity,
391
- time: top.attempt.observedTime,
392
- 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
+ }));
393
463
  });
394
- return this.runQuery(functionPath, resolved, args, {
395
- caller,
396
- request: newRequest(caller)
397
- }, false, top).then((value) => run({
398
- _tag: "returned",
399
- value
400
- }), (error) => run({
401
- _tag: "failed",
402
- error
403
- }));
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.`
404
650
  });
405
651
  }
406
652
  /**
@@ -422,9 +668,11 @@ var World = class {
422
668
  * crosses no future deadline.
423
669
  */
424
670
  advance(ms, budget = defaultBudget) {
425
- return this.open().then(() => {
426
- const until = this.now() + ms;
427
- 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
+ });
428
676
  });
429
677
  }
430
678
  /**
@@ -432,7 +680,9 @@ var World = class {
432
680
  * standing jobs are pending. Every job run counts against `budget`.
433
681
  */
434
682
  drain(budget = defaultBudget) {
435
- 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
+ });
436
686
  }
437
687
  /**
438
688
  * Run the first of `select(pending)` until it is empty, moving the clock to
@@ -533,6 +783,7 @@ var World = class {
533
783
  const target = functionPathOf(args, execution.componentPath);
534
784
  this.component(target.componentPath);
535
785
  const jobArgs = jsonToConvex(args.args);
786
+ this.keepArgs(target, jobArgs);
536
787
  if (hasPendingValues(jobArgs)) throw new Error(`Invalid arguments for "${target.udfPath}": a scheduled function cannot take an unresolved placeholder value.`);
537
788
  refuse(attempt.meter.schedule(jobArgs, execution.limits));
538
789
  const jobsIndex = indexKey(execution.componentPath, table, "by_id");
@@ -570,16 +821,19 @@ var World = class {
570
821
  }
571
822
  queryAt(functionPath, args, from, snapshotOnly = false) {
572
823
  const resolved = this.resolve(functionPath, "query");
824
+ this.keepArgs(functionPath, args);
573
825
  validateFunctionArgs(resolved.args, args ?? {}, functionPath, from.parent?.attempt !== void 0);
574
826
  return this.runQuery(functionPath, resolved, args, from, snapshotOnly);
575
827
  }
576
828
  mutationAt(functionPath, args, from) {
577
829
  const resolved = this.resolve(functionPath, "mutation");
830
+ this.keepArgs(functionPath, args);
578
831
  validateFunctionArgs(resolved.args, args ?? {}, functionPath, from.parent?.attempt !== void 0);
579
832
  return this.runMutation(functionPath, resolved, args, from);
580
833
  }
581
834
  actionAt(functionPath, args, from) {
582
835
  const resolved = this.resolve(functionPath, "action");
836
+ this.keepArgs(functionPath, args);
583
837
  validateFunctionArgs(resolved.args, args ?? {}, functionPath, false);
584
838
  return this.runAction(functionPath, resolved, args, from);
585
839
  }
@@ -602,25 +856,28 @@ var World = class {
602
856
  };
603
857
  }
604
858
  runQuery(functionPath, resolved, args, from, snapshotOnly = false, top) {
605
- const parentAttempt = from.parent?.attempt;
606
- const isNested = parentAttempt !== void 0;
607
- const attempt = top?.attempt ?? (parentAttempt === void 0 ? new KernelAttempt(this.state) : snapshotOnly ? parentAttempt.staleSnapshot() : parentAttempt);
608
- const execution = this.execution(functionPath, "query", attempt, from, top?.pagination ?? (isNested && from.parent?.pagination || {
609
- calls: 0,
610
- journal: null,
611
- end: null
612
- }), top?.startTime);
613
- const q = queryGeneric({ handler: (ctx, a) => resolved.handler({
614
- ...ctx,
615
- auth: queryAuth(execution)
616
- }, a) });
617
- return enter(execution, () => q.invokeQuery(JSON.stringify(convexToJson([parseArgs(args)])))).then((raw) => settled(execution, raw)).then((raw) => {
618
- const result = jsonToConvex(JSON.parse(raw));
619
- if (!isNested && hasPendingValues(result)) throw new Error(`Function ${functionPath.udfPath} return value invalid: queries cannot return an unresolved placeholder value.`);
620
- validateReturnValue(resolved.returns, result, "query", functionPath);
621
- return result;
622
- }).catch((error) => {
623
- 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
+ });
624
881
  });
625
882
  }
626
883
  invokeMutation(execution, resolved, args) {
@@ -630,28 +887,32 @@ var World = class {
630
887
  }, a) });
631
888
  return enter(execution, () => m.invokeMutation(JSON.stringify(convexToJson([parseArgs(args)])))).then((raw) => settled(execution, raw)).then((raw) => {
632
889
  const result = jsonToConvex(JSON.parse(raw));
633
- validateReturnValue(resolved.returns, result, "mutation", {
890
+ const functionPath = {
634
891
  componentPath: execution.componentPath,
635
892
  udfPath: execution.udfPath
636
- });
893
+ };
894
+ this.keepReturns(functionPath, result);
895
+ validateReturnValue(resolved.returns, result, "mutation", functionPath);
637
896
  return result;
638
897
  });
639
898
  }
640
899
  runMutation(functionPath, resolved, args, from) {
641
- const parentAttempt = from.parent?.attempt;
642
- if (parentAttempt !== void 0) {
643
- const frame = parentAttempt.child();
644
- const execution = this.execution(functionPath, "mutation", frame, from, void 0);
645
- return this.invokeMutation(execution, resolved, args).then((result) => frame.drained().then(() => {
646
- frame.close();
647
- frame.merge();
648
- return result;
649
- }), (error) => {
650
- frame.close();
651
- throw deserializeConvexErrorData(error);
652
- });
653
- }
654
- 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
+ });
655
916
  }
656
917
  /**
657
918
  * Run one attempt of a top-level mutation against the latest commit, then
@@ -661,34 +922,37 @@ var World = class {
661
922
  * number.
662
923
  */
663
924
  prepareMutation(functionPath, resolved, args, from, number, bound) {
664
- const attempt = new KernelAttempt(this.state, number);
665
- const execution = this.execution(functionPath, "mutation", attempt, from, void 0);
666
- const run = () => this.invokeMutation(execution, resolved, args);
667
- return (bound === void 0 ? run() : bind({
668
- ...bound,
669
- attempt: number
670
- }, run)).then((value) => attempt.drained().then(() => ({
671
- _tag: "returned",
672
- value
673
- })), (error) => ({
674
- _tag: "failed",
675
- error: deserializeConvexErrorData(error)
676
- })).then((outcome) => {
677
- attempt.close();
678
- const held = {
679
- view: {
680
- id: `attempt-${this.nextAttemptId++}`,
681
- request: from.request.requestId,
682
- number,
683
- snapshot: attempt.snapshot.version,
684
- reads: attempt.reads,
685
- outcome
686
- },
687
- attempt,
688
- again: (next) => this.prepareMutation(functionPath, resolved, args, from, next, bound)
689
- };
690
- this.holding.set(held.view.id, held);
691
- 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
+ });
692
956
  });
693
957
  }
694
958
  /**
@@ -753,28 +1017,174 @@ var World = class {
753
1017
  });
754
1018
  }
755
1019
  runAction(functionPath, resolved, args, from) {
756
- const execution = this.execution(functionPath, "action", void 0, from, void 0);
757
- const inner = {
758
- request: from.request,
759
- 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)
760
1064
  };
761
- const a = actionGeneric({ handler: (ctx, innerArgs) => resolved.handler({
762
- ...ctx,
763
- runQuery: (reference, queryArgs) => this.queryAt(pathOfReference(reference, functionPath.componentPath), queryArgs, inner),
764
- runMutation: (reference, mutationArgs) => this.mutationAt(pathOfReference(reference, functionPath.componentPath), mutationArgs, inner),
765
- runAction: (reference, actionArgs) => this.actionAt(pathOfReference(reference, functionPath.componentPath), actionArgs, inner),
766
- auth: queryAuth(execution)
767
- }, innerArgs) });
768
- const run = () => enter(execution, () => a.invokeAction(from.request.requestId, JSON.stringify(convexToJson([parseArgs(args)]))));
769
- const bound = from.parent === void 0 ? void 0 : current$1();
770
- return (bound === void 0 ? run() : bind(fresh(bound.world, functionPath.componentPath, from.request), run)).then((raw) => {
771
- const result = jsonToConvex(JSON.parse(raw));
772
- validateReturnValue(resolved.returns, result, "action", functionPath);
773
- return result;
774
- }).catch((error) => {
775
- 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
+ };
776
1136
  });
777
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
+ }
778
1188
  schema(componentPath) {
779
1189
  return this.component(componentPath).schema;
780
1190
  }
@@ -990,6 +1400,7 @@ var World = class {
990
1400
  }
991
1401
  insert(execution, table, value) {
992
1402
  const attempt = transaction(execution);
1403
+ this.keepRow(execution.componentPath, table, value);
993
1404
  validateDocument(this.schema(execution.componentPath), table, value);
994
1405
  const _id = this.generateId(table);
995
1406
  attempt.meter.deriveInterval(indexKey(execution.componentPath, table, "by_id"), point([_id]));
@@ -1030,6 +1441,7 @@ var World = class {
1030
1441
  ...fields,
1031
1442
  ...this.fields(document, value)
1032
1443
  };
1444
+ this.keepRow(execution.componentPath, tableNameFromId(_id), merged);
1033
1445
  const next = {
1034
1446
  _id,
1035
1447
  _creationTime,
@@ -1042,6 +1454,7 @@ var World = class {
1042
1454
  replace(execution, tableName, id, value) {
1043
1455
  const document = this.existing(execution, tableName, id, "replace", value);
1044
1456
  const fields = this.fields(document, value);
1457
+ this.keepRow(execution.componentPath, tableNameFromId(document._id), fields);
1045
1458
  const next = {
1046
1459
  ...fields,
1047
1460
  _id: document._id,