@gaganref/confect-test 10.3.1-fork.91 → 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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Metering.js","names":[],"sources":["../../src/internal/Metering.ts"],"sourcesContent":["// The limits, counters and messages are convex-backend's, read at the commit\n// the fork's local backend runs (`precompiled-2026-10-28-cccbade`). Paths below\n// are in that repository.\n\nimport type { TransactionMetrics } from \"convex/server\";\nimport { compareValues, getConvexSize, type Value } from \"convex/values\";\nimport * as Schema from \"effect/Schema\";\nimport type { SerializedRangeExpression } from \"./KernelValues.js\";\nimport { evaluateQueryValue } from \"./KernelValues.js\";\n\n/**\n * The ceilings one function execution runs under, the binary's\n * `TransactionLimits` (`crates/database/src/execution_size.rs:62-95`).\n */\nexport type Limits = { readonly [K in keyof TransactionMetrics]: number };\n\n/**\n * The binary's default ceilings (`crates/common/src/knobs.rs`).\n */\nexport const defaults: Limits = {\n // `TRANSACTION_MAX_READ_SIZE_BYTES`, knobs.rs:645-647.\n bytesRead: 1 << 24,\n // `TRANSACTION_MAX_READ_SIZE_ROWS`, knobs.rs:641-642.\n documentsRead: 32000,\n // `TRANSACTION_MAX_READ_SET_INTERVALS`, knobs.rs:657-658.\n databaseQueries: 4096,\n // `TRANSACTION_MAX_NUM_USER_WRITES`, knobs.rs:406-407.\n documentsWritten: 16000,\n // `TRANSACTION_MAX_USER_WRITE_SIZE_BYTES`, knobs.rs:410-412.\n bytesWritten: 1 << 24,\n // `TRANSACTION_MAX_NUM_SCHEDULED`, knobs.rs:514-515.\n functionsScheduled: 1000,\n // `TRANSACTION_MAX_SCHEDULED_TOTAL_ARGUMENT_SIZE_BYTES`, knobs.rs:530-536.\n scheduledFunctionArgsBytes: 1 << 24,\n // `TRANSACTION_MAX_NUM_FILES_WRITTEN`, knobs.rs:539-540.\n filesWritten: 10,\n // `TRANSACTION_MAX_FILE_WRITE_SIZE_BYTES`, knobs.rs:544-546.\n fileWriteBytes: 1 << 24,\n // `TRANSACTION_MAX_NUM_FILES_READ`, knobs.rs:549-550.\n filesRead: 10,\n // `TRANSACTION_MAX_FILE_READ_SIZE_BYTES`, knobs.rs:554-556.\n fileReadBytes: 1 << 24,\n};\n\n/**\n * What a nested call may use on top of what its transaction has used, as\n * `ctx.runQuery` and `ctx.runMutation` take it in `transactionLimits`.\n */\nexport type Budget = Partial<Limits>;\n\n/**\n * A nested call's ceilings: each one what the transaction has used plus the\n * call's budget, never above the caller's (`TransactionLimits::from_budget`,\n * execution_size.rs:97-160). An unset field leaves the caller's ceiling.\n */\nexport const fromBudget = (\n budget: Budget,\n used: Limits,\n ceiling: Limits,\n): Limits =>\n Object.fromEntries(\n (Object.keys(ceiling) as Array<keyof Limits>).map((metric) => [\n metric,\n Math.min(\n used[metric] + (budget[metric] ?? ceiling[metric]),\n ceiling[metric],\n ),\n ]),\n ) as unknown as Limits;\n\nconst overLimitHelp =\n \"Consider using smaller limits in your queries, paginating your queries, or using indexed queries with a selective index range expressions.\";\n\n/**\n * A function execution crossed one of its ceilings. `code` is the binary's\n * short message and `message` its text, which the function receives as a\n * catchable error.\n */\nexport class TransactionLimitExceeded extends Schema.TaggedError<TransactionLimitExceeded>()(\n \"TransactionLimitExceeded\",\n {\n code: Schema.Literals([\n \"TooManyReads\",\n \"TooManyDocumentsRead\",\n \"TooManyBytesRead\",\n \"TooManyWrites\",\n \"TooManyBytesWritten\",\n \"TooManyFunctionsScheduled\",\n \"ScheduledFunctionsArgumentsTooLarge\",\n ]),\n limit: Schema.Int,\n message: Schema.String,\n },\n) {}\n\nconst byteUnits: ReadonlyArray<readonly [number, string]> = [\n [2 ** 30, \"GiB\"],\n [1_000_000_000, \"GB\"],\n [2 ** 20, \"MiB\"],\n [1_000_000, \"MB\"],\n [2 ** 10, \"KiB\"],\n [1_000, \"KB\"],\n];\n\n/**\n * `common::fmt::format_bytes` (`crates/common/src/fmt.rs:29-47`).\n */\nexport const formatBytes = (n: number): string => {\n if (n === 0) return \"0 bytes\";\n for (const [unit, name] of byteUnits) {\n if (n < unit) continue;\n if (n % unit === 0) return `${n / unit} ${name}`;\n if ((n * 10) % unit === 0) {\n return `${Math.floor(n / unit)}.${Math.floor((n * 10) / unit) % 10} ${name}`;\n }\n }\n return `${n} bytes`;\n};\n\n/**\n * A position in an index's key space: just before, or just after, every key\n * that starts with `key`.\n */\ninterface Bound {\n readonly key: ReadonlyArray<Value | undefined>;\n readonly side: -1 | 1;\n}\n\n/**\n * The keys from `start` up to `end`.\n */\nexport interface KeyInterval {\n readonly start: Bound;\n readonly end: Bound;\n}\n\nconst compareBounds = (a: Bound, b: Bound): number => {\n const shared = Math.min(a.key.length, b.key.length);\n for (let i = 0; i < shared; i++) {\n const cmp = compareValues(a.key[i] as Value, b.key[i] as Value);\n if (cmp !== 0) return cmp;\n }\n if (a.key.length === b.key.length) return a.side - b.side;\n return a.key.length < b.key.length ? a.side : -b.side;\n};\n\nconst maxBound = (a: Bound, b: Bound) => (compareBounds(a, b) >= 0 ? a : b);\nconst minBound = (a: Bound, b: Bound) => (compareBounds(a, b) <= 0 ? a : b);\n\n/**\n * The one key `key`, as a read or write of a document by ID records it.\n */\nexport const point = (key: ReadonlyArray<Value | undefined>): KeyInterval => ({\n start: { key, side: -1 },\n end: { key, side: 1 },\n});\n\n/**\n * Every key of an index.\n */\nexport const everything: KeyInterval = point([]);\n\n/**\n * The part of an index range a query has examined: from where it starts, past\n * the cursor `after`, through `through` in its order. `null` when it has\n * examined nothing.\n */\nexport const examined = (\n range: ReadonlyArray<SerializedRangeExpression>,\n order: \"asc\" | \"desc\",\n after: ReadonlyArray<Value | undefined> | null,\n through: ReadonlyArray<Value | undefined> | \"end\",\n): KeyInterval | null => {\n const equal: Array<Value | undefined> = [];\n let start: Bound | undefined;\n let end: Bound | undefined;\n for (const expression of range) {\n const value = evaluateQueryValue(expression.value);\n switch (expression.type) {\n case \"Eq\":\n equal.push(value);\n break;\n case \"Gt\":\n case \"Gte\":\n start = {\n key: [...equal, value],\n side: expression.type === \"Gt\" ? 1 : -1,\n };\n break;\n case \"Lt\":\n case \"Lte\":\n end = {\n key: [...equal, value],\n side: expression.type === \"Lt\" ? -1 : 1,\n };\n break;\n }\n }\n start ??= { key: equal, side: -1 };\n end ??= { key: equal, side: 1 };\n if (after !== null) {\n if (order === \"asc\") start = maxBound(start, { key: after, side: 1 });\n else end = minBound(end, { key: after, side: -1 });\n }\n if (through !== \"end\") {\n if (order === \"asc\") end = minBound(end, { key: through, side: 1 });\n else start = maxBound(start, { key: through, side: -1 });\n }\n return compareBounds(start, end) < 0 ? { start, end } : null;\n};\n\n/**\n * The intervals read from one index, overlapping and adjacent ones merged\n * (`IntervalSet::add`, `crates/common/src/interval/interval_set.rs:150-230`).\n * They stay sorted and disjoint, so an add touches only its neighbours.\n */\nclass IntervalSet {\n private readonly intervals: Array<KeyInterval> = [];\n\n get size(): number {\n return this.intervals.length;\n }\n\n add(interval: KeyInterval) {\n const intervals = this.intervals;\n // The first interval that ends at or after the new one's start.\n let low = 0;\n let high = intervals.length;\n while (low < high) {\n const middle = (low + high) >> 1;\n if (compareBounds(intervals[middle]!.end, interval.start) < 0) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n let last = low;\n let merged = interval;\n while (\n last < intervals.length &&\n compareBounds(intervals[last]!.start, interval.end) <= 0\n ) {\n const other = intervals[last]!;\n merged = {\n start: minBound(merged.start, other.start),\n end: maxBound(merged.end, other.end),\n };\n last++;\n }\n intervals.splice(low, last - low, merged);\n }\n}\n\n/**\n * The counters every frame of one attempt shares: reads and schedules stay when\n * a nested mutation fails, as the binary keeps them outside its subtransaction\n * token (`crates/database/src/transaction.rs:418-454`).\n */\nclass Shared {\n readonly indexes = new Map<string, IntervalSet>();\n intervals = 0;\n documentsRead = 0;\n bytesRead = 0;\n functionsScheduled = 0;\n scheduledFunctionArgsBytes = 0;\n // File storage counters stay at zero until the kernel stores files.\n readonly filesRead = 0;\n readonly fileReadBytes = 0;\n\n /**\n * A stale-snapshot query's counters: the scheduling and file-read counters\n * carried over, reads and intervals from zero (`clone_for_snapshot_query`,\n * `crates/database/src/transaction.rs:1460-1485`).\n */\n forStaleSnapshot(): Shared {\n const copy = new Shared();\n copy.functionsScheduled = this.functionsScheduled;\n copy.scheduledFunctionArgsBytes = this.scheduledFunctionArgsBytes;\n return copy;\n }\n}\n\n/**\n * `a - b`, or 0 when `b` is larger: Rust's `usize::saturating_sub`.\n */\nconst saturatingSub = (a: number, b: number) => (a > b ? a - b : 0);\n\n/**\n * One frame's share of an attempt's accounting. The outermost frame belongs to\n * the top-level function and each nested mutation gets a child. A child's\n * writes count toward its caller's once it merges and are forgotten when it\n * fails, as the binary's nested write set rolls its sizes back\n * (`crates/database/src/writes.rs:96-136`).\n */\nexport class Meter {\n private documentsWritten = 0;\n private bytesWritten = 0;\n // File storage counters stay at zero until the kernel stores files.\n private readonly filesWritten = 0;\n private readonly fileWriteBytes = 0;\n\n constructor(\n private readonly shared: Shared = new Shared(),\n private readonly parent?: Meter,\n ) {}\n\n child(): Meter {\n return new Meter(this.shared, this);\n }\n\n /**\n * The meter of a query run on a stale snapshot. The binary runs it on a clone\n * of the transaction that keeps the scheduling counters and starts reads and\n * writes from zero, then drops the clone, so nothing it counts reaches this\n * meter (`clone_for_snapshot_query`,\n * `crates/database/src/transaction.rs:1460-1485`; `run_udf`,\n * `crates/isolate/src/environment/udf/async_syscall.rs:695-776`).\n */\n forStaleSnapshot(): Meter {\n return new Meter(this.shared.forStaleSnapshot());\n }\n\n /**\n * The child's writes become its caller's.\n */\n merge() {\n if (this.parent === undefined) {\n throw new Error(\"The outermost meter of an attempt has no caller.\");\n }\n this.parent.documentsWritten += this.documentsWritten;\n this.parent.bytesWritten += this.bytesWritten;\n }\n\n private chain(): Array<Meter> {\n return this.parent === undefined ? [this] : [...this.parent.chain(), this];\n }\n\n /**\n * How much of each limit the execution has used.\n */\n used(): Limits {\n const frames = this.chain();\n const sum = (of: (meter: Meter) => number) =>\n frames.reduce((total, meter) => total + of(meter), 0);\n const shared = this.shared;\n return {\n bytesRead: shared.bytesRead,\n documentsRead: shared.documentsRead,\n databaseQueries: shared.intervals,\n documentsWritten: sum((meter) => meter.documentsWritten),\n bytesWritten: sum((meter) => meter.bytesWritten),\n functionsScheduled: shared.functionsScheduled,\n scheduledFunctionArgsBytes: shared.scheduledFunctionArgsBytes,\n filesWritten: sum((meter) => meter.filesWritten),\n fileWriteBytes: sum((meter) => meter.fileWriteBytes),\n filesRead: shared.filesRead,\n fileReadBytes: shared.fileReadBytes,\n };\n }\n\n /**\n * What `getTransactionMetrics` answers (`tx_metrics`,\n * `crates/isolate/src/environment/udf/async_syscall.rs:947-993`).\n */\n metrics(limits: Limits): TransactionMetrics {\n const used = this.used();\n return Object.fromEntries(\n (Object.keys(limits) as Array<keyof Limits>).map((metric) => [\n metric,\n { used: used[metric], remaining: limits[metric] - used[metric] },\n ]),\n ) as unknown as TransactionMetrics;\n }\n\n private set(index: string): IntervalSet {\n const set = this.shared.indexes.get(index) ?? new IntervalSet();\n this.shared.indexes.set(index, set);\n return set;\n }\n\n /**\n * A read the function made of `interval` in `index`, counted as the binary\n * counts it: the total drops by the index's interval count before the read,\n * saturating at zero, and rises by its count after\n * (`record_indexed_directly`, `crates/database/src/reads.rs:447-480`). The\n * index's count includes the intervals of IDs this execution inserted, which\n * joined it without counting (`deriveInterval`), so a read that covers them\n * can move the total by more than the read itself. The binary starts the\n * total at the system-table intervals it reads to run the function, which the\n * kernel does not simulate. While neither total saturates they differ by\n * exactly that share. A read that saturates only the kernel's lower total\n * narrows the gap, and it never widens or turns negative (see the\n * `SystemTableReadIntervals` boundary).\n */\n readInterval(\n index: string,\n interval: KeyInterval,\n limits: Limits,\n ): TransactionLimitExceeded | undefined {\n const set = this.set(index);\n const before = set.size;\n set.add(interval);\n const shared = this.shared;\n shared.intervals = saturatingSub(shared.intervals, before) + set.size;\n // Both terms are counts, so the total never goes below zero.\n if (!Number.isSafeInteger(shared.intervals) || shared.intervals < 0) {\n throw new Error(\n `The kernel counted ${shared.intervals} intervals read, which is no count.`,\n );\n }\n return shared.intervals > limits.databaseQueries\n ? new TransactionLimitExceeded({\n code: \"TooManyReads\",\n limit: limits.databaseQueries,\n message: `Too many reads in a single function execution (limit: ${limits.databaseQueries}). ${overLimitHelp}`,\n })\n : undefined;\n }\n\n /**\n * A read the binary takes for a write rather than for the function: a new\n * document's ID (`register_new_id`, writes.rs:426-441). It joins the index's\n * intervals and leaves the total as it was (`record_indexed_derived` calls\n * `_record_indexed` and ignores its counts, reads.rs:333-344), so a later\n * read of that index starts from a count that includes it.\n */\n deriveInterval(index: string, interval: KeyInterval) {\n this.set(index).add(interval);\n }\n\n /**\n * A document the function read. A system table's document counts toward no\n * limit (`record_read_document`, reads.rs:365-445).\n */\n readDocument(\n document: Value,\n system: boolean,\n limits: Limits,\n ): TransactionLimitExceeded | undefined {\n if (system) return undefined;\n const shared = this.shared;\n shared.documentsRead += 1;\n shared.bytesRead += getConvexSize(document);\n if (shared.documentsRead > limits.documentsRead) {\n return new TransactionLimitExceeded({\n code: \"TooManyDocumentsRead\",\n limit: limits.documentsRead,\n message: `Too many documents read in a single function execution (limit: ${limits.documentsRead}). ${overLimitHelp}`,\n });\n }\n if (shared.bytesRead > limits.bytesRead) {\n return new TransactionLimitExceeded({\n code: \"TooManyBytesRead\",\n limit: limits.bytesRead,\n message: `Too many bytes read in a single function execution (limit: ${limits.bytesRead} bytes). ${overLimitHelp}`,\n });\n }\n return undefined;\n }\n\n /**\n * A write to a user table: the new document, or `null` for a delete. Each\n * write counts, even to a document this execution already wrote\n * (`Writes::update`, writes.rs:241-310).\n */\n write(\n document: Value | null,\n limits: Limits,\n ): TransactionLimitExceeded | undefined {\n this.documentsWritten += 1;\n this.bytesWritten += document === null ? 0 : getConvexSize(document);\n const used = this.used();\n if (used.documentsWritten > limits.documentsWritten) {\n return new TransactionLimitExceeded({\n code: \"TooManyWrites\",\n limit: limits.documentsWritten,\n message: `Too many writes in a single function execution (limit: ${limits.documentsWritten})`,\n });\n }\n if (used.bytesWritten > limits.bytesWritten) {\n return new TransactionLimitExceeded({\n code: \"TooManyBytesWritten\",\n limit: limits.bytesWritten,\n message: `Too many bytes written in a single function execution (limit: ${formatBytes(limits.bytesWritten)})`,\n });\n }\n return undefined;\n }\n\n /**\n * A function scheduled with `args`. The binary checks before it counts, so a\n * refused schedule counts nothing (`check_scheduling_limits`,\n * `crates/model/src/scheduled_jobs/mod.rs:162-196`). Its arguments are sized\n * as the one-element array the scheduler stores.\n */\n schedule(args: Value, limits: Limits): TransactionLimitExceeded | undefined {\n const shared = this.shared;\n const size = getConvexSize([args]);\n if (shared.functionsScheduled >= limits.functionsScheduled) {\n return new TransactionLimitExceeded({\n code: \"TooManyFunctionsScheduled\",\n limit: limits.functionsScheduled,\n message: `Too many functions scheduled by this mutation (limit: ${limits.functionsScheduled})`,\n });\n }\n if (\n shared.scheduledFunctionArgsBytes + size >\n limits.scheduledFunctionArgsBytes\n ) {\n return new TransactionLimitExceeded({\n code: \"ScheduledFunctionsArgumentsTooLarge\",\n limit: limits.scheduledFunctionArgsBytes,\n message: `Too large total size of the arguments of scheduled functions from this mutation (limit: ${limits.scheduledFunctionArgsBytes} bytes)`,\n });\n }\n shared.functionsScheduled += 1;\n shared.scheduledFunctionArgsBytes += size;\n return undefined;\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@gaganref/confect-test",
3
- "version": "10.3.1-fork.91",
3
+ "version": "10.3.1-fork.92",
4
4
  "peerDependencies": {
5
- "@confect/core": "npm:@gaganref/confect-core@10.3.1-fork.91",
6
- "@confect/server": "npm:@gaganref/confect-server@10.3.1-fork.91",
5
+ "@confect/core": "npm:@gaganref/confect-core@10.3.1-fork.92",
6
+ "@confect/server": "npm:@gaganref/confect-server@10.3.1-fork.92",
7
7
  "convex": "^1.32.0",
8
8
  "convex-test": ">=0.0.65 <0.1.0",
9
9
  "effect": ">=4.0.0 <4.1.0"
@@ -4,6 +4,7 @@ import type { Attempt as AttemptNumber } from "./Invocation.js";
4
4
  import * as Kernel from "./Kernel.js";
5
5
  import type { DocumentId, StoredDocument } from "./KernelValues.js";
6
6
  import { tableNameFromId } from "./KernelValues.js";
7
+ import * as Metering from "./Metering.js";
7
8
  import type * as ReadSet from "./ReadSet.js";
8
9
 
9
10
  /**
@@ -44,6 +45,12 @@ export class KernelAttempt {
44
45
  private readonly overlay: Overlay = new Map();
45
46
  private readonly life: Life = { running: 0, started: 0, open: true };
46
47
  private readonly allReads: Array<ReadSet.Read>;
48
+ /**
49
+ * The attempt's transaction-limit accounting. A nested mutation's frame
50
+ * meters into its own child, so its writes merge or drop with it while its
51
+ * reads stay.
52
+ */
53
+ readonly meter: Metering.Meter;
47
54
  /**
48
55
  * Whether the attempt asked for its caller's identity, and the time it read.
49
56
  * Every frame of the attempt shares them, as it shares the reads.
@@ -54,8 +61,10 @@ export class KernelAttempt {
54
61
  readonly snapshot: Kernel.State,
55
62
  readonly number: AttemptNumber = 1,
56
63
  private readonly parent?: KernelAttempt,
64
+ meter?: Metering.Meter,
57
65
  ) {
58
66
  this.allReads = parent?.allReads ?? [];
67
+ this.meter = meter ?? parent?.meter.child() ?? new Metering.Meter();
59
68
  this.observed = parent?.observed ?? { identity: false, time: null };
60
69
  }
61
70
 
@@ -179,6 +188,20 @@ export class KernelAttempt {
179
188
  writes.set(id, doc);
180
189
  }
181
190
 
191
+ /**
192
+ * A query run on this attempt's snapshot rather than its pending writes. It
193
+ * reads and meters on its own, and its meter starts with this attempt's
194
+ * scheduling counters, as the binary's snapshot clone does.
195
+ */
196
+ staleSnapshot(): KernelAttempt {
197
+ return new KernelAttempt(
198
+ this.snapshot,
199
+ this.number,
200
+ undefined,
201
+ this.meter.forStaleSnapshot(),
202
+ );
203
+ }
204
+
182
205
  /**
183
206
  * A nested mutation begins, in a frame of its own. Dropping a failed one is
184
207
  * forgetting it.
@@ -200,6 +223,7 @@ export class KernelAttempt {
200
223
  }
201
224
  for (const [componentPath, writes] of this.overlay)
202
225
  for (const [id, doc] of writes) this.parent.write(componentPath, id, doc);
226
+ this.meter.merge();
203
227
  }
204
228
 
205
229
  /**
@@ -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,
@@ -217,6 +225,11 @@ interface Execution {
217
225
  readonly startTime: number;
218
226
  readonly pagination: Pagination | undefined;
219
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;
220
233
  }
221
234
 
222
235
  /**
@@ -915,7 +928,26 @@ export class World {
915
928
  `Invalid arguments for "${target.udfPath}": a scheduled function cannot take an unresolved placeholder value.`,
916
929
  );
917
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
+ }
918
949
  const id = this.generateId(Scheduling.table);
950
+ attempt.meter.deriveInterval(jobsIndex, Metering.point([id]));
919
951
  attempt.write(
920
952
  execution.componentPath,
921
953
  id,
@@ -942,10 +974,10 @@ export class World {
942
974
  /**
943
975
  * Cancel a job that has not finished. A finished job stays as it ended.
944
976
  */
945
- private cancelJob(attempt: KernelAttempt, componentPath: string, id: any) {
977
+ private cancelJob(attempt: KernelAttempt, execution: Execution, id: any) {
978
+ const { componentPath } = execution;
946
979
  validateId(Scheduling.table, id);
947
- attempt.readDocument(componentPath, id);
948
- const doc = attempt.get(componentPath, id);
980
+ const doc = this.readById(attempt, execution, id);
949
981
  if (doc === null) {
950
982
  throw new Error(`Patch on non-existent document with ID "${id}"`);
951
983
  }
@@ -1013,6 +1045,18 @@ export class World {
1013
1045
  : from.parent !== undefined
1014
1046
  ? from.parent.identity
1015
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);
1016
1060
  return {
1017
1061
  world: this,
1018
1062
  componentPath: functionPath.componentPath,
@@ -1028,6 +1072,7 @@ export class World {
1028
1072
  : this.now()),
1029
1073
  pagination,
1030
1074
  turn: new Turn(),
1075
+ limits,
1031
1076
  };
1032
1077
  }
1033
1078
 
@@ -1046,7 +1091,7 @@ export class World {
1046
1091
  (parentAttempt === undefined
1047
1092
  ? new KernelAttempt(this.state)
1048
1093
  : snapshotOnly
1049
- ? new KernelAttempt(parentAttempt.snapshot)
1094
+ ? parentAttempt.staleSnapshot()
1050
1095
  : parentAttempt);
1051
1096
  const execution = this.execution(
1052
1097
  functionPath,
@@ -1375,18 +1420,83 @@ export class World {
1375
1420
 
1376
1421
  private view(execution: Execution): KernelQuery.TableView {
1377
1422
  const attempt = transaction(execution);
1423
+ const { componentPath, limits } = execution;
1378
1424
  return {
1379
- schema: this.schema(execution.componentPath),
1380
- scan: (tableName) => attempt.scan(execution.componentPath, tableName),
1381
- read: (interval, after) =>
1382
- attempt.readInterval({
1425
+ schema: this.schema(componentPath),
1426
+ scan: (tableName) => attempt.scan(componentPath, tableName),
1427
+ read: ({ index, ...interval }, after) => {
1428
+ const advance = attempt.readInterval({
1383
1429
  ...interval,
1384
- componentPath: execution.componentPath,
1430
+ componentPath,
1385
1431
  after,
1386
- }),
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)),
1387
1454
  };
1388
1455
  }
1389
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
+
1390
1500
  private generateId(table: string): DocumentId {
1391
1501
  const counter = String(this.nextDocId++).padStart(
1392
1502
  TYPICAL_CONVEX_ID_LENGTH - table.length,
@@ -1512,6 +1622,15 @@ export class World {
1512
1622
  return JSON.stringify({});
1513
1623
  case "1.0/count": {
1514
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
+ );
1515
1634
  attempt.readInterval({
1516
1635
  kind: "interval",
1517
1636
  componentPath: execution.componentPath,
@@ -1558,8 +1677,12 @@ export class World {
1558
1677
  return run.then((value) => JSON.stringify(convexToJson(value)));
1559
1678
  }
1560
1679
  case "1.0/runUdf": {
1561
- const { udfType, args: udfArgs } = args;
1562
- 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
+ };
1563
1686
  const functionPath = functionPathOf(args, execution.componentPath);
1564
1687
  const value = jsonToConvex(udfArgs);
1565
1688
  const run =
@@ -1598,15 +1721,11 @@ export class World {
1598
1721
  ),
1599
1722
  );
1600
1723
  case "1.0/cancel_job":
1601
- this.cancelJob(
1602
- transaction(execution),
1603
- execution.componentPath,
1604
- args.id,
1605
- );
1724
+ this.cancelJob(transaction(execution), execution, args.id);
1606
1725
  return JSON.stringify({});
1607
1726
  case "1.0/actions/cancel_job":
1608
1727
  this.commitNow((attempt) =>
1609
- this.cancelJob(attempt, execution.componentPath, args.id),
1728
+ this.cancelJob(attempt, execution, args.id),
1610
1729
  );
1611
1730
  return JSON.stringify({});
1612
1731
  case "1.0/storageDelete":
@@ -1614,7 +1733,14 @@ export class World {
1614
1733
  case "1.0/storageGenerateUploadUrl":
1615
1734
  throw KernelRefusal.rowPending("storage", 6);
1616
1735
  case "1.0/getTransactionMetrics":
1617
- 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
+ );
1618
1744
  case "1.0/actions/vectorSearch":
1619
1745
  throw KernelRefusal.requiresBinary("vectorSearch");
1620
1746
  default:
@@ -1636,9 +1762,7 @@ export class World {
1636
1762
 
1637
1763
  private get(execution: Execution, tableName: unknown, id: unknown) {
1638
1764
  validateId(tableName, id);
1639
- const attempt = transaction(execution);
1640
- attempt.readDocument(execution.componentPath, id);
1641
- return attempt.get(execution.componentPath, id);
1765
+ return this.readById(transaction(execution), execution, id);
1642
1766
  }
1643
1767
 
1644
1768
  private insert(execution: Execution, table: string, value: any): DocumentId {
@@ -1649,7 +1773,12 @@ export class World {
1649
1773
  value,
1650
1774
  );
1651
1775
  const _id = this.generateId(table);
1652
- 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, {
1653
1782
  ...value,
1654
1783
  _id,
1655
1784
  _creationTime: this.creationTime(execution.startTime),
@@ -1681,9 +1810,7 @@ export class World {
1681
1810
  `Invalid argument \`value\` in \`db.${write}\`, expected object but got '${typeof value}': ${value as any}`,
1682
1811
  );
1683
1812
  }
1684
- const attempt = transaction(execution);
1685
- attempt.readDocument(execution.componentPath, id);
1686
- const document = attempt.get(execution.componentPath, id);
1813
+ const document = this.readById(transaction(execution), execution, id);
1687
1814
  if (document === null) {
1688
1815
  throw new Error(
1689
1816
  write === "delete"
@@ -1728,16 +1855,14 @@ export class World {
1728
1855
  const document = this.existing(execution, tableName, id, "patch", value);
1729
1856
  const { _id, _creationTime, ...fields } = document;
1730
1857
  const merged = { ...fields, ...this.fields(document, value) };
1858
+ const next = { _id, _creationTime, ...merged } as StoredDocument;
1859
+ if (sameDocument(document, next)) return;
1731
1860
  KernelQuery.validateDocument(
1732
1861
  this.schema(execution.componentPath),
1733
1862
  tableNameFromId(_id)!,
1734
1863
  merged,
1735
1864
  );
1736
- transaction(execution).write(execution.componentPath, _id, {
1737
- _id,
1738
- _creationTime,
1739
- ...merged,
1740
- } as StoredDocument);
1865
+ this.stage(execution, _id, next);
1741
1866
  }
1742
1867
 
1743
1868
  private replace(
@@ -1748,16 +1873,18 @@ export class World {
1748
1873
  ) {
1749
1874
  const document = this.existing(execution, tableName, id, "replace", value);
1750
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;
1751
1882
  KernelQuery.validateDocument(
1752
1883
  this.schema(execution.componentPath),
1753
1884
  tableNameFromId(document._id)!,
1754
1885
  fields,
1755
1886
  );
1756
- transaction(execution).write(execution.componentPath, document._id, {
1757
- ...fields,
1758
- _id: document._id,
1759
- _creationTime: document._creationTime,
1760
- } as StoredDocument);
1887
+ this.stage(execution, document._id, next);
1761
1888
  }
1762
1889
 
1763
1890
  private remove(execution: Execution, tableName: unknown, id: unknown) {
@@ -1768,7 +1895,7 @@ export class World {
1768
1895
  "delete",
1769
1896
  undefined,
1770
1897
  );
1771
- transaction(execution).write(execution.componentPath, document._id, null);
1898
+ this.stage(execution, document._id, null);
1772
1899
  }
1773
1900
  }
1774
1901
 
@@ -1781,8 +1908,99 @@ type From =
1781
1908
  readonly caller: Caller;
1782
1909
  readonly request: Request;
1783
1910
  readonly parent?: undefined;
1911
+ readonly budget?: undefined;
1784
1912
  }
1785
- | { 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
+ };
1786
2004
 
1787
2005
  const closed = (execution: Execution, operation: string) =>
1788
2006
  new KernelRefusal.ClosedAttempt({