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