@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
@@ -99,17 +99,19 @@ export const validateDocument = (
99
99
  };
100
100
 
101
101
  /**
102
- * An interval a query reads, before it says where it starts and how far it got.
102
+ * An interval a query reads, before it says where it starts and how far it got,
103
+ * and the index it reads it from.
103
104
  */
104
105
  export type Interval = Omit<
105
106
  ReadSet.IntervalRead,
106
107
  "componentPath" | "after" | "through"
107
- >;
108
+ > & { readonly index: string };
108
109
 
109
110
  /**
110
111
  * What a query reads: one table of one component, as the running attempt sees
111
112
  * it. `read` records an interval in the attempt's read set and returns how to
112
- * advance it.
113
+ * advance it. `examine` charges one document the query examined, before the
114
+ * query's filters see it.
113
115
  */
114
116
  export interface TableView {
115
117
  readonly schema: KernelSchema | null;
@@ -118,6 +120,7 @@ export interface TableView {
118
120
  interval: Interval,
119
121
  after: ReadSet.Key | null,
120
122
  ) => (through: ReadSet.Key | "end") => void;
123
+ readonly examine: (table: string, doc: GenericDocument) => void;
121
124
  }
122
125
 
123
126
  interface ResolvedSource {
@@ -138,9 +141,12 @@ const resolveSource = (
138
141
  let order: "asc" | "desc";
139
142
  let table: string;
140
143
  let range: ReadonlyArray<SerializedRangeExpression> = [];
144
+ let index: string;
141
145
  switch (source.type) {
142
146
  case "FullTableScan": {
143
147
  table = source.tableName;
148
+ // The binary scans a whole table through its creation-time index.
149
+ index = "by_creation_time";
144
150
  docs = [...view.scan(source.tableName)];
145
151
  order = source.order ?? "asc";
146
152
  fieldPathsToSortBy = ["_creationTime"];
@@ -152,6 +158,7 @@ const resolveSource = (
152
158
  string,
153
159
  ];
154
160
  table = tableName;
161
+ index = indexName;
155
162
  range = source.range;
156
163
  order = source.order ?? "asc";
157
164
  let fields: Array<string>;
@@ -160,18 +167,18 @@ const resolveSource = (
160
167
  } else if (indexName === "by_id") {
161
168
  fields = ["_id"];
162
169
  } else {
163
- const index = view.schema?.tables
170
+ const declared = view.schema?.tables
164
171
  .get(tableName)
165
172
  ?.indexes.find(
166
173
  ({ indexDescriptor }) => indexDescriptor === indexName,
167
174
  );
168
- if (index === undefined) {
175
+ if (declared === undefined) {
169
176
  throw new Error(
170
177
  `Cannot use index "${indexName}" for table "${tableName}" because ` +
171
178
  `it is not declared in the schema.`,
172
179
  );
173
180
  }
174
- fields = index.fields.concat(["_creationTime", "_id"]);
181
+ fields = declared.fields.concat(["_creationTime", "_id"]);
175
182
  }
176
183
  fieldPathsToSortBy = fields;
177
184
  validateIndexRangeExpression(source, fields);
@@ -216,6 +223,7 @@ const resolveSource = (
216
223
  order,
217
224
  interval: {
218
225
  kind: "interval",
226
+ index,
219
227
  table,
220
228
  fields: fieldPathsToSortBy.includes("_id")
221
229
  ? fieldPathsToSortBy
@@ -241,6 +249,7 @@ export const stream = (
241
249
  let returned = 0;
242
250
  for (const doc of sortedDocs) {
243
251
  if (limit !== null && returned >= limit) return;
252
+ view.examine(interval.table, doc);
244
253
  examined(ReadSet.keyOf(interval.fields, doc as StoredDocument));
245
254
  if (filterFn(doc)) {
246
255
  returned++;
@@ -352,6 +361,7 @@ export const paginate = (
352
361
  reachedEnd = false;
353
362
  examined(endCursorKeys);
354
363
  if (cmp === 0) {
364
+ view.examine(interval.table, doc);
355
365
  rowsRead += 1;
356
366
  bytesRead += getDocumentSize(doc);
357
367
  readDocCursors.push(encodeCursor(doc, cursorFields));
@@ -361,6 +371,7 @@ export const paginate = (
361
371
  break;
362
372
  }
363
373
  }
374
+ view.examine(interval.table, doc);
364
375
  rowsRead += 1;
365
376
  bytesRead += getDocumentSize(doc);
366
377
  readDocCursors.push(encodeCursor(doc, cursorFields));
@@ -0,0 +1,520 @@
1
+ // The limits, counters and messages are convex-backend's, read at the commit
2
+ // the fork's local backend runs (`precompiled-2026-10-28-cccbade`). Paths below
3
+ // are in that repository.
4
+
5
+ import type { TransactionMetrics } from "convex/server";
6
+ import { compareValues, getConvexSize, type Value } from "convex/values";
7
+ import * as Schema from "effect/Schema";
8
+ import type { SerializedRangeExpression } from "./KernelValues.js";
9
+ import { evaluateQueryValue } from "./KernelValues.js";
10
+
11
+ /**
12
+ * The ceilings one function execution runs under, the binary's
13
+ * `TransactionLimits` (`crates/database/src/execution_size.rs:62-95`).
14
+ */
15
+ export type Limits = { readonly [K in keyof TransactionMetrics]: number };
16
+
17
+ /**
18
+ * The binary's default ceilings (`crates/common/src/knobs.rs`).
19
+ */
20
+ export const defaults: Limits = {
21
+ // `TRANSACTION_MAX_READ_SIZE_BYTES`, knobs.rs:645-647.
22
+ bytesRead: 1 << 24,
23
+ // `TRANSACTION_MAX_READ_SIZE_ROWS`, knobs.rs:641-642.
24
+ documentsRead: 32000,
25
+ // `TRANSACTION_MAX_READ_SET_INTERVALS`, knobs.rs:657-658.
26
+ databaseQueries: 4096,
27
+ // `TRANSACTION_MAX_NUM_USER_WRITES`, knobs.rs:406-407.
28
+ documentsWritten: 16000,
29
+ // `TRANSACTION_MAX_USER_WRITE_SIZE_BYTES`, knobs.rs:410-412.
30
+ bytesWritten: 1 << 24,
31
+ // `TRANSACTION_MAX_NUM_SCHEDULED`, knobs.rs:514-515.
32
+ functionsScheduled: 1000,
33
+ // `TRANSACTION_MAX_SCHEDULED_TOTAL_ARGUMENT_SIZE_BYTES`, knobs.rs:530-536.
34
+ scheduledFunctionArgsBytes: 1 << 24,
35
+ // `TRANSACTION_MAX_NUM_FILES_WRITTEN`, knobs.rs:539-540.
36
+ filesWritten: 10,
37
+ // `TRANSACTION_MAX_FILE_WRITE_SIZE_BYTES`, knobs.rs:544-546.
38
+ fileWriteBytes: 1 << 24,
39
+ // `TRANSACTION_MAX_NUM_FILES_READ`, knobs.rs:549-550.
40
+ filesRead: 10,
41
+ // `TRANSACTION_MAX_FILE_READ_SIZE_BYTES`, knobs.rs:554-556.
42
+ fileReadBytes: 1 << 24,
43
+ };
44
+
45
+ /**
46
+ * What a nested call may use on top of what its transaction has used, as
47
+ * `ctx.runQuery` and `ctx.runMutation` take it in `transactionLimits`.
48
+ */
49
+ export type Budget = Partial<Limits>;
50
+
51
+ /**
52
+ * A nested call's ceilings: each one what the transaction has used plus the
53
+ * call's budget, never above the caller's (`TransactionLimits::from_budget`,
54
+ * execution_size.rs:97-160). An unset field leaves the caller's ceiling.
55
+ */
56
+ export const fromBudget = (
57
+ budget: Budget,
58
+ used: Limits,
59
+ ceiling: Limits,
60
+ ): Limits =>
61
+ Object.fromEntries(
62
+ (Object.keys(ceiling) as Array<keyof Limits>).map((metric) => [
63
+ metric,
64
+ Math.min(
65
+ used[metric] + (budget[metric] ?? ceiling[metric]),
66
+ ceiling[metric],
67
+ ),
68
+ ]),
69
+ ) as unknown as Limits;
70
+
71
+ const overLimitHelp =
72
+ "Consider using smaller limits in your queries, paginating your queries, or using indexed queries with a selective index range expressions.";
73
+
74
+ /**
75
+ * A function execution crossed one of its ceilings. `code` is the binary's
76
+ * short message and `message` its text, which the function receives as a
77
+ * catchable error.
78
+ */
79
+ export class TransactionLimitExceeded extends Schema.TaggedError<TransactionLimitExceeded>()(
80
+ "TransactionLimitExceeded",
81
+ {
82
+ code: Schema.Literals([
83
+ "TooManyReads",
84
+ "TooManyDocumentsRead",
85
+ "TooManyBytesRead",
86
+ "TooManyWrites",
87
+ "TooManyBytesWritten",
88
+ "TooManyFunctionsScheduled",
89
+ "ScheduledFunctionsArgumentsTooLarge",
90
+ ]),
91
+ limit: Schema.Int,
92
+ message: Schema.String,
93
+ },
94
+ ) {}
95
+
96
+ const byteUnits: ReadonlyArray<readonly [number, string]> = [
97
+ [2 ** 30, "GiB"],
98
+ [1_000_000_000, "GB"],
99
+ [2 ** 20, "MiB"],
100
+ [1_000_000, "MB"],
101
+ [2 ** 10, "KiB"],
102
+ [1_000, "KB"],
103
+ ];
104
+
105
+ /**
106
+ * `common::fmt::format_bytes` (`crates/common/src/fmt.rs:29-47`).
107
+ */
108
+ export const formatBytes = (n: number): string => {
109
+ if (n === 0) return "0 bytes";
110
+ for (const [unit, name] of byteUnits) {
111
+ if (n < unit) continue;
112
+ if (n % unit === 0) return `${n / unit} ${name}`;
113
+ if ((n * 10) % unit === 0) {
114
+ return `${Math.floor(n / unit)}.${Math.floor((n * 10) / unit) % 10} ${name}`;
115
+ }
116
+ }
117
+ return `${n} bytes`;
118
+ };
119
+
120
+ /**
121
+ * A position in an index's key space: just before, or just after, every key
122
+ * that starts with `key`.
123
+ */
124
+ interface Bound {
125
+ readonly key: ReadonlyArray<Value | undefined>;
126
+ readonly side: -1 | 1;
127
+ }
128
+
129
+ /**
130
+ * The keys from `start` up to `end`.
131
+ */
132
+ export interface KeyInterval {
133
+ readonly start: Bound;
134
+ readonly end: Bound;
135
+ }
136
+
137
+ const compareBounds = (a: Bound, b: Bound): number => {
138
+ const shared = Math.min(a.key.length, b.key.length);
139
+ for (let i = 0; i < shared; i++) {
140
+ const cmp = compareValues(a.key[i] as Value, b.key[i] as Value);
141
+ if (cmp !== 0) return cmp;
142
+ }
143
+ if (a.key.length === b.key.length) return a.side - b.side;
144
+ return a.key.length < b.key.length ? a.side : -b.side;
145
+ };
146
+
147
+ const maxBound = (a: Bound, b: Bound) => (compareBounds(a, b) >= 0 ? a : b);
148
+ const minBound = (a: Bound, b: Bound) => (compareBounds(a, b) <= 0 ? a : b);
149
+
150
+ /**
151
+ * The one key `key`, as a read or write of a document by ID records it.
152
+ */
153
+ export const point = (key: ReadonlyArray<Value | undefined>): KeyInterval => ({
154
+ start: { key, side: -1 },
155
+ end: { key, side: 1 },
156
+ });
157
+
158
+ /**
159
+ * Every key of an index.
160
+ */
161
+ export const everything: KeyInterval = point([]);
162
+
163
+ /**
164
+ * The part of an index range a query has examined: from where it starts, past
165
+ * the cursor `after`, through `through` in its order. `null` when it has
166
+ * examined nothing.
167
+ */
168
+ export const examined = (
169
+ range: ReadonlyArray<SerializedRangeExpression>,
170
+ order: "asc" | "desc",
171
+ after: ReadonlyArray<Value | undefined> | null,
172
+ through: ReadonlyArray<Value | undefined> | "end",
173
+ ): KeyInterval | null => {
174
+ const equal: Array<Value | undefined> = [];
175
+ let start: Bound | undefined;
176
+ let end: Bound | undefined;
177
+ for (const expression of range) {
178
+ const value = evaluateQueryValue(expression.value);
179
+ switch (expression.type) {
180
+ case "Eq":
181
+ equal.push(value);
182
+ break;
183
+ case "Gt":
184
+ case "Gte":
185
+ start = {
186
+ key: [...equal, value],
187
+ side: expression.type === "Gt" ? 1 : -1,
188
+ };
189
+ break;
190
+ case "Lt":
191
+ case "Lte":
192
+ end = {
193
+ key: [...equal, value],
194
+ side: expression.type === "Lt" ? -1 : 1,
195
+ };
196
+ break;
197
+ }
198
+ }
199
+ start ??= { key: equal, side: -1 };
200
+ end ??= { key: equal, side: 1 };
201
+ if (after !== null) {
202
+ if (order === "asc") start = maxBound(start, { key: after, side: 1 });
203
+ else end = minBound(end, { key: after, side: -1 });
204
+ }
205
+ if (through !== "end") {
206
+ if (order === "asc") end = minBound(end, { key: through, side: 1 });
207
+ else start = maxBound(start, { key: through, side: -1 });
208
+ }
209
+ return compareBounds(start, end) < 0 ? { start, end } : null;
210
+ };
211
+
212
+ /**
213
+ * The intervals read from one index, overlapping and adjacent ones merged
214
+ * (`IntervalSet::add`, `crates/common/src/interval/interval_set.rs:150-230`).
215
+ * They stay sorted and disjoint, so an add touches only its neighbours.
216
+ */
217
+ class IntervalSet {
218
+ private readonly intervals: Array<KeyInterval> = [];
219
+
220
+ get size(): number {
221
+ return this.intervals.length;
222
+ }
223
+
224
+ add(interval: KeyInterval) {
225
+ const intervals = this.intervals;
226
+ // The first interval that ends at or after the new one's start.
227
+ let low = 0;
228
+ let high = intervals.length;
229
+ while (low < high) {
230
+ const middle = (low + high) >> 1;
231
+ if (compareBounds(intervals[middle]!.end, interval.start) < 0) {
232
+ low = middle + 1;
233
+ } else {
234
+ high = middle;
235
+ }
236
+ }
237
+ let last = low;
238
+ let merged = interval;
239
+ while (
240
+ last < intervals.length &&
241
+ compareBounds(intervals[last]!.start, interval.end) <= 0
242
+ ) {
243
+ const other = intervals[last]!;
244
+ merged = {
245
+ start: minBound(merged.start, other.start),
246
+ end: maxBound(merged.end, other.end),
247
+ };
248
+ last++;
249
+ }
250
+ intervals.splice(low, last - low, merged);
251
+ }
252
+ }
253
+
254
+ /**
255
+ * The counters every frame of one attempt shares: reads and schedules stay when
256
+ * a nested mutation fails, as the binary keeps them outside its subtransaction
257
+ * token (`crates/database/src/transaction.rs:418-454`).
258
+ */
259
+ class Shared {
260
+ readonly indexes = new Map<string, IntervalSet>();
261
+ intervals = 0;
262
+ documentsRead = 0;
263
+ bytesRead = 0;
264
+ functionsScheduled = 0;
265
+ scheduledFunctionArgsBytes = 0;
266
+ // File storage counters stay at zero until the kernel stores files.
267
+ readonly filesRead = 0;
268
+ readonly fileReadBytes = 0;
269
+
270
+ /**
271
+ * A stale-snapshot query's counters: the scheduling and file-read counters
272
+ * carried over, reads and intervals from zero (`clone_for_snapshot_query`,
273
+ * `crates/database/src/transaction.rs:1460-1485`).
274
+ */
275
+ forStaleSnapshot(): Shared {
276
+ const copy = new Shared();
277
+ copy.functionsScheduled = this.functionsScheduled;
278
+ copy.scheduledFunctionArgsBytes = this.scheduledFunctionArgsBytes;
279
+ return copy;
280
+ }
281
+ }
282
+
283
+ /**
284
+ * `a - b`, or 0 when `b` is larger: Rust's `usize::saturating_sub`.
285
+ */
286
+ const saturatingSub = (a: number, b: number) => (a > b ? a - b : 0);
287
+
288
+ /**
289
+ * One frame's share of an attempt's accounting. The outermost frame belongs to
290
+ * the top-level function and each nested mutation gets a child. A child's
291
+ * writes count toward its caller's once it merges and are forgotten when it
292
+ * fails, as the binary's nested write set rolls its sizes back
293
+ * (`crates/database/src/writes.rs:96-136`).
294
+ */
295
+ export class Meter {
296
+ private documentsWritten = 0;
297
+ private bytesWritten = 0;
298
+ // File storage counters stay at zero until the kernel stores files.
299
+ private readonly filesWritten = 0;
300
+ private readonly fileWriteBytes = 0;
301
+
302
+ constructor(
303
+ private readonly shared: Shared = new Shared(),
304
+ private readonly parent?: Meter,
305
+ ) {}
306
+
307
+ child(): Meter {
308
+ return new Meter(this.shared, this);
309
+ }
310
+
311
+ /**
312
+ * The meter of a query run on a stale snapshot. The binary runs it on a clone
313
+ * of the transaction that keeps the scheduling counters and starts reads and
314
+ * writes from zero, then drops the clone, so nothing it counts reaches this
315
+ * meter (`clone_for_snapshot_query`,
316
+ * `crates/database/src/transaction.rs:1460-1485`; `run_udf`,
317
+ * `crates/isolate/src/environment/udf/async_syscall.rs:695-776`).
318
+ */
319
+ forStaleSnapshot(): Meter {
320
+ return new Meter(this.shared.forStaleSnapshot());
321
+ }
322
+
323
+ /**
324
+ * The child's writes become its caller's.
325
+ */
326
+ merge() {
327
+ if (this.parent === undefined) {
328
+ throw new Error("The outermost meter of an attempt has no caller.");
329
+ }
330
+ this.parent.documentsWritten += this.documentsWritten;
331
+ this.parent.bytesWritten += this.bytesWritten;
332
+ }
333
+
334
+ private chain(): Array<Meter> {
335
+ return this.parent === undefined ? [this] : [...this.parent.chain(), this];
336
+ }
337
+
338
+ /**
339
+ * How much of each limit the execution has used.
340
+ */
341
+ used(): Limits {
342
+ const frames = this.chain();
343
+ const sum = (of: (meter: Meter) => number) =>
344
+ frames.reduce((total, meter) => total + of(meter), 0);
345
+ const shared = this.shared;
346
+ return {
347
+ bytesRead: shared.bytesRead,
348
+ documentsRead: shared.documentsRead,
349
+ databaseQueries: shared.intervals,
350
+ documentsWritten: sum((meter) => meter.documentsWritten),
351
+ bytesWritten: sum((meter) => meter.bytesWritten),
352
+ functionsScheduled: shared.functionsScheduled,
353
+ scheduledFunctionArgsBytes: shared.scheduledFunctionArgsBytes,
354
+ filesWritten: sum((meter) => meter.filesWritten),
355
+ fileWriteBytes: sum((meter) => meter.fileWriteBytes),
356
+ filesRead: shared.filesRead,
357
+ fileReadBytes: shared.fileReadBytes,
358
+ };
359
+ }
360
+
361
+ /**
362
+ * What `getTransactionMetrics` answers (`tx_metrics`,
363
+ * `crates/isolate/src/environment/udf/async_syscall.rs:947-993`).
364
+ */
365
+ metrics(limits: Limits): TransactionMetrics {
366
+ const used = this.used();
367
+ return Object.fromEntries(
368
+ (Object.keys(limits) as Array<keyof Limits>).map((metric) => [
369
+ metric,
370
+ { used: used[metric], remaining: limits[metric] - used[metric] },
371
+ ]),
372
+ ) as unknown as TransactionMetrics;
373
+ }
374
+
375
+ private set(index: string): IntervalSet {
376
+ const set = this.shared.indexes.get(index) ?? new IntervalSet();
377
+ this.shared.indexes.set(index, set);
378
+ return set;
379
+ }
380
+
381
+ /**
382
+ * A read the function made of `interval` in `index`, counted as the binary
383
+ * counts it: the total drops by the index's interval count before the read,
384
+ * saturating at zero, and rises by its count after
385
+ * (`record_indexed_directly`, `crates/database/src/reads.rs:447-480`). The
386
+ * index's count includes the intervals of IDs this execution inserted, which
387
+ * joined it without counting (`deriveInterval`), so a read that covers them
388
+ * can move the total by more than the read itself. The binary starts the
389
+ * total at the system-table intervals it reads to run the function, which the
390
+ * kernel does not simulate. While neither total saturates they differ by
391
+ * exactly that share. A read that saturates only the kernel's lower total
392
+ * narrows the gap, and it never widens or turns negative (see the
393
+ * `SystemTableReadIntervals` boundary).
394
+ */
395
+ readInterval(
396
+ index: string,
397
+ interval: KeyInterval,
398
+ limits: Limits,
399
+ ): TransactionLimitExceeded | undefined {
400
+ const set = this.set(index);
401
+ const before = set.size;
402
+ set.add(interval);
403
+ const shared = this.shared;
404
+ shared.intervals = saturatingSub(shared.intervals, before) + set.size;
405
+ // Both terms are counts, so the total never goes below zero.
406
+ if (!Number.isSafeInteger(shared.intervals) || shared.intervals < 0) {
407
+ throw new Error(
408
+ `The kernel counted ${shared.intervals} intervals read, which is no count.`,
409
+ );
410
+ }
411
+ return shared.intervals > limits.databaseQueries
412
+ ? new TransactionLimitExceeded({
413
+ code: "TooManyReads",
414
+ limit: limits.databaseQueries,
415
+ message: `Too many reads in a single function execution (limit: ${limits.databaseQueries}). ${overLimitHelp}`,
416
+ })
417
+ : undefined;
418
+ }
419
+
420
+ /**
421
+ * A read the binary takes for a write rather than for the function: a new
422
+ * document's ID (`register_new_id`, writes.rs:426-441). It joins the index's
423
+ * intervals and leaves the total as it was (`record_indexed_derived` calls
424
+ * `_record_indexed` and ignores its counts, reads.rs:333-344), so a later
425
+ * read of that index starts from a count that includes it.
426
+ */
427
+ deriveInterval(index: string, interval: KeyInterval) {
428
+ this.set(index).add(interval);
429
+ }
430
+
431
+ /**
432
+ * A document the function read. A system table's document counts toward no
433
+ * limit (`record_read_document`, reads.rs:365-445).
434
+ */
435
+ readDocument(
436
+ document: Value,
437
+ system: boolean,
438
+ limits: Limits,
439
+ ): TransactionLimitExceeded | undefined {
440
+ if (system) return undefined;
441
+ const shared = this.shared;
442
+ shared.documentsRead += 1;
443
+ shared.bytesRead += getConvexSize(document);
444
+ if (shared.documentsRead > limits.documentsRead) {
445
+ return new TransactionLimitExceeded({
446
+ code: "TooManyDocumentsRead",
447
+ limit: limits.documentsRead,
448
+ message: `Too many documents read in a single function execution (limit: ${limits.documentsRead}). ${overLimitHelp}`,
449
+ });
450
+ }
451
+ if (shared.bytesRead > limits.bytesRead) {
452
+ return new TransactionLimitExceeded({
453
+ code: "TooManyBytesRead",
454
+ limit: limits.bytesRead,
455
+ message: `Too many bytes read in a single function execution (limit: ${limits.bytesRead} bytes). ${overLimitHelp}`,
456
+ });
457
+ }
458
+ return undefined;
459
+ }
460
+
461
+ /**
462
+ * A write to a user table: the new document, or `null` for a delete. Each
463
+ * write counts, even to a document this execution already wrote
464
+ * (`Writes::update`, writes.rs:241-310).
465
+ */
466
+ write(
467
+ document: Value | null,
468
+ limits: Limits,
469
+ ): TransactionLimitExceeded | undefined {
470
+ this.documentsWritten += 1;
471
+ this.bytesWritten += document === null ? 0 : getConvexSize(document);
472
+ const used = this.used();
473
+ if (used.documentsWritten > limits.documentsWritten) {
474
+ return new TransactionLimitExceeded({
475
+ code: "TooManyWrites",
476
+ limit: limits.documentsWritten,
477
+ message: `Too many writes in a single function execution (limit: ${limits.documentsWritten})`,
478
+ });
479
+ }
480
+ if (used.bytesWritten > limits.bytesWritten) {
481
+ return new TransactionLimitExceeded({
482
+ code: "TooManyBytesWritten",
483
+ limit: limits.bytesWritten,
484
+ message: `Too many bytes written in a single function execution (limit: ${formatBytes(limits.bytesWritten)})`,
485
+ });
486
+ }
487
+ return undefined;
488
+ }
489
+
490
+ /**
491
+ * A function scheduled with `args`. The binary checks before it counts, so a
492
+ * refused schedule counts nothing (`check_scheduling_limits`,
493
+ * `crates/model/src/scheduled_jobs/mod.rs:162-196`). Its arguments are sized
494
+ * as the one-element array the scheduler stores.
495
+ */
496
+ schedule(args: Value, limits: Limits): TransactionLimitExceeded | undefined {
497
+ const shared = this.shared;
498
+ const size = getConvexSize([args]);
499
+ if (shared.functionsScheduled >= limits.functionsScheduled) {
500
+ return new TransactionLimitExceeded({
501
+ code: "TooManyFunctionsScheduled",
502
+ limit: limits.functionsScheduled,
503
+ message: `Too many functions scheduled by this mutation (limit: ${limits.functionsScheduled})`,
504
+ });
505
+ }
506
+ if (
507
+ shared.scheduledFunctionArgsBytes + size >
508
+ limits.scheduledFunctionArgsBytes
509
+ ) {
510
+ return new TransactionLimitExceeded({
511
+ code: "ScheduledFunctionsArgumentsTooLarge",
512
+ limit: limits.scheduledFunctionArgsBytes,
513
+ message: `Too large total size of the arguments of scheduled functions from this mutation (limit: ${limits.scheduledFunctionArgsBytes} bytes)`,
514
+ });
515
+ }
516
+ shared.functionsScheduled += 1;
517
+ shared.scheduledFunctionArgsBytes += size;
518
+ return undefined;
519
+ }
520
+ }