@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.
- package/dist/internal/Attempt.d.ts +14 -1
- package/dist/internal/Attempt.d.ts.map +1 -1
- package/dist/internal/Attempt.js +18 -1
- package/dist/internal/Attempt.js.map +1 -1
- package/dist/internal/Execution.d.ts +13 -0
- package/dist/internal/Execution.d.ts.map +1 -1
- package/dist/internal/Execution.js +137 -33
- package/dist/internal/Execution.js.map +1 -1
- package/dist/internal/KernelQuery.d.ts +8 -3
- package/dist/internal/KernelQuery.d.ts.map +1 -1
- package/dist/internal/KernelQuery.js +10 -3
- package/dist/internal/KernelQuery.js.map +1 -1
- package/dist/internal/Metering.d.ts +186 -0
- package/dist/internal/Metering.d.ts.map +1 -0
- package/dist/internal/Metering.js +401 -0
- package/dist/internal/Metering.js.map +1 -0
- package/package.json +3 -3
- package/src/internal/Attempt.ts +24 -0
- package/src/internal/Execution.ts +257 -39
- package/src/internal/KernelQuery.ts +17 -6
- package/src/internal/Metering.ts +520 -0
|
@@ -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.
|
|
3
|
+
"version": "10.3.1-fork.92",
|
|
4
4
|
"peerDependencies": {
|
|
5
|
-
"@confect/core": "npm:@gaganref/confect-core@10.3.1-fork.
|
|
6
|
-
"@confect/server": "npm:@gaganref/confect-server@10.3.1-fork.
|
|
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"
|
package/src/internal/Attempt.ts
CHANGED
|
@@ -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 {
|
|
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,
|
|
977
|
+
private cancelJob(attempt: KernelAttempt, execution: Execution, id: any) {
|
|
978
|
+
const { componentPath } = execution;
|
|
946
979
|
validateId(Scheduling.table, id);
|
|
947
|
-
|
|
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
|
-
?
|
|
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(
|
|
1380
|
-
scan: (tableName) => attempt.scan(
|
|
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
|
|
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 = {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
| {
|
|
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({
|