@gaganref/confect-test 10.3.1-fork.90 → 10.3.1-fork.92

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 (34) hide show
  1. package/dist/internal/Attempt.d.ts +29 -1
  2. package/dist/internal/Attempt.d.ts.map +1 -1
  3. package/dist/internal/Attempt.js +45 -1
  4. package/dist/internal/Attempt.js.map +1 -1
  5. package/dist/internal/ConvexTest.d.ts +23 -2
  6. package/dist/internal/ConvexTest.d.ts.map +1 -1
  7. package/dist/internal/ConvexTest.js +11 -5
  8. package/dist/internal/ConvexTest.js.map +1 -1
  9. package/dist/internal/Execution.d.ts +43 -0
  10. package/dist/internal/Execution.d.ts.map +1 -1
  11. package/dist/internal/Execution.js +200 -41
  12. package/dist/internal/Execution.js.map +1 -1
  13. package/dist/internal/Invocation.d.ts.map +1 -1
  14. package/dist/internal/Invocation.js.map +1 -1
  15. package/dist/internal/KernelQuery.d.ts +8 -3
  16. package/dist/internal/KernelQuery.d.ts.map +1 -1
  17. package/dist/internal/KernelQuery.js +10 -3
  18. package/dist/internal/KernelQuery.js.map +1 -1
  19. package/dist/internal/Metering.d.ts +186 -0
  20. package/dist/internal/Metering.d.ts.map +1 -0
  21. package/dist/internal/Metering.js +401 -0
  22. package/dist/internal/Metering.js.map +1 -0
  23. package/dist/internal/Subscriptions.d.ts +116 -0
  24. package/dist/internal/Subscriptions.d.ts.map +1 -0
  25. package/dist/internal/Subscriptions.js +186 -0
  26. package/dist/internal/Subscriptions.js.map +1 -0
  27. package/package.json +3 -3
  28. package/src/internal/Attempt.ts +52 -0
  29. package/src/internal/ConvexTest.ts +33 -5
  30. package/src/internal/Execution.ts +375 -54
  31. package/src/internal/Invocation.ts +2 -1
  32. package/src/internal/KernelQuery.ts +17 -6
  33. package/src/internal/Metering.ts +520 -0
  34. package/src/internal/Subscriptions.ts +302 -0
@@ -9,7 +9,13 @@ import {
9
9
  type SchemaDefinition,
10
10
  type UserIdentity,
11
11
  } from "convex/server";
12
- import { convexToJson, jsonToConvex, type Value } from "convex/values";
12
+ import {
13
+ CommitTsPlaceholder,
14
+ compareValues,
15
+ convexToJson,
16
+ jsonToConvex,
17
+ type Value,
18
+ } from "convex/values";
13
19
  import { KernelAttempt } from "./Attempt.js";
14
20
  import * as Clock from "./Clock.js";
15
21
  import * as Globals from "./Globals.js";
@@ -17,6 +23,7 @@ import * as Invocation from "./Invocation.js";
17
23
  import * as Kernel from "./Kernel.js";
18
24
  import * as KernelQuery from "./KernelQuery.js";
19
25
  import * as KernelRefusal from "./KernelRefusal.js";
26
+ import * as Metering from "./Metering.js";
20
27
  import type * as ReadSet from "./ReadSet.js";
21
28
  import * as Scheduling from "./Scheduling.js";
22
29
  import type {
@@ -31,6 +38,7 @@ import {
31
38
  deserializeConvexErrorData,
32
39
  evaluateValue,
33
40
  hasPendingValues,
41
+ isSimpleObject,
34
42
  parseArgs,
35
43
  parseFunctionHandle,
36
44
  resolveCommittedValues,
@@ -137,6 +145,47 @@ export type KernelApply =
137
145
  }
138
146
  | { readonly kind: "refused"; readonly tag: string; readonly error: unknown };
139
147
 
148
+ /**
149
+ * One top-level query run and every dependency it took: the commit and reads it
150
+ * read, whether it asked for the caller's identity, the pinned time it read and
151
+ * the end of its paginated read.
152
+ */
153
+ export interface QueryRun {
154
+ readonly outcome: Outcome;
155
+ readonly snapshot: Kernel.Version;
156
+ readonly reads: ReadonlyArray<ReadSet.Read>;
157
+ readonly observedIdentity: boolean;
158
+ /**
159
+ * The pinned time the query read, or `null` when it never read the time.
160
+ */
161
+ readonly time: number | null;
162
+ /**
163
+ * Where the query's paginated read ended, or `null` when it ran none. A rerun
164
+ * ends its page there, as the binary's query journal does.
165
+ */
166
+ readonly journal: string | null;
167
+ }
168
+
169
+ /**
170
+ * The one paginated read a query and its nested calls may run. `journal` is
171
+ * where a previous run's page ended, and `end` where this run's page ends.
172
+ */
173
+ interface Pagination {
174
+ calls: number;
175
+ readonly journal: string | null;
176
+ end: string | null;
177
+ }
178
+
179
+ /**
180
+ * A top-level query whose attempt and page the caller keeps, at a pinned start
181
+ * time or the kernel's time now.
182
+ */
183
+ interface TopQuery {
184
+ readonly attempt: KernelAttempt;
185
+ readonly pagination: Pagination;
186
+ readonly startTime: number | undefined;
187
+ }
188
+
140
189
  interface Held {
141
190
  readonly view: HeldAttempt;
142
191
  readonly attempt: KernelAttempt;
@@ -174,8 +223,13 @@ interface Execution {
174
223
  * calls share it, and `Date` answers it inside them.
175
224
  */
176
225
  readonly startTime: number;
177
- readonly pagination: { calls: number } | undefined;
226
+ readonly pagination: Pagination | undefined;
178
227
  readonly turn: Turn;
228
+ /**
229
+ * The transaction limits this function runs under: the defaults at the top
230
+ * level, its caller's inside a nested call, tightened by the call's budget.
231
+ */
232
+ readonly limits: Metering.Limits;
179
233
  }
180
234
 
181
235
  /**
@@ -227,14 +281,15 @@ const kernelBrand = Symbol.for("@confect/test/Kernel");
227
281
 
228
282
  /**
229
283
  * The time `Date` answers inside a kernel invocation: a query's or mutation's
230
- * start, or the clock as it runs in an action. Outside one, none.
284
+ * start, or the clock as it runs in an action. Outside one, none. A query or
285
+ * mutation that reads it takes a dependency on that time.
231
286
  */
232
287
  const pinnedTime = (): number | undefined => {
233
288
  const execution = executions.getStore();
234
289
  if (execution === undefined) return undefined;
235
- return execution.udfType === "action"
236
- ? execution.world.now()
237
- : execution.startTime;
290
+ if (execution.udfType === "action") return execution.world.now();
291
+ execution.attempt?.observeTime(execution.startTime);
292
+ return execution.startTime;
238
293
  };
239
294
 
240
295
  /**
@@ -642,6 +697,51 @@ export class World {
642
697
  return Array.from(this.holding.values(), (held) => held.view);
643
698
  }
644
699
 
700
+ // Subscribed queries.
701
+
702
+ /**
703
+ * Run a query at the latest commit as a subscription runs it, under `caller`,
704
+ * and return its answer with every dependency it took. `time` pins the time
705
+ * it reads, the kernel's time now by default. `journal` ends its paginated
706
+ * read where an earlier run's ended.
707
+ */
708
+ subscribed(
709
+ caller: Caller,
710
+ reference: any,
711
+ args: any,
712
+ pinned: { readonly time?: number; readonly journal: string | null },
713
+ ): Promise<QueryRun> {
714
+ return this.open().then(() => {
715
+ const functionPath = pathOfReference(reference, ROOT);
716
+ const resolved = this.resolve(functionPath, "query");
717
+ validateFunctionArgs(resolved.args, args ?? {}, functionPath, false);
718
+ const top: TopQuery = {
719
+ attempt: new KernelAttempt(this.state),
720
+ pagination: { calls: 0, journal: pinned.journal, end: null },
721
+ startTime: pinned.time,
722
+ };
723
+ const run = (outcome: Outcome): QueryRun => ({
724
+ outcome,
725
+ snapshot: top.attempt.snapshot.version,
726
+ reads: top.attempt.reads,
727
+ observedIdentity: top.attempt.observedIdentity,
728
+ time: top.attempt.observedTime,
729
+ journal: pinned.journal ?? top.pagination.end,
730
+ });
731
+ return this.runQuery(
732
+ functionPath,
733
+ resolved,
734
+ args,
735
+ { caller, request: newRequest(caller) },
736
+ false,
737
+ top,
738
+ ).then(
739
+ (value: Value) => run({ _tag: "returned", value }),
740
+ (error: unknown) => run({ _tag: "failed", error }),
741
+ );
742
+ });
743
+ }
744
+
645
745
  // Scheduled functions, on the kernel's clock.
646
746
 
647
747
  /**
@@ -828,7 +928,26 @@ export class World {
828
928
  `Invalid arguments for "${target.udfPath}": a scheduled function cannot take an unresolved placeholder value.`,
829
929
  );
830
930
  }
931
+ refuse(attempt.meter.schedule(jobArgs, execution.limits));
932
+ const jobsIndex = indexKey(
933
+ execution.componentPath,
934
+ Scheduling.table,
935
+ "by_id",
936
+ );
937
+ // A scheduled function that schedules reads its own job first
938
+ // (`SchedulerModel::schedule`, `crates/model/src/scheduled_jobs/mod.rs:238-300`).
939
+ const parentJob = execution.request.scheduledFunctionId;
940
+ if (parentJob !== null) {
941
+ refuse(
942
+ attempt.meter.readInterval(
943
+ jobsIndex,
944
+ Metering.point([parentJob]),
945
+ execution.limits,
946
+ ),
947
+ );
948
+ }
831
949
  const id = this.generateId(Scheduling.table);
950
+ attempt.meter.deriveInterval(jobsIndex, Metering.point([id]));
832
951
  attempt.write(
833
952
  execution.componentPath,
834
953
  id,
@@ -855,10 +974,10 @@ export class World {
855
974
  /**
856
975
  * Cancel a job that has not finished. A finished job stays as it ended.
857
976
  */
858
- private cancelJob(attempt: KernelAttempt, componentPath: string, id: any) {
977
+ private cancelJob(attempt: KernelAttempt, execution: Execution, id: any) {
978
+ const { componentPath } = execution;
859
979
  validateId(Scheduling.table, id);
860
- attempt.readDocument(componentPath, id);
861
- const doc = attempt.get(componentPath, id);
980
+ const doc = this.readById(attempt, execution, id);
862
981
  if (doc === null) {
863
982
  throw new Error(`Patch on non-existent document with ID "${id}"`);
864
983
  }
@@ -914,7 +1033,8 @@ export class World {
914
1033
  udfType: UdfType,
915
1034
  attempt: KernelAttempt | undefined,
916
1035
  from: From,
917
- pagination: { calls: number } | undefined,
1036
+ pagination: Pagination | undefined,
1037
+ startTime?: number,
918
1038
  ): Execution {
919
1039
  // A function in a component runs with no identity, whoever called it, as
920
1040
  // Convex gives a component no `ctx.auth` of the caller's. A nested call in
@@ -925,6 +1045,18 @@ export class World {
925
1045
  : from.parent !== undefined
926
1046
  ? from.parent.identity
927
1047
  : from.caller.identity;
1048
+ // A nested call runs under its caller's limits, which its budget can only
1049
+ // tighten, and gives them back when it ends (`run_udf`,
1050
+ // `crates/isolate/src/environment/udf/async_syscall.rs:700-706`, and
1051
+ // `commit_subtransaction`, `crates/database/src/transaction.rs:431-441`).
1052
+ const inherited =
1053
+ attempt === undefined || from.parent?.attempt === undefined
1054
+ ? Metering.defaults
1055
+ : from.parent.limits;
1056
+ const limits =
1057
+ attempt === undefined || from.budget === undefined
1058
+ ? inherited
1059
+ : Metering.fromBudget(from.budget, attempt.meter.used(), inherited);
928
1060
  return {
929
1061
  world: this,
930
1062
  componentPath: functionPath.componentPath,
@@ -934,11 +1066,13 @@ export class World {
934
1066
  identity,
935
1067
  request: from.request,
936
1068
  startTime:
937
- from.parent !== undefined && from.parent.udfType !== "action"
1069
+ startTime ??
1070
+ (from.parent !== undefined && from.parent.udfType !== "action"
938
1071
  ? from.parent.startTime
939
- : this.now(),
1072
+ : this.now()),
940
1073
  pagination,
941
1074
  turn: new Turn(),
1075
+ limits,
942
1076
  };
943
1077
  }
944
1078
 
@@ -948,21 +1082,29 @@ export class World {
948
1082
  args: any,
949
1083
  from: From,
950
1084
  snapshotOnly = false,
1085
+ top?: TopQuery,
951
1086
  ): Promise<any> {
952
1087
  const parentAttempt = from.parent?.attempt;
953
1088
  const isNested = parentAttempt !== undefined;
954
1089
  const attempt =
955
- parentAttempt === undefined
1090
+ top?.attempt ??
1091
+ (parentAttempt === undefined
956
1092
  ? new KernelAttempt(this.state)
957
1093
  : snapshotOnly
958
- ? new KernelAttempt(parentAttempt.snapshot)
959
- : parentAttempt;
1094
+ ? parentAttempt.staleSnapshot()
1095
+ : parentAttempt);
960
1096
  const execution = this.execution(
961
1097
  functionPath,
962
1098
  "query",
963
1099
  attempt,
964
1100
  from,
965
- (isNested && from.parent?.pagination) || { calls: 0 },
1101
+ top?.pagination ??
1102
+ ((isNested && from.parent?.pagination) || {
1103
+ calls: 0,
1104
+ journal: null,
1105
+ end: null,
1106
+ }),
1107
+ top?.startTime,
966
1108
  );
967
1109
  const q = queryGeneric({
968
1110
  handler: (ctx: any, a: any) =>
@@ -1278,18 +1420,83 @@ export class World {
1278
1420
 
1279
1421
  private view(execution: Execution): KernelQuery.TableView {
1280
1422
  const attempt = transaction(execution);
1423
+ const { componentPath, limits } = execution;
1281
1424
  return {
1282
- schema: this.schema(execution.componentPath),
1283
- scan: (tableName) => attempt.scan(execution.componentPath, tableName),
1284
- read: (interval, after) =>
1285
- attempt.readInterval({
1425
+ schema: this.schema(componentPath),
1426
+ scan: (tableName) => attempt.scan(componentPath, tableName),
1427
+ read: ({ index, ...interval }, after) => {
1428
+ const advance = attempt.readInterval({
1286
1429
  ...interval,
1287
- componentPath: execution.componentPath,
1430
+ componentPath,
1288
1431
  after,
1289
- }),
1432
+ });
1433
+ return (through) => {
1434
+ advance(through);
1435
+ const examined = Metering.examined(
1436
+ interval.range,
1437
+ interval.order,
1438
+ after,
1439
+ through,
1440
+ );
1441
+ if (examined !== null) {
1442
+ refuse(
1443
+ attempt.meter.readInterval(
1444
+ indexKey(componentPath, interval.table, index),
1445
+ examined,
1446
+ limits,
1447
+ ),
1448
+ );
1449
+ }
1450
+ };
1451
+ },
1452
+ examine: (table, doc) =>
1453
+ refuse(attempt.meter.readDocument(doc, isSystemTable(table), limits)),
1290
1454
  };
1291
1455
  }
1292
1456
 
1457
+ /**
1458
+ * Read one document by ID, as the binary's `get_inner` does for a get and for
1459
+ * the document a patch, replace, delete or cancel changes: it reads the ID's
1460
+ * interval of the table's `by_id` index, then charges the document if there
1461
+ * is one (`crates/database/src/transaction.rs:1188-1250`).
1462
+ */
1463
+ private readById(
1464
+ attempt: KernelAttempt,
1465
+ execution: Execution,
1466
+ id: DocumentId,
1467
+ ): StoredDocument | null {
1468
+ const { componentPath, limits } = execution;
1469
+ const table = tableNameFromId(id)!;
1470
+ attempt.readDocument(componentPath, id);
1471
+ refuse(
1472
+ attempt.meter.readInterval(
1473
+ indexKey(componentPath, table, "by_id"),
1474
+ Metering.point([id]),
1475
+ limits,
1476
+ ),
1477
+ );
1478
+ const doc = attempt.get(componentPath, id);
1479
+ if (doc !== null) {
1480
+ refuse(attempt.meter.readDocument(doc, isSystemTable(table), limits));
1481
+ }
1482
+ return doc;
1483
+ }
1484
+
1485
+ /**
1486
+ * Stage a write to a user table, counted against the execution's limits
1487
+ * first. A write over a limit is not staged (`Writes::update`,
1488
+ * `crates/database/src/writes.rs:241-310`).
1489
+ */
1490
+ private stage(
1491
+ execution: Execution,
1492
+ id: DocumentId,
1493
+ doc: StoredDocument | null,
1494
+ ) {
1495
+ const attempt = transaction(execution);
1496
+ refuse(attempt.meter.write(doc, execution.limits));
1497
+ attempt.write(execution.componentPath, id, doc);
1498
+ }
1499
+
1293
1500
  private generateId(table: string): DocumentId {
1294
1501
  const counter = String(this.nextDocId++).padStart(
1295
1502
  TYPICAL_CONVEX_ID_LENGTH - table.length,
@@ -1393,9 +1600,12 @@ export class World {
1393
1600
  );
1394
1601
  }
1395
1602
  }
1396
- return JSON.stringify(
1397
- convexToJson(KernelQuery.paginate(this.view(execution), args) as any),
1398
- );
1603
+ const page = KernelQuery.paginate(this.view(execution), {
1604
+ ...args,
1605
+ endCursor: args.endCursor ?? pagination?.journal ?? null,
1606
+ });
1607
+ if (pagination) pagination.end = page.continueCursor;
1608
+ return JSON.stringify(convexToJson(page as any));
1399
1609
  }
1400
1610
  case "1.0/insert":
1401
1611
  return JSON.stringify({
@@ -1412,6 +1622,15 @@ export class World {
1412
1622
  return JSON.stringify({});
1413
1623
  case "1.0/count": {
1414
1624
  const attempt = transaction(execution);
1625
+ // The binary counts a table by reading its whole `by_id` index, and no
1626
+ // document (`count_tablet`, `crates/database/src/bootstrap_model/table.rs:133-160`).
1627
+ refuse(
1628
+ attempt.meter.readInterval(
1629
+ indexKey(execution.componentPath, args.table, "by_id"),
1630
+ Metering.everything,
1631
+ execution.limits,
1632
+ ),
1633
+ );
1415
1634
  attempt.readInterval({
1416
1635
  kind: "interval",
1417
1636
  componentPath: execution.componentPath,
@@ -1458,8 +1677,12 @@ export class World {
1458
1677
  return run.then((value) => JSON.stringify(convexToJson(value)));
1459
1678
  }
1460
1679
  case "1.0/runUdf": {
1461
- const { udfType, args: udfArgs } = args;
1462
- const from: From = { request: execution.request, parent: execution };
1680
+ const { udfType, args: udfArgs, transactionLimits } = args;
1681
+ const from: From = {
1682
+ request: execution.request,
1683
+ parent: execution,
1684
+ budget: budgetOf(transactionLimits),
1685
+ };
1463
1686
  const functionPath = functionPathOf(args, execution.componentPath);
1464
1687
  const value = jsonToConvex(udfArgs);
1465
1688
  const run =
@@ -1498,15 +1721,11 @@ export class World {
1498
1721
  ),
1499
1722
  );
1500
1723
  case "1.0/cancel_job":
1501
- this.cancelJob(
1502
- transaction(execution),
1503
- execution.componentPath,
1504
- args.id,
1505
- );
1724
+ this.cancelJob(transaction(execution), execution, args.id);
1506
1725
  return JSON.stringify({});
1507
1726
  case "1.0/actions/cancel_job":
1508
1727
  this.commitNow((attempt) =>
1509
- this.cancelJob(attempt, execution.componentPath, args.id),
1728
+ this.cancelJob(attempt, execution, args.id),
1510
1729
  );
1511
1730
  return JSON.stringify({});
1512
1731
  case "1.0/storageDelete":
@@ -1514,7 +1733,14 @@ export class World {
1514
1733
  case "1.0/storageGenerateUploadUrl":
1515
1734
  throw KernelRefusal.rowPending("storage", 6);
1516
1735
  case "1.0/getTransactionMetrics":
1517
- throw KernelRefusal.rowPending("transaction limits", 6);
1736
+ // An action has no transaction, and the binary's action syscalls do
1737
+ // not know this one (`crates/isolate/src/environment/action/async_syscall.rs:105`).
1738
+ if (execution.attempt === undefined) {
1739
+ throw new Error(`Unknown async operation ${op}`);
1740
+ }
1741
+ return JSON.stringify(
1742
+ execution.attempt.meter.metrics(execution.limits),
1743
+ );
1518
1744
  case "1.0/actions/vectorSearch":
1519
1745
  throw KernelRefusal.requiresBinary("vectorSearch");
1520
1746
  default:
@@ -1536,9 +1762,7 @@ export class World {
1536
1762
 
1537
1763
  private get(execution: Execution, tableName: unknown, id: unknown) {
1538
1764
  validateId(tableName, id);
1539
- const attempt = transaction(execution);
1540
- attempt.readDocument(execution.componentPath, id);
1541
- return attempt.get(execution.componentPath, id);
1765
+ return this.readById(transaction(execution), execution, id);
1542
1766
  }
1543
1767
 
1544
1768
  private insert(execution: Execution, table: string, value: any): DocumentId {
@@ -1549,7 +1773,12 @@ export class World {
1549
1773
  value,
1550
1774
  );
1551
1775
  const _id = this.generateId(table);
1552
- attempt.write(execution.componentPath, _id, {
1776
+ // A new ID joins the table's `by_id` reads without counting as one.
1777
+ attempt.meter.deriveInterval(
1778
+ indexKey(execution.componentPath, table, "by_id"),
1779
+ Metering.point([_id]),
1780
+ );
1781
+ this.stage(execution, _id, {
1553
1782
  ...value,
1554
1783
  _id,
1555
1784
  _creationTime: this.creationTime(execution.startTime),
@@ -1581,9 +1810,7 @@ export class World {
1581
1810
  `Invalid argument \`value\` in \`db.${write}\`, expected object but got '${typeof value}': ${value as any}`,
1582
1811
  );
1583
1812
  }
1584
- const attempt = transaction(execution);
1585
- attempt.readDocument(execution.componentPath, id);
1586
- const document = attempt.get(execution.componentPath, id);
1813
+ const document = this.readById(transaction(execution), execution, id);
1587
1814
  if (document === null) {
1588
1815
  throw new Error(
1589
1816
  write === "delete"
@@ -1628,16 +1855,14 @@ export class World {
1628
1855
  const document = this.existing(execution, tableName, id, "patch", value);
1629
1856
  const { _id, _creationTime, ...fields } = document;
1630
1857
  const merged = { ...fields, ...this.fields(document, value) };
1858
+ const next = { _id, _creationTime, ...merged } as StoredDocument;
1859
+ if (sameDocument(document, next)) return;
1631
1860
  KernelQuery.validateDocument(
1632
1861
  this.schema(execution.componentPath),
1633
1862
  tableNameFromId(_id)!,
1634
1863
  merged,
1635
1864
  );
1636
- transaction(execution).write(execution.componentPath, _id, {
1637
- _id,
1638
- _creationTime,
1639
- ...merged,
1640
- } as StoredDocument);
1865
+ this.stage(execution, _id, next);
1641
1866
  }
1642
1867
 
1643
1868
  private replace(
@@ -1648,16 +1873,18 @@ export class World {
1648
1873
  ) {
1649
1874
  const document = this.existing(execution, tableName, id, "replace", value);
1650
1875
  const fields = this.fields(document, value);
1876
+ const next = {
1877
+ ...fields,
1878
+ _id: document._id,
1879
+ _creationTime: document._creationTime,
1880
+ } as StoredDocument;
1881
+ if (sameDocument(document, next)) return;
1651
1882
  KernelQuery.validateDocument(
1652
1883
  this.schema(execution.componentPath),
1653
1884
  tableNameFromId(document._id)!,
1654
1885
  fields,
1655
1886
  );
1656
- transaction(execution).write(execution.componentPath, document._id, {
1657
- ...fields,
1658
- _id: document._id,
1659
- _creationTime: document._creationTime,
1660
- } as StoredDocument);
1887
+ this.stage(execution, document._id, next);
1661
1888
  }
1662
1889
 
1663
1890
  private remove(execution: Execution, tableName: unknown, id: unknown) {
@@ -1668,7 +1895,7 @@ export class World {
1668
1895
  "delete",
1669
1896
  undefined,
1670
1897
  );
1671
- transaction(execution).write(execution.componentPath, document._id, null);
1898
+ this.stage(execution, document._id, null);
1672
1899
  }
1673
1900
  }
1674
1901
 
@@ -1681,8 +1908,99 @@ type From =
1681
1908
  readonly caller: Caller;
1682
1909
  readonly request: Request;
1683
1910
  readonly parent?: undefined;
1911
+ readonly budget?: undefined;
1684
1912
  }
1685
- | { readonly parent: Execution; readonly request: Request };
1913
+ | {
1914
+ readonly parent: Execution;
1915
+ readonly request: Request;
1916
+ readonly budget?: Metering.Budget | undefined;
1917
+ };
1918
+
1919
+ const refuse = (error: Metering.TransactionLimitExceeded | undefined) => {
1920
+ if (error !== undefined) throw error;
1921
+ };
1922
+
1923
+ /**
1924
+ * One index of one table in one component, as the binary keys a read set.
1925
+ */
1926
+ const indexKey = (componentPath: string, table: string, index: string) =>
1927
+ `${componentPath}\u0000${table}\u0000${index}`;
1928
+
1929
+ const isSystemTable = (table: string) => table.startsWith("_");
1930
+
1931
+ const isFieldMap = (value: unknown): value is object =>
1932
+ typeof value === "object" && value !== null && isSimpleObject(value);
1933
+
1934
+ /**
1935
+ * Whether two stored values are the same value, an unresolved commit timestamp
1936
+ * included. The binary compares pending values structurally, so a pending
1937
+ * timestamp equals only another pending timestamp, at any depth (`PendingValue`
1938
+ * derives `PartialEq`, `crates/value/src/pending.rs:56-75`, as
1939
+ * `PendingDocument` does, `crates/common/src/document.rs:624-632`). Concrete
1940
+ * values compare by `ConvexValue`'s total order
1941
+ * (`crates/value/src/sorting.rs:468-472`), which `compareValues` follows.
1942
+ * `compareValues` alone ranks a pending timestamp as the largest int64, so it
1943
+ * cannot tell the two apart. An object field set to `undefined` is absent.
1944
+ */
1945
+ const sameValue = (a: unknown, b: unknown): boolean => {
1946
+ const aPending = a instanceof CommitTsPlaceholder;
1947
+ const bPending = b instanceof CommitTsPlaceholder;
1948
+ if (aPending || bPending) return aPending && bPending;
1949
+ if (Array.isArray(a) || Array.isArray(b)) {
1950
+ return (
1951
+ Array.isArray(a) &&
1952
+ Array.isArray(b) &&
1953
+ a.length === b.length &&
1954
+ a.every((value, i) => sameValue(value, b[i]))
1955
+ );
1956
+ }
1957
+ if (isFieldMap(a) || isFieldMap(b)) {
1958
+ if (!isFieldMap(a) || !isFieldMap(b)) return false;
1959
+ const fields = (value: object) =>
1960
+ Object.entries(value).filter(([, field]) => field !== undefined);
1961
+ const aFields = fields(a);
1962
+ const bFields = new Map(fields(b));
1963
+ return (
1964
+ aFields.length === bFields.size &&
1965
+ aFields.every(
1966
+ ([key, field]) =>
1967
+ bFields.has(key) && sameValue(field, bFields.get(key)),
1968
+ )
1969
+ );
1970
+ }
1971
+ return compareValues(a as Value, b as Value) === 0;
1972
+ };
1973
+
1974
+ /**
1975
+ * Whether a patch or replace leaves a document as it was. The binary then
1976
+ * writes nothing (`patch_inner` and `replace_inner`,
1977
+ * `crates/database/src/transaction.rs:713-790`).
1978
+ */
1979
+ const sameDocument = (before: StoredDocument, after: StoredDocument) =>
1980
+ sameValue(before, after);
1981
+
1982
+ /**
1983
+ * A nested call's `transactionLimits`. The binary reads each field as an
1984
+ * unsigned integer and ignores fields it does not know (`TransactionLimits`,
1985
+ * `crates/database/src/execution_size.rs:62-95`).
1986
+ */
1987
+ const budgetOf = (limits: unknown): Metering.Budget | undefined => {
1988
+ if (limits === undefined || limits === null) return undefined;
1989
+ const budget: Partial<Record<keyof Metering.Limits, number>> = {};
1990
+ for (const metric of Object.keys(Metering.defaults) as Array<
1991
+ keyof Metering.Limits
1992
+ >) {
1993
+ const value = (limits as Record<string, unknown>)[metric];
1994
+ if (value === undefined) continue;
1995
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
1996
+ throw new Error(
1997
+ `Invalid arguments for \`runUdf\`: transactionLimits.${metric} must be a non-negative integer, got ${JSON.stringify(value)}`,
1998
+ );
1999
+ }
2000
+ budget[metric] = value;
2001
+ }
2002
+ return budget;
2003
+ };
1686
2004
 
1687
2005
  const closed = (execution: Execution, operation: string) =>
1688
2006
  new KernelRefusal.ClosedAttempt({
@@ -1700,7 +2018,10 @@ const transaction = (execution: Execution): KernelAttempt => {
1700
2018
  };
1701
2019
 
1702
2020
  const queryAuth = (execution: Execution) => ({
1703
- getUserIdentity: () => Promise.resolve(execution.identity),
2021
+ getUserIdentity: () => {
2022
+ execution.attempt?.observeIdentity();
2023
+ return Promise.resolve(execution.identity);
2024
+ },
1704
2025
  });
1705
2026
 
1706
2027
  const mutationAuth = (execution: Execution) => ({
@@ -214,7 +214,8 @@ const pin = (
214
214
  const host = globalThis;
215
215
  const previous = { Date: host.Date, Math: host.Math, crypto: host.crypto };
216
216
  // On the kernel, `Date` already answers the running function's time on the
217
- // kernel's clock, so `runAfter` schedules on it. The bridge leaves it there.
217
+ // kernel's clock, so `runAfter` schedules on it, and a query that reads it
218
+ // records the read. The bridge leaves it there.
218
219
  const kernelDate = Clock.answersInvocation(previous.Date);
219
220
  const pinned = {
220
221
  Date: kernelDate