@gaganref/confect-test 10.3.1-fork.9 → 10.3.1-fork.90
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/TestConfect.d.ts +37 -11
- package/dist/TestConfect.d.ts.map +1 -1
- package/dist/TestConfect.js +22 -7
- package/dist/TestConfect.js.map +1 -1
- package/dist/internal/Attempt.d.ts +77 -0
- package/dist/internal/Attempt.d.ts.map +1 -0
- package/dist/internal/Attempt.js +154 -0
- package/dist/internal/Attempt.js.map +1 -0
- package/dist/internal/Clock.d.ts +29 -0
- package/dist/internal/Clock.d.ts.map +1 -0
- package/dist/internal/Clock.js +66 -0
- package/dist/internal/Clock.js.map +1 -0
- package/dist/internal/ConvexTest.d.ts +66 -0
- package/dist/internal/ConvexTest.d.ts.map +1 -0
- package/dist/internal/ConvexTest.js +62 -0
- package/dist/internal/ConvexTest.js.map +1 -0
- package/dist/internal/Execution.d.ts +247 -0
- package/dist/internal/Execution.d.ts.map +1 -0
- package/dist/internal/Execution.js +1024 -0
- package/dist/internal/Execution.js.map +1 -0
- package/dist/internal/Globals.d.ts +11 -0
- package/dist/internal/Globals.d.ts.map +1 -0
- package/dist/internal/Globals.js +88 -0
- package/dist/internal/Globals.js.map +1 -0
- package/dist/internal/Invocation.d.ts +68 -0
- package/dist/internal/Invocation.d.ts.map +1 -0
- package/dist/internal/Invocation.js +158 -0
- package/dist/internal/Invocation.js.map +1 -0
- package/dist/internal/Kernel.d.ts +95 -0
- package/dist/internal/Kernel.d.ts.map +1 -0
- package/dist/internal/Kernel.js +91 -0
- package/dist/internal/Kernel.js.map +1 -0
- package/dist/internal/KernelQuery.d.ts +55 -0
- package/dist/internal/KernelQuery.d.ts.map +1 -0
- package/dist/internal/KernelQuery.js +205 -0
- package/dist/internal/KernelQuery.js.map +1 -0
- package/dist/internal/KernelRefusal.d.ts +102 -0
- package/dist/internal/KernelRefusal.d.ts.map +1 -0
- package/dist/internal/KernelRefusal.js +94 -0
- package/dist/internal/KernelRefusal.js.map +1 -0
- package/dist/internal/KernelValues.d.ts +138 -0
- package/dist/internal/KernelValues.d.ts.map +1 -0
- package/dist/internal/KernelValues.js +281 -0
- package/dist/internal/KernelValues.js.map +1 -0
- package/dist/internal/MountedHttp.d.ts +1 -1
- package/dist/internal/MountedHttp.d.ts.map +1 -1
- package/dist/internal/MountedHttp.js +3 -3
- package/dist/internal/MountedHttp.js.map +1 -1
- package/dist/internal/ReadSet.d.ts +55 -0
- package/dist/internal/ReadSet.d.ts.map +1 -0
- package/dist/internal/ReadSet.js +29 -0
- package/dist/internal/ReadSet.js.map +1 -0
- package/dist/internal/Scheduling.d.ts +71 -0
- package/dist/internal/Scheduling.d.ts.map +1 -0
- package/dist/internal/Scheduling.js +60 -0
- package/dist/internal/Scheduling.js.map +1 -0
- package/package.json +5 -5
- package/src/TestConfect.ts +84 -19
- package/src/internal/Attempt.ts +185 -0
- package/src/internal/Clock.ts +95 -0
- package/src/internal/ConvexTest.ts +163 -0
- package/src/internal/Execution.ts +1795 -0
- package/src/internal/Globals.ts +113 -0
- package/src/internal/Invocation.ts +316 -0
- package/src/internal/Kernel.ts +190 -0
- package/src/internal/KernelQuery.ts +405 -0
- package/src/internal/KernelRefusal.ts +112 -0
- package/src/internal/KernelValues.ts +765 -0
- package/src/internal/MountedHttp.ts +13 -6
- package/src/internal/ReadSet.ts +108 -0
- package/src/internal/Scheduling.ts +126 -0
|
@@ -0,0 +1,1024 @@
|
|
|
1
|
+
import { own } from "./Globals.js";
|
|
2
|
+
import { install } from "./Clock.js";
|
|
3
|
+
import { createFunctionHandle, createTestJwt, deserializeConvexErrorData, evaluateValue, hasPendingValues, parseArgs, parseFunctionHandle, resolveCommittedValues, simpleHash, tableNameFromId, validateFunctionArgs, validateReturnValue } from "./KernelValues.js";
|
|
4
|
+
import { apply, conflict, empty, wallClock } from "./Kernel.js";
|
|
5
|
+
import { KernelAttempt } from "./Attempt.js";
|
|
6
|
+
import { bind, current as current$1, fresh, unbound } from "./Invocation.js";
|
|
7
|
+
import { ClosedAttempt, CommitTsNotIncreasing, DrainBudgetExceeded, OptimisticConcurrencyControlFailure, RegisteredAfterOpen, StaleAttempt, UnknownStandingSource, requiresBinary, rowPending } from "./KernelRefusal.js";
|
|
8
|
+
import { exportSchema, paginate, stream, validateDocument } from "./KernelQuery.js";
|
|
9
|
+
import { functions, pending, row, stateOf, table } from "./Scheduling.js";
|
|
10
|
+
import { actionGeneric, getFunctionAddress, mutationGeneric, queryGeneric } from "convex/server";
|
|
11
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
12
|
+
import { convexToJson, jsonToConvex } from "convex/values";
|
|
13
|
+
//#region src/internal/Execution.ts
|
|
14
|
+
const ROOT = "";
|
|
15
|
+
const TYPICAL_CONVEX_ID_LENGTH = 32;
|
|
16
|
+
const anonymous = {
|
|
17
|
+
identity: null,
|
|
18
|
+
token: null,
|
|
19
|
+
ip: null,
|
|
20
|
+
userAgent: null
|
|
21
|
+
};
|
|
22
|
+
const withIdentity = (caller, identity) => {
|
|
23
|
+
const subject = identity.subject ?? "" + simpleHash(JSON.stringify(identity));
|
|
24
|
+
const issuer = identity.issuer ?? "https://convex.test";
|
|
25
|
+
const tokenIdentifier = identity.tokenIdentifier ?? `${issuer}|${subject}`;
|
|
26
|
+
const full = {
|
|
27
|
+
...identity,
|
|
28
|
+
subject,
|
|
29
|
+
issuer,
|
|
30
|
+
tokenIdentifier
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
...caller,
|
|
34
|
+
identity: full,
|
|
35
|
+
token: createTestJwt(full)
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
const lastAttempt = 5;
|
|
39
|
+
const tagOf = (error) => {
|
|
40
|
+
const tagged = (value) => typeof value === "object" && value !== null && typeof value._tag === "string";
|
|
41
|
+
const data = error?.data;
|
|
42
|
+
if (tagged(data)) return data._tag;
|
|
43
|
+
if (tagged(error)) return error._tag;
|
|
44
|
+
return error instanceof Error ? error.name : "Error";
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* A queue that runs one task at a time, in the order they were taken.
|
|
48
|
+
*/
|
|
49
|
+
var Turn = class {
|
|
50
|
+
tail = Promise.resolve();
|
|
51
|
+
take(task) {
|
|
52
|
+
const result = this.tail.then(task);
|
|
53
|
+
this.tail = result.then(() => void 0, () => void 0);
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolves once every task taken so far has settled, including the tasks
|
|
58
|
+
* taken while waiting.
|
|
59
|
+
*/
|
|
60
|
+
idle() {
|
|
61
|
+
const tail = this.tail;
|
|
62
|
+
return tail.then(() => this.tail === tail ? void 0 : this.idle());
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* A function's invocation finishes once its handler returned and every syscall
|
|
67
|
+
* it started has run. On the binary a query or mutation keeps running the
|
|
68
|
+
* syscalls in its queue after its promise resolves, then returns (`run_inner`
|
|
69
|
+
* in `crates/isolate/src/environment/udf/mod.rs`, 1.49), so an unawaited write
|
|
70
|
+
* still lands in its own transaction.
|
|
71
|
+
*/
|
|
72
|
+
const settled = (execution, result) => execution.turn.idle().then(() => result);
|
|
73
|
+
const executions = new AsyncLocalStorage();
|
|
74
|
+
/**
|
|
75
|
+
* Run a function's code as `execution`, with its own copy of the globals a test
|
|
76
|
+
* invocation pins.
|
|
77
|
+
*/
|
|
78
|
+
const enter = (execution, run) => {
|
|
79
|
+
installConvexGlobal();
|
|
80
|
+
return own(() => executions.run(execution, run));
|
|
81
|
+
};
|
|
82
|
+
const kernelBrand = Symbol.for("@confect/test/Kernel");
|
|
83
|
+
/**
|
|
84
|
+
* The time `Date` answers inside a kernel invocation: a query's or mutation's
|
|
85
|
+
* start, or the clock as it runs in an action. Outside one, none.
|
|
86
|
+
*/
|
|
87
|
+
const pinnedTime = () => {
|
|
88
|
+
const execution = executions.getStore();
|
|
89
|
+
if (execution === void 0) return void 0;
|
|
90
|
+
return execution.udfType === "action" ? execution.world.now() : execution.startTime;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* How many jobs one drain or advance runs before it gives up, as convex-test's
|
|
94
|
+
* `finishAllScheduledFunctions` defaults to.
|
|
95
|
+
*/
|
|
96
|
+
const defaultBudget = 100;
|
|
97
|
+
const current = () => {
|
|
98
|
+
const execution = executions.getStore();
|
|
99
|
+
if (execution === void 0) throw new Error("A Convex syscall ran outside any function the test kernel started.");
|
|
100
|
+
return execution;
|
|
101
|
+
};
|
|
102
|
+
let nextRequestId = 0;
|
|
103
|
+
const newRequest = (caller) => ({
|
|
104
|
+
ip: caller.ip,
|
|
105
|
+
userAgent: caller.userAgent,
|
|
106
|
+
requestId: `kernel-request-${nextRequestId++}`,
|
|
107
|
+
scheduledFunctionId: null,
|
|
108
|
+
authToken: caller.token
|
|
109
|
+
});
|
|
110
|
+
/**
|
|
111
|
+
* A scheduled call is a request of its own: no caller's attributes, and its
|
|
112
|
+
* job's ID.
|
|
113
|
+
*/
|
|
114
|
+
const jobRequest = (jobId) => ({
|
|
115
|
+
...newRequest(anonymous),
|
|
116
|
+
scheduledFunctionId: jobId
|
|
117
|
+
});
|
|
118
|
+
const findModulesRoot = (paths) => {
|
|
119
|
+
const generated = paths.find((path) => path.includes("_generated"));
|
|
120
|
+
if (generated === void 0) throw new Error("Could not find the \"_generated\" directory, make sure to run `npx convex dev` or `npx convex codegen`. Make sure your `import.meta.glob` includes the files in the \"_generated\" directory");
|
|
121
|
+
return generated.split("_generated", 2)[0];
|
|
122
|
+
};
|
|
123
|
+
const makeComponent = (path, schema, modules, standing = []) => {
|
|
124
|
+
const root = findModulesRoot(Object.keys(modules));
|
|
125
|
+
const relative = Object.entries(modules).filter(([key]) => key.startsWith(root)).map(([key, load]) => [key.slice(root.length), load]);
|
|
126
|
+
const nested = relative.map(([file]) => file).filter((file) => file.endsWith("/convex.config.ts")).map((file) => file.slice(0, -16));
|
|
127
|
+
return {
|
|
128
|
+
path,
|
|
129
|
+
schema: exportSchema(schema),
|
|
130
|
+
loaders: new Map(relative.filter(([file]) => isFunctionModule(file) && !nested.some((dir) => file.startsWith(dir))).map(([file, load]) => [file.replace(/\.[^.]+$/, ""), load])),
|
|
131
|
+
loaded: /* @__PURE__ */ new Map(),
|
|
132
|
+
standing: new Set(standing)
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
const entryExtensions = [
|
|
136
|
+
".js",
|
|
137
|
+
".mjs",
|
|
138
|
+
".cjs",
|
|
139
|
+
".ts",
|
|
140
|
+
".tsx",
|
|
141
|
+
".mts",
|
|
142
|
+
".cts",
|
|
143
|
+
".jsx"
|
|
144
|
+
];
|
|
145
|
+
/**
|
|
146
|
+
* Whether Convex's bundler deploys this file as a function module, by the rule
|
|
147
|
+
* `entryPoints` applies in `convex/src/bundler/index.ts` (1.49). A test file, a
|
|
148
|
+
* schema, a config and `_generated` never reach the binary, so the kernel
|
|
149
|
+
* neither loads them nor resolves a function in them.
|
|
150
|
+
*/
|
|
151
|
+
const isFunctionModule = (file) => {
|
|
152
|
+
const base = file.split("/").pop();
|
|
153
|
+
return entryExtensions.some((extension) => file.endsWith(extension)) && !file.startsWith("_generated/") && !base.startsWith(".") && !base.startsWith("#") && base !== "schema.ts" && base !== "schema.js" && (base.match(/\./g) ?? []).length <= 1 && !file.includes(" ");
|
|
154
|
+
};
|
|
155
|
+
const unregistered = (world, componentPath) => {
|
|
156
|
+
const registered = [...world.components.keys()].filter((p) => p !== ROOT);
|
|
157
|
+
return /* @__PURE__ */ new Error(`Component "${componentPath}" is not registered on this test backend. ` + (registered.length === 0 ? "No components have been registered." : `Registered components: ${registered.map((p) => `"${p}"`).join(", ")}.`));
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* Where an address points, read from the calling component.
|
|
161
|
+
*/
|
|
162
|
+
const functionPathOf = (address, from) => {
|
|
163
|
+
if (address.functionHandle !== void 0) return parseFunctionHandle(address.functionHandle);
|
|
164
|
+
if (address.name !== void 0) return {
|
|
165
|
+
udfPath: address.name,
|
|
166
|
+
componentPath: from
|
|
167
|
+
};
|
|
168
|
+
if (address.reference !== void 0) {
|
|
169
|
+
const parts = address.reference.split("/");
|
|
170
|
+
const child = parts[2];
|
|
171
|
+
const rest = parts.slice(3);
|
|
172
|
+
return {
|
|
173
|
+
componentPath: from.length > 0 ? `${from}/${child}` : child,
|
|
174
|
+
udfPath: `${rest.slice(0, -1).join("/")}:${rest[rest.length - 1]}`
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
throw new Error("Function address not supported");
|
|
178
|
+
};
|
|
179
|
+
const pathOfReference = (reference, from) => functionPathOf(getFunctionAddress(reference), from);
|
|
180
|
+
const kindCheck = {
|
|
181
|
+
query: "isQuery",
|
|
182
|
+
mutation: "isMutation",
|
|
183
|
+
action: "isAction"
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* A test backend: its registered components and the kernel state they share.
|
|
187
|
+
*/
|
|
188
|
+
var World = class {
|
|
189
|
+
runtime;
|
|
190
|
+
components = /* @__PURE__ */ new Map();
|
|
191
|
+
state = empty;
|
|
192
|
+
opened;
|
|
193
|
+
nextDocId = 1e4;
|
|
194
|
+
lastCreationTime = 0;
|
|
195
|
+
nextQueryId = 1;
|
|
196
|
+
queries = /* @__PURE__ */ new Map();
|
|
197
|
+
holding = /* @__PURE__ */ new Map();
|
|
198
|
+
nextAttemptId = 1;
|
|
199
|
+
/**
|
|
200
|
+
* The latest deadline time has been advanced to. The clock never reads
|
|
201
|
+
* earlier.
|
|
202
|
+
*/
|
|
203
|
+
floor = Number.NEGATIVE_INFINITY;
|
|
204
|
+
targets = /* @__PURE__ */ new Map();
|
|
205
|
+
constructor(schema, modules, runtime = wallClock()) {
|
|
206
|
+
this.runtime = runtime;
|
|
207
|
+
installConvexGlobal();
|
|
208
|
+
install(pinnedTime, () => executions.getStore() !== void 0);
|
|
209
|
+
this.components.set(ROOT, makeComponent(ROOT, schema, modules));
|
|
210
|
+
}
|
|
211
|
+
register(componentPath, schema, modules, standing = []) {
|
|
212
|
+
if (this.opened !== void 0) throw new RegisteredAfterOpen({
|
|
213
|
+
componentPath,
|
|
214
|
+
message: `Component "${componentPath}" was registered after the test backend opened. Register every component before the first call.`
|
|
215
|
+
});
|
|
216
|
+
this.components.set(componentPath, makeComponent(componentPath, schema, modules, standing));
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Load every registered module of every component, once, before any function
|
|
220
|
+
* runs, then check that each standing source names an exported function.
|
|
221
|
+
*/
|
|
222
|
+
open() {
|
|
223
|
+
this.opened ??= Promise.all(Array.from(this.components.values(), (component) => Promise.all(Array.from(component.loaders, ([path, load]) => load().then((module) => component.loaded.set(path, module)))))).then(() => this.checkStanding());
|
|
224
|
+
return this.opened;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Refuse a standing source no module of its component exports.
|
|
228
|
+
*/
|
|
229
|
+
checkStanding() {
|
|
230
|
+
for (const component of this.components.values()) for (const name of component.standing) {
|
|
231
|
+
const [modulePath, exportName = "default"] = name.split(":");
|
|
232
|
+
const func = component.loaded.get(modulePath)?.[exportName];
|
|
233
|
+
if (func?.isMutation !== true && func?.isAction !== true) throw new UnknownStandingSource({
|
|
234
|
+
componentPath: component.path,
|
|
235
|
+
name,
|
|
236
|
+
message: `Component "${component.path}" declares "${name}" standing, but exports no mutation or action by that name. Name the self-rescheduling function as module:export.`
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* The kernel's time: the runtime's, never earlier than the latest deadline
|
|
242
|
+
* time was advanced to.
|
|
243
|
+
*/
|
|
244
|
+
now() {
|
|
245
|
+
return Math.max(this.runtime.now(), this.floor);
|
|
246
|
+
}
|
|
247
|
+
component(componentPath) {
|
|
248
|
+
const component = this.components.get(componentPath);
|
|
249
|
+
if (component === void 0) throw unregistered(this, componentPath);
|
|
250
|
+
return component;
|
|
251
|
+
}
|
|
252
|
+
resolve(functionPath, kind) {
|
|
253
|
+
const [modulePath, maybeExport] = functionPath.udfPath.split(":");
|
|
254
|
+
const exportName = maybeExport ?? "default";
|
|
255
|
+
const module = this.component(functionPath.componentPath).loaded.get(modulePath);
|
|
256
|
+
if (module === void 0) throw new Error(`Could not find module for: "${modulePath}"`);
|
|
257
|
+
const func = module[exportName];
|
|
258
|
+
if (func === void 0) throw new Error(`Expected a Convex function exported from module "${modulePath}" as \`${exportName}\`, but there is no such export.`);
|
|
259
|
+
const handler = "_handler" in func ? func._handler : func;
|
|
260
|
+
if (typeof handler !== "function") throw new Error(`Expected a Convex function exported from module "${modulePath}" as \`${exportName}\`, but got: ${func}`);
|
|
261
|
+
if (kind !== "any" && !func[kindCheck[kind]]) throw new Error(`Expected a ${kind} function, but the function exported from module "${modulePath}" as \`${exportName}\` is not a ${kind}.`);
|
|
262
|
+
const args = JSON.parse(func.exportArgs());
|
|
263
|
+
if (args.type !== "object" && args.type !== "any") throw new Error(`Invalid JSON returned from ${modulePath}.js:${exportName}.exportArgs(): Args validator must be an object or any`);
|
|
264
|
+
return {
|
|
265
|
+
func,
|
|
266
|
+
handler,
|
|
267
|
+
args,
|
|
268
|
+
returns: JSON.parse(func.exportReturns())
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
query(caller, reference, args) {
|
|
272
|
+
return this.open().then(() => {
|
|
273
|
+
const from = {
|
|
274
|
+
caller,
|
|
275
|
+
request: newRequest(caller)
|
|
276
|
+
};
|
|
277
|
+
return typeof reference === "function" ? this.runQuery({
|
|
278
|
+
componentPath: ROOT,
|
|
279
|
+
udfPath: "inline"
|
|
280
|
+
}, {
|
|
281
|
+
handler: reference,
|
|
282
|
+
args: { type: "any" },
|
|
283
|
+
returns: null
|
|
284
|
+
}, args ?? {}, from) : this.queryAt(pathOfReference(reference, ROOT), args, from);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
mutation(caller, reference, args) {
|
|
288
|
+
return this.open().then(() => {
|
|
289
|
+
const from = {
|
|
290
|
+
caller,
|
|
291
|
+
request: newRequest(caller)
|
|
292
|
+
};
|
|
293
|
+
return typeof reference === "function" ? this.runMutation({
|
|
294
|
+
componentPath: ROOT,
|
|
295
|
+
udfPath: "inline"
|
|
296
|
+
}, {
|
|
297
|
+
handler: reference,
|
|
298
|
+
args: { type: "any" },
|
|
299
|
+
returns: null
|
|
300
|
+
}, args ?? {}, from) : this.mutationAt(pathOfReference(reference, ROOT), args, from);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
action(caller, reference, args) {
|
|
304
|
+
return this.open().then(() => {
|
|
305
|
+
const from = {
|
|
306
|
+
caller,
|
|
307
|
+
request: newRequest(caller)
|
|
308
|
+
};
|
|
309
|
+
return typeof reference === "function" ? this.runAction({
|
|
310
|
+
componentPath: ROOT,
|
|
311
|
+
udfPath: "inline"
|
|
312
|
+
}, {
|
|
313
|
+
handler: reference,
|
|
314
|
+
args: { type: "any" },
|
|
315
|
+
returns: null
|
|
316
|
+
}, args ?? {}, from) : this.actionAt(pathOfReference(reference, ROOT), args, from);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
run(caller, componentPath, handler) {
|
|
320
|
+
return this.open().then(() => {
|
|
321
|
+
this.component(componentPath);
|
|
322
|
+
return this.runMutation({
|
|
323
|
+
componentPath,
|
|
324
|
+
udfPath: "custom"
|
|
325
|
+
}, {
|
|
326
|
+
handler,
|
|
327
|
+
args: { type: "any" },
|
|
328
|
+
returns: null
|
|
329
|
+
}, {}, {
|
|
330
|
+
caller,
|
|
331
|
+
request: newRequest(caller)
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Run a mutation to its commit point and hold it as attempt 1.
|
|
337
|
+
*/
|
|
338
|
+
prepare(caller, reference, args) {
|
|
339
|
+
return this.open().then(() => {
|
|
340
|
+
const functionPath = pathOfReference(reference, ROOT);
|
|
341
|
+
const resolved = this.resolve(functionPath, "mutation");
|
|
342
|
+
validateFunctionArgs(resolved.args, args ?? {}, functionPath, false);
|
|
343
|
+
return this.prepareMutation(functionPath, resolved, args, {
|
|
344
|
+
caller,
|
|
345
|
+
request: newRequest(caller)
|
|
346
|
+
}, 1, current$1()).then((held) => held.view);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
apply(id) {
|
|
350
|
+
const held = this.holding.get(id);
|
|
351
|
+
return held === void 0 ? Promise.reject(new StaleAttempt({
|
|
352
|
+
id,
|
|
353
|
+
message: `The test kernel holds no attempt "${id}". It was already applied, or never prepared.`
|
|
354
|
+
})) : this.applyHeld(held);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* The held attempts, oldest first.
|
|
358
|
+
*/
|
|
359
|
+
held() {
|
|
360
|
+
return Array.from(this.holding.values(), (held) => held.view);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* The committed pending jobs, earliest deadline first.
|
|
364
|
+
*/
|
|
365
|
+
jobs() {
|
|
366
|
+
return pending(this.state, this.targets).map(({ id, componentPath, name, scheduledTime, standing }) => ({
|
|
367
|
+
id,
|
|
368
|
+
componentPath,
|
|
369
|
+
name,
|
|
370
|
+
scheduledTime,
|
|
371
|
+
standing
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Move the clock forward `ms`, running every job whose deadline falls on the
|
|
376
|
+
* way, in deadline order, each starting at its deadline. Jobs those jobs
|
|
377
|
+
* schedule inside the window run too. `advance(0)` runs what is due now and
|
|
378
|
+
* crosses no future deadline.
|
|
379
|
+
*/
|
|
380
|
+
advance(ms, budget = defaultBudget) {
|
|
381
|
+
return this.open().then(() => {
|
|
382
|
+
const until = this.now() + ms;
|
|
383
|
+
return this.walk((pending) => pending.filter((job) => job.scheduledTime <= until), budget).then(() => this.raiseFloor(until));
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Run jobs, earliest deadline first and each at its deadline, until only
|
|
388
|
+
* standing jobs are pending. Every job run counts against `budget`.
|
|
389
|
+
*/
|
|
390
|
+
drain(budget = defaultBudget) {
|
|
391
|
+
return this.open().then(() => this.walk((pending) => pending.filter((job) => !job.standing), budget));
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Run the first of `select(pending)` until it is empty, moving the clock to
|
|
395
|
+
* each job's deadline as it starts.
|
|
396
|
+
*/
|
|
397
|
+
walk(select, budget) {
|
|
398
|
+
const step = (ran) => {
|
|
399
|
+
const left = select(pending(this.state, this.targets));
|
|
400
|
+
const next = left[0];
|
|
401
|
+
if (next === void 0) return Promise.resolve();
|
|
402
|
+
if (ran === budget) {
|
|
403
|
+
const pending = functions(left);
|
|
404
|
+
return Promise.reject(new DrainBudgetExceeded({
|
|
405
|
+
budget,
|
|
406
|
+
pending,
|
|
407
|
+
message: `Scheduled functions ran ${budget} jobs and work remains: ${pending.map(({ componentPath, name }) => componentPath === ROOT ? name : `${name} in "${componentPath}"`).join(", ")}. A function that schedules its own next run never lets the drain end, so its kit declares it with registerComponent's \`standing\` option.`
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
this.raiseFloor(next.scheduledTime);
|
|
411
|
+
return this.runJob(next).then(() => step(ran + 1));
|
|
412
|
+
};
|
|
413
|
+
return step(0);
|
|
414
|
+
}
|
|
415
|
+
raiseFloor(to) {
|
|
416
|
+
this.floor = Math.max(this.floor, to);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Run one pending job as its own request, moving its row through in progress
|
|
420
|
+
* to success or failed. `walk` picks it from the committed pending jobs and
|
|
421
|
+
* starts it in the same turn, so no cancel can land in between. The job runs
|
|
422
|
+
* as a fresh invocation of the world that scheduled it, whatever invocation
|
|
423
|
+
* the drain itself runs under.
|
|
424
|
+
*/
|
|
425
|
+
runJob(job) {
|
|
426
|
+
this.commitNow((attempt) => {
|
|
427
|
+
const doc = attempt.get(job.componentPath, job.id);
|
|
428
|
+
attempt.write(job.componentPath, job.id, {
|
|
429
|
+
...doc,
|
|
430
|
+
state: { kind: "inProgress" }
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
const request = jobRequest(job.id);
|
|
434
|
+
const from = {
|
|
435
|
+
caller: anonymous,
|
|
436
|
+
request
|
|
437
|
+
};
|
|
438
|
+
const run = () => Promise.resolve().then(() => {
|
|
439
|
+
const { func } = this.resolve(job.target, "any");
|
|
440
|
+
return func.isQuery ? this.queryAt(job.target, job.args, from) : func.isMutation ? this.mutationAt(job.target, job.args, from) : this.actionAt(job.target, job.args, from);
|
|
441
|
+
});
|
|
442
|
+
return (job.world === void 0 ? unbound(run) : bind(fresh(job.world, job.target.componentPath, request), run)).then(() => ({ kind: "success" }), (error) => ({
|
|
443
|
+
kind: "failed",
|
|
444
|
+
error: error instanceof Error ? error.message : String(error)
|
|
445
|
+
})).then((state) => this.commitNow((attempt) => {
|
|
446
|
+
const doc = attempt.get(job.componentPath, job.id);
|
|
447
|
+
if (doc === null || stateOf(doc).kind !== "inProgress") return;
|
|
448
|
+
attempt.write(job.componentPath, job.id, {
|
|
449
|
+
...doc,
|
|
450
|
+
state,
|
|
451
|
+
completedTime: this.now()
|
|
452
|
+
});
|
|
453
|
+
}));
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Write through a fresh attempt and publish it at once, as the scheduler does
|
|
457
|
+
* for an action's schedule and a job's state. Nothing runs between the
|
|
458
|
+
* snapshot and the commit, so no other commit can conflict with it.
|
|
459
|
+
*/
|
|
460
|
+
commitNow(write) {
|
|
461
|
+
const attempt = new KernelAttempt(this.state);
|
|
462
|
+
const result = write(attempt);
|
|
463
|
+
this.publishWrites(attempt.writes());
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Publish `writes` as the next commit, at the runtime's timestamp for it. No
|
|
468
|
+
* writes publish nothing and ask the runtime for no timestamp, which answers
|
|
469
|
+
* the latest commit's.
|
|
470
|
+
*/
|
|
471
|
+
publishWrites(writes) {
|
|
472
|
+
if (writes.length === 0) return this.state.commitTs;
|
|
473
|
+
const commitTs = this.runtime.commitTs(this.state.version + 1);
|
|
474
|
+
if (commitTs <= this.state.commitTs) throw new CommitTsNotIncreasing({
|
|
475
|
+
version: this.state.version + 1,
|
|
476
|
+
previous: this.state.commitTs,
|
|
477
|
+
commitTs,
|
|
478
|
+
message: `The runtime answered commit timestamp ${commitTs} for version ${this.state.version + 1}, which is not greater than the previous commit's ${this.state.commitTs}.`
|
|
479
|
+
});
|
|
480
|
+
this.state = apply(this.state, writes, commitTs);
|
|
481
|
+
return commitTs;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Write a pending job into `attempt`, so the job exists only once that
|
|
485
|
+
* attempt publishes. A job that a standing function schedules, or that runs
|
|
486
|
+
* one, is standing.
|
|
487
|
+
*/
|
|
488
|
+
scheduleJob(attempt, execution, args) {
|
|
489
|
+
const target = functionPathOf(args, execution.componentPath);
|
|
490
|
+
this.component(target.componentPath);
|
|
491
|
+
const jobArgs = jsonToConvex(args.args);
|
|
492
|
+
if (hasPendingValues(jobArgs)) throw new Error(`Invalid arguments for "${target.udfPath}": a scheduled function cannot take an unresolved placeholder value.`);
|
|
493
|
+
const id = this.generateId(table);
|
|
494
|
+
attempt.write(execution.componentPath, id, row(id, this.creationTime(execution.startTime), target.udfPath, jobArgs, args.ts * 1e3));
|
|
495
|
+
this.targets.set(id, {
|
|
496
|
+
functionPath: target,
|
|
497
|
+
standing: this.isStanding({
|
|
498
|
+
componentPath: execution.componentPath,
|
|
499
|
+
udfPath: execution.udfPath
|
|
500
|
+
}) || this.isStanding(target),
|
|
501
|
+
world: current$1()?.world
|
|
502
|
+
});
|
|
503
|
+
return id;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Cancel a job that has not finished. A finished job stays as it ended.
|
|
507
|
+
*/
|
|
508
|
+
cancelJob(attempt, componentPath, id) {
|
|
509
|
+
validateId(table, id);
|
|
510
|
+
attempt.readDocument(componentPath, id);
|
|
511
|
+
const doc = attempt.get(componentPath, id);
|
|
512
|
+
if (doc === null) throw new Error(`Patch on non-existent document with ID "${id}"`);
|
|
513
|
+
const { kind } = stateOf(doc);
|
|
514
|
+
if (kind === "pending" || kind === "inProgress") attempt.write(componentPath, id, {
|
|
515
|
+
...doc,
|
|
516
|
+
state: { kind: "canceled" }
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
isStanding({ componentPath, udfPath }) {
|
|
520
|
+
return this.components.get(componentPath)?.standing.has(udfPath) ?? false;
|
|
521
|
+
}
|
|
522
|
+
queryAt(functionPath, args, from, snapshotOnly = false) {
|
|
523
|
+
const resolved = this.resolve(functionPath, "query");
|
|
524
|
+
validateFunctionArgs(resolved.args, args ?? {}, functionPath, from.parent?.attempt !== void 0);
|
|
525
|
+
return this.runQuery(functionPath, resolved, args, from, snapshotOnly);
|
|
526
|
+
}
|
|
527
|
+
mutationAt(functionPath, args, from) {
|
|
528
|
+
const resolved = this.resolve(functionPath, "mutation");
|
|
529
|
+
validateFunctionArgs(resolved.args, args ?? {}, functionPath, from.parent?.attempt !== void 0);
|
|
530
|
+
return this.runMutation(functionPath, resolved, args, from);
|
|
531
|
+
}
|
|
532
|
+
actionAt(functionPath, args, from) {
|
|
533
|
+
const resolved = this.resolve(functionPath, "action");
|
|
534
|
+
validateFunctionArgs(resolved.args, args ?? {}, functionPath, false);
|
|
535
|
+
return this.runAction(functionPath, resolved, args, from);
|
|
536
|
+
}
|
|
537
|
+
execution(functionPath, udfType, attempt, from, pagination) {
|
|
538
|
+
const identity = functionPath.componentPath !== ROOT ? null : from.parent !== void 0 ? from.parent.identity : from.caller.identity;
|
|
539
|
+
return {
|
|
540
|
+
world: this,
|
|
541
|
+
componentPath: functionPath.componentPath,
|
|
542
|
+
udfPath: functionPath.udfPath,
|
|
543
|
+
udfType,
|
|
544
|
+
attempt,
|
|
545
|
+
identity,
|
|
546
|
+
request: from.request,
|
|
547
|
+
startTime: from.parent !== void 0 && from.parent.udfType !== "action" ? from.parent.startTime : this.now(),
|
|
548
|
+
pagination,
|
|
549
|
+
turn: new Turn()
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
runQuery(functionPath, resolved, args, from, snapshotOnly = false) {
|
|
553
|
+
const parentAttempt = from.parent?.attempt;
|
|
554
|
+
const isNested = parentAttempt !== void 0;
|
|
555
|
+
const attempt = parentAttempt === void 0 ? new KernelAttempt(this.state) : snapshotOnly ? new KernelAttempt(parentAttempt.snapshot) : parentAttempt;
|
|
556
|
+
const execution = this.execution(functionPath, "query", attempt, from, isNested && from.parent?.pagination || { calls: 0 });
|
|
557
|
+
const q = queryGeneric({ handler: (ctx, a) => resolved.handler({
|
|
558
|
+
...ctx,
|
|
559
|
+
auth: queryAuth(execution)
|
|
560
|
+
}, a) });
|
|
561
|
+
return enter(execution, () => q.invokeQuery(JSON.stringify(convexToJson([parseArgs(args)])))).then((raw) => settled(execution, raw)).then((raw) => {
|
|
562
|
+
const result = jsonToConvex(JSON.parse(raw));
|
|
563
|
+
if (!isNested && hasPendingValues(result)) throw new Error(`Function ${functionPath.udfPath} return value invalid: queries cannot return an unresolved placeholder value.`);
|
|
564
|
+
validateReturnValue(resolved.returns, result, "query", functionPath);
|
|
565
|
+
return result;
|
|
566
|
+
}).catch((error) => {
|
|
567
|
+
throw deserializeConvexErrorData(error);
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
invokeMutation(execution, resolved, args) {
|
|
571
|
+
const m = mutationGeneric({ handler: (ctx, a) => resolved.handler({
|
|
572
|
+
...ctx,
|
|
573
|
+
auth: mutationAuth(execution)
|
|
574
|
+
}, a) });
|
|
575
|
+
return enter(execution, () => m.invokeMutation(JSON.stringify(convexToJson([parseArgs(args)])))).then((raw) => settled(execution, raw)).then((raw) => {
|
|
576
|
+
const result = jsonToConvex(JSON.parse(raw));
|
|
577
|
+
validateReturnValue(resolved.returns, result, "mutation", {
|
|
578
|
+
componentPath: execution.componentPath,
|
|
579
|
+
udfPath: execution.udfPath
|
|
580
|
+
});
|
|
581
|
+
return result;
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
runMutation(functionPath, resolved, args, from) {
|
|
585
|
+
const parentAttempt = from.parent?.attempt;
|
|
586
|
+
if (parentAttempt !== void 0) {
|
|
587
|
+
const frame = parentAttempt.child();
|
|
588
|
+
const execution = this.execution(functionPath, "mutation", frame, from, void 0);
|
|
589
|
+
return this.invokeMutation(execution, resolved, args).then((result) => frame.drained().then(() => {
|
|
590
|
+
frame.close();
|
|
591
|
+
frame.merge();
|
|
592
|
+
return result;
|
|
593
|
+
}), (error) => {
|
|
594
|
+
frame.close();
|
|
595
|
+
throw deserializeConvexErrorData(error);
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
return this.prepareMutation(functionPath, resolved, args, from, 1, current$1()).then((held) => this.publish(held));
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Run one attempt of a top-level mutation against the latest commit, then
|
|
602
|
+
* hold it. The commit point is once the function has returned and every
|
|
603
|
+
* syscall it started has settled. A test invocation bound when the mutation
|
|
604
|
+
* was first called reaches each attempt's handler with that attempt's
|
|
605
|
+
* number.
|
|
606
|
+
*/
|
|
607
|
+
prepareMutation(functionPath, resolved, args, from, number, bound) {
|
|
608
|
+
const attempt = new KernelAttempt(this.state, number);
|
|
609
|
+
const execution = this.execution(functionPath, "mutation", attempt, from, void 0);
|
|
610
|
+
const run = () => this.invokeMutation(execution, resolved, args);
|
|
611
|
+
return (bound === void 0 ? run() : bind({
|
|
612
|
+
...bound,
|
|
613
|
+
attempt: number
|
|
614
|
+
}, run)).then((value) => attempt.drained().then(() => ({
|
|
615
|
+
_tag: "returned",
|
|
616
|
+
value
|
|
617
|
+
})), (error) => ({
|
|
618
|
+
_tag: "failed",
|
|
619
|
+
error: deserializeConvexErrorData(error)
|
|
620
|
+
})).then((outcome) => {
|
|
621
|
+
attempt.close();
|
|
622
|
+
const held = {
|
|
623
|
+
view: {
|
|
624
|
+
id: `attempt-${this.nextAttemptId++}`,
|
|
625
|
+
request: from.request.requestId,
|
|
626
|
+
number,
|
|
627
|
+
snapshot: attempt.snapshot.version,
|
|
628
|
+
reads: attempt.reads,
|
|
629
|
+
outcome
|
|
630
|
+
},
|
|
631
|
+
attempt,
|
|
632
|
+
again: (next) => this.prepareMutation(functionPath, resolved, args, from, next, bound)
|
|
633
|
+
};
|
|
634
|
+
this.holding.set(held.view.id, held);
|
|
635
|
+
return held;
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Validate a held attempt's reads against every commit since its snapshot,
|
|
640
|
+
* then publish its writes or prepare its next attempt. An attempt that wrote
|
|
641
|
+
* nothing publishes nothing and has nothing to validate. It answers its
|
|
642
|
+
* snapshot's version and commit timestamp.
|
|
643
|
+
*/
|
|
644
|
+
applyHeld({ view, attempt, again }) {
|
|
645
|
+
this.holding.delete(view.id);
|
|
646
|
+
const { outcome } = view;
|
|
647
|
+
if (outcome._tag === "failed") return Promise.resolve({
|
|
648
|
+
kind: "refused",
|
|
649
|
+
tag: tagOf(outcome.error),
|
|
650
|
+
error: outcome.error
|
|
651
|
+
});
|
|
652
|
+
const writes = attempt.writes();
|
|
653
|
+
if (writes.length === 0) {
|
|
654
|
+
const { version, commitTs } = attempt.snapshot;
|
|
655
|
+
return Promise.resolve({
|
|
656
|
+
kind: "applied",
|
|
657
|
+
version,
|
|
658
|
+
commitTs,
|
|
659
|
+
result: resolveCommittedValues(outcome.value, commitTs)
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
if (conflict(this.state, view.snapshot, attempt.reads) !== void 0) {
|
|
663
|
+
if (view.number === lastAttempt) return Promise.resolve({
|
|
664
|
+
kind: "exhausted",
|
|
665
|
+
error: new OptimisticConcurrencyControlFailure({
|
|
666
|
+
attempts: lastAttempt,
|
|
667
|
+
message: `Documents read by the mutation changed before each of its ${lastAttempt} attempts could commit.`
|
|
668
|
+
})
|
|
669
|
+
});
|
|
670
|
+
return again(view.number + 1).then((next) => ({
|
|
671
|
+
kind: "conflict",
|
|
672
|
+
next: next.view
|
|
673
|
+
}));
|
|
674
|
+
}
|
|
675
|
+
let commitTs;
|
|
676
|
+
try {
|
|
677
|
+
commitTs = this.publishWrites(writes);
|
|
678
|
+
} catch (error) {
|
|
679
|
+
return Promise.reject(error);
|
|
680
|
+
}
|
|
681
|
+
return Promise.resolve({
|
|
682
|
+
kind: "applied",
|
|
683
|
+
version: this.state.version,
|
|
684
|
+
commitTs,
|
|
685
|
+
result: resolveCommittedValues(outcome.value, commitTs)
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Apply until the mutation publishes, is refused or is exhausted, as the
|
|
690
|
+
* binary retries a mutation that conflicts.
|
|
691
|
+
*/
|
|
692
|
+
publish(held) {
|
|
693
|
+
return this.applyHeld(held).then((applied) => {
|
|
694
|
+
if (applied.kind === "applied") return applied.result;
|
|
695
|
+
if (applied.kind === "conflict") return this.publish(this.holding.get(applied.next.id));
|
|
696
|
+
throw applied.error;
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
runAction(functionPath, resolved, args, from) {
|
|
700
|
+
const execution = this.execution(functionPath, "action", void 0, from, void 0);
|
|
701
|
+
const inner = {
|
|
702
|
+
request: from.request,
|
|
703
|
+
parent: execution
|
|
704
|
+
};
|
|
705
|
+
const a = actionGeneric({ handler: (ctx, innerArgs) => resolved.handler({
|
|
706
|
+
...ctx,
|
|
707
|
+
runQuery: (reference, queryArgs) => this.queryAt(pathOfReference(reference, functionPath.componentPath), queryArgs, inner),
|
|
708
|
+
runMutation: (reference, mutationArgs) => this.mutationAt(pathOfReference(reference, functionPath.componentPath), mutationArgs, inner),
|
|
709
|
+
runAction: (reference, actionArgs) => this.actionAt(pathOfReference(reference, functionPath.componentPath), actionArgs, inner),
|
|
710
|
+
auth: queryAuth(execution)
|
|
711
|
+
}, innerArgs) });
|
|
712
|
+
const run = () => enter(execution, () => a.invokeAction(from.request.requestId, JSON.stringify(convexToJson([parseArgs(args)]))));
|
|
713
|
+
const bound = from.parent === void 0 ? void 0 : current$1();
|
|
714
|
+
return (bound === void 0 ? run() : bind(fresh(bound.world, functionPath.componentPath, from.request), run)).then((raw) => {
|
|
715
|
+
const result = jsonToConvex(JSON.parse(raw));
|
|
716
|
+
validateReturnValue(resolved.returns, result, "action", functionPath);
|
|
717
|
+
return result;
|
|
718
|
+
}).catch((error) => {
|
|
719
|
+
throw deserializeConvexErrorData(error);
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
schema(componentPath) {
|
|
723
|
+
return this.component(componentPath).schema;
|
|
724
|
+
}
|
|
725
|
+
view(execution) {
|
|
726
|
+
const attempt = transaction(execution);
|
|
727
|
+
return {
|
|
728
|
+
schema: this.schema(execution.componentPath),
|
|
729
|
+
scan: (tableName) => attempt.scan(execution.componentPath, tableName),
|
|
730
|
+
read: (interval, after) => attempt.readInterval({
|
|
731
|
+
...interval,
|
|
732
|
+
componentPath: execution.componentPath,
|
|
733
|
+
after
|
|
734
|
+
})
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
generateId(table) {
|
|
738
|
+
return String(this.nextDocId++).padStart(TYPICAL_CONVEX_ID_LENGTH - table.length, "0") + table;
|
|
739
|
+
}
|
|
740
|
+
syscall(op, jsonArgs) {
|
|
741
|
+
const args = JSON.parse(jsonArgs);
|
|
742
|
+
const execution = current();
|
|
743
|
+
if (execution.attempt?.closed === true) throw closed(execution, op);
|
|
744
|
+
switch (op) {
|
|
745
|
+
case "1.0/getSnapshotTs": return JSON.stringify(convexToJson(transaction(execution).snapshot.commitTs));
|
|
746
|
+
case "1.0/queryStream": {
|
|
747
|
+
const queryId = this.nextQueryId++;
|
|
748
|
+
const view = this.view(execution);
|
|
749
|
+
let results;
|
|
750
|
+
this.queries.set(queryId, { next: () => (results ??= stream(view, args.query)).next() });
|
|
751
|
+
return JSON.stringify({ queryId });
|
|
752
|
+
}
|
|
753
|
+
case "1.0/queryCleanup":
|
|
754
|
+
this.queries.delete(args.queryId);
|
|
755
|
+
return JSON.stringify({});
|
|
756
|
+
case "1.0/db/normalizeId": {
|
|
757
|
+
const tableName = tableNameFromId(args.idString);
|
|
758
|
+
return JSON.stringify({ id: tableName === args.table ? args.idString : null });
|
|
759
|
+
}
|
|
760
|
+
default: throw new Error(`The test kernel does not support syscall: "${op}"`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* The binary queues a query's or mutation's async syscalls and runs them one
|
|
765
|
+
* at a time, oldest first (`start_async_syscall` and `run_inner` in
|
|
766
|
+
* `crates/isolate/src/environment/udf/mod.rs`, 1.49). A write and a nested
|
|
767
|
+
* call never share a batch (`AsyncSyscallBatch::can_push` in
|
|
768
|
+
* `async_syscall.rs`), and a nested call holds the queue until it has
|
|
769
|
+
* committed or rolled back its subtransaction (`run_udf` in
|
|
770
|
+
* `async_syscall.rs`). So a caller's write made after its nested call runs
|
|
771
|
+
* after that call, and a nested mutation sees every write made before it. An
|
|
772
|
+
* action's calls run side by side.
|
|
773
|
+
*/
|
|
774
|
+
asyncSyscall(op, jsonArgs) {
|
|
775
|
+
const execution = current();
|
|
776
|
+
const attempt = execution.attempt;
|
|
777
|
+
if (attempt === void 0) return Promise.resolve().then(() => this.dispatchAsync(op, JSON.parse(jsonArgs), execution));
|
|
778
|
+
if (attempt.closed) return Promise.reject(closed(execution, op));
|
|
779
|
+
const end = attempt.begin();
|
|
780
|
+
return execution.turn.take(() => this.dispatchAsync(op, JSON.parse(jsonArgs), execution)).finally(end);
|
|
781
|
+
}
|
|
782
|
+
dispatchAsync(op, args, execution) {
|
|
783
|
+
switch (op) {
|
|
784
|
+
case "1.0/get": {
|
|
785
|
+
const { table, id } = args;
|
|
786
|
+
return JSON.stringify(convexToJson(this.get(execution, table, id)));
|
|
787
|
+
}
|
|
788
|
+
case "1.0/queryStreamNext": {
|
|
789
|
+
const results = this.queries.get(args.queryId);
|
|
790
|
+
if (results === void 0) throw new Error("Bad queryId");
|
|
791
|
+
const { value, done } = results.next();
|
|
792
|
+
return JSON.stringify(convexToJson(done ? {
|
|
793
|
+
value: null,
|
|
794
|
+
done: true
|
|
795
|
+
} : {
|
|
796
|
+
value,
|
|
797
|
+
done: false
|
|
798
|
+
}));
|
|
799
|
+
}
|
|
800
|
+
case "1.0/queryPage": {
|
|
801
|
+
const pagination = execution.pagination;
|
|
802
|
+
if (pagination) {
|
|
803
|
+
pagination.calls += 1;
|
|
804
|
+
if (pagination.calls > 1) throw new Error("Only a single paginated query (`.paginate()`) is allowed per query, including nested query calls and component calls. This is a Convex limit: https://docs.convex.dev/database/pagination");
|
|
805
|
+
}
|
|
806
|
+
return JSON.stringify(convexToJson(paginate(this.view(execution), args)));
|
|
807
|
+
}
|
|
808
|
+
case "1.0/insert": return JSON.stringify({ _id: this.insert(execution, args.table, jsonToConvex(args.value)) });
|
|
809
|
+
case "1.0/shallowMerge":
|
|
810
|
+
this.patch(execution, args.table, args.id, args.value);
|
|
811
|
+
return JSON.stringify({});
|
|
812
|
+
case "1.0/replace":
|
|
813
|
+
this.replace(execution, args.table, args.id, args.value);
|
|
814
|
+
return JSON.stringify({});
|
|
815
|
+
case "1.0/remove":
|
|
816
|
+
this.remove(execution, args.table, args.id);
|
|
817
|
+
return JSON.stringify({});
|
|
818
|
+
case "1.0/count": {
|
|
819
|
+
const attempt = transaction(execution);
|
|
820
|
+
attempt.readInterval({
|
|
821
|
+
kind: "interval",
|
|
822
|
+
componentPath: execution.componentPath,
|
|
823
|
+
table: args.table,
|
|
824
|
+
fields: ["_creationTime", "_id"],
|
|
825
|
+
order: "asc",
|
|
826
|
+
range: [],
|
|
827
|
+
after: null
|
|
828
|
+
})("end");
|
|
829
|
+
return JSON.stringify(attempt.scan(execution.componentPath, args.table).length);
|
|
830
|
+
}
|
|
831
|
+
case "1.0/getFunctionMetadata": return JSON.stringify(convexToJson({
|
|
832
|
+
name: execution.udfPath,
|
|
833
|
+
componentPath: execution.componentPath
|
|
834
|
+
}));
|
|
835
|
+
case "1.0/getRequestMetadata":
|
|
836
|
+
if (execution.udfType === "query") throw new Error("getRequestMetadata() can only be called from a mutation, action or HTTP action. It is not available in queries.");
|
|
837
|
+
return JSON.stringify(convexToJson(execution.request));
|
|
838
|
+
case "1.0/getDeploymentMetadata": return JSON.stringify(convexToJson({
|
|
839
|
+
name: "test",
|
|
840
|
+
region: null,
|
|
841
|
+
class: "s16"
|
|
842
|
+
}));
|
|
843
|
+
case "1.0/actions/query":
|
|
844
|
+
case "1.0/actions/mutation":
|
|
845
|
+
case "1.0/actions/action": {
|
|
846
|
+
const from = {
|
|
847
|
+
request: execution.request,
|
|
848
|
+
parent: execution
|
|
849
|
+
};
|
|
850
|
+
const functionPath = functionPathOf(args, execution.componentPath);
|
|
851
|
+
return (op === "1.0/actions/query" ? this.queryAt(functionPath, args.args, from) : op === "1.0/actions/mutation" ? this.mutationAt(functionPath, args.args, from) : this.actionAt(functionPath, args.args, from)).then((value) => JSON.stringify(convexToJson(value)));
|
|
852
|
+
}
|
|
853
|
+
case "1.0/runUdf": {
|
|
854
|
+
const { udfType, args: udfArgs } = args;
|
|
855
|
+
const from = {
|
|
856
|
+
request: execution.request,
|
|
857
|
+
parent: execution
|
|
858
|
+
};
|
|
859
|
+
const functionPath = functionPathOf(args, execution.componentPath);
|
|
860
|
+
const value = jsonToConvex(udfArgs);
|
|
861
|
+
return (udfType === "query" ? this.queryAt(functionPath, value, from) : udfType === "snapshotQuery" ? this.queryAt(functionPath, value, from, true) : udfType === "mutation" ? this.mutationAt(functionPath, value, from) : Promise.reject(/* @__PURE__ */ new Error(`The test kernel does not support udf type: "${udfType}"`))).then((result) => JSON.stringify(convexToJson(result)));
|
|
862
|
+
}
|
|
863
|
+
case "1.0/createFunctionHandle": {
|
|
864
|
+
const functionPath = functionPathOf(args, execution.componentPath);
|
|
865
|
+
this.component(functionPath.componentPath);
|
|
866
|
+
return JSON.stringify(createFunctionHandle(functionPath));
|
|
867
|
+
}
|
|
868
|
+
case "1.0/auditLog": return JSON.stringify(null);
|
|
869
|
+
case "1.0/schedule": return JSON.stringify(convexToJson(this.scheduleJob(transaction(execution), execution, args)));
|
|
870
|
+
case "1.0/actions/schedule": return JSON.stringify(convexToJson(this.commitNow((attempt) => this.scheduleJob(attempt, execution, args))));
|
|
871
|
+
case "1.0/cancel_job":
|
|
872
|
+
this.cancelJob(transaction(execution), execution.componentPath, args.id);
|
|
873
|
+
return JSON.stringify({});
|
|
874
|
+
case "1.0/actions/cancel_job":
|
|
875
|
+
this.commitNow((attempt) => this.cancelJob(attempt, execution.componentPath, args.id));
|
|
876
|
+
return JSON.stringify({});
|
|
877
|
+
case "1.0/storageDelete":
|
|
878
|
+
case "1.0/storageGetUrl":
|
|
879
|
+
case "1.0/storageGenerateUploadUrl": throw rowPending("storage", 6);
|
|
880
|
+
case "1.0/getTransactionMetrics": throw rowPending("transaction limits", 6);
|
|
881
|
+
case "1.0/actions/vectorSearch": throw requiresBinary("vectorSearch");
|
|
882
|
+
default: throw new Error(`The test kernel does not support async syscall: "${op}"`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
jsSyscall(op) {
|
|
886
|
+
return Promise.reject(op.startsWith("storage/") ? rowPending("storage", 6) : /* @__PURE__ */ new Error(`The test kernel does not support js syscall: "${op}"`));
|
|
887
|
+
}
|
|
888
|
+
get(execution, tableName, id) {
|
|
889
|
+
validateId(tableName, id);
|
|
890
|
+
const attempt = transaction(execution);
|
|
891
|
+
attempt.readDocument(execution.componentPath, id);
|
|
892
|
+
return attempt.get(execution.componentPath, id);
|
|
893
|
+
}
|
|
894
|
+
insert(execution, table, value) {
|
|
895
|
+
const attempt = transaction(execution);
|
|
896
|
+
validateDocument(this.schema(execution.componentPath), table, value);
|
|
897
|
+
const _id = this.generateId(table);
|
|
898
|
+
attempt.write(execution.componentPath, _id, {
|
|
899
|
+
...value,
|
|
900
|
+
_id,
|
|
901
|
+
_creationTime: this.creationTime(execution.startTime)
|
|
902
|
+
});
|
|
903
|
+
return _id;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* A new document's creation time: `at`, or just past the latest one, so no
|
|
907
|
+
* two documents share one.
|
|
908
|
+
*/
|
|
909
|
+
creationTime(at) {
|
|
910
|
+
const time = at <= this.lastCreationTime ? this.lastCreationTime + .001 : at;
|
|
911
|
+
this.lastCreationTime = time;
|
|
912
|
+
return time;
|
|
913
|
+
}
|
|
914
|
+
existing(execution, tableName, id, write, value) {
|
|
915
|
+
validateId(tableName, id);
|
|
916
|
+
if (value !== void 0 && typeof value !== "object") throw new Error(`Invalid argument \`value\` in \`db.${write}\`, expected object but got '${typeof value}': ${value}`);
|
|
917
|
+
const attempt = transaction(execution);
|
|
918
|
+
attempt.readDocument(execution.componentPath, id);
|
|
919
|
+
const document = attempt.get(execution.componentPath, id);
|
|
920
|
+
if (document === null) throw new Error(write === "delete" ? "Delete on non-existent doc" : `${write === "patch" ? "Patch" : "Replace"} on non-existent document with ID "${id}"`);
|
|
921
|
+
return document;
|
|
922
|
+
}
|
|
923
|
+
fields(document, value) {
|
|
924
|
+
if (value._id !== void 0 && value._id !== document._id) throw new Error(`Provided \`_id\` field value "${value._id}" does not match the document ID "${document._id}"`);
|
|
925
|
+
if (value._creationTime !== void 0 && value._creationTime !== document._creationTime) throw new Error(`Provided \`_creationTime\` field value ${value._creationTime} does not match the document's creation time ${document._creationTime}`);
|
|
926
|
+
const convexValue = {};
|
|
927
|
+
for (const [key, v] of Object.entries(value)) if (key !== "_id" && key !== "_creationTime") convexValue[key] = evaluateValue(v);
|
|
928
|
+
return convexValue;
|
|
929
|
+
}
|
|
930
|
+
patch(execution, tableName, id, value) {
|
|
931
|
+
const document = this.existing(execution, tableName, id, "patch", value);
|
|
932
|
+
const { _id, _creationTime, ...fields } = document;
|
|
933
|
+
const merged = {
|
|
934
|
+
...fields,
|
|
935
|
+
...this.fields(document, value)
|
|
936
|
+
};
|
|
937
|
+
validateDocument(this.schema(execution.componentPath), tableNameFromId(_id), merged);
|
|
938
|
+
transaction(execution).write(execution.componentPath, _id, {
|
|
939
|
+
_id,
|
|
940
|
+
_creationTime,
|
|
941
|
+
...merged
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
replace(execution, tableName, id, value) {
|
|
945
|
+
const document = this.existing(execution, tableName, id, "replace", value);
|
|
946
|
+
const fields = this.fields(document, value);
|
|
947
|
+
validateDocument(this.schema(execution.componentPath), tableNameFromId(document._id), fields);
|
|
948
|
+
transaction(execution).write(execution.componentPath, document._id, {
|
|
949
|
+
...fields,
|
|
950
|
+
_id: document._id,
|
|
951
|
+
_creationTime: document._creationTime
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
remove(execution, tableName, id) {
|
|
955
|
+
const document = this.existing(execution, tableName, id, "delete", void 0);
|
|
956
|
+
transaction(execution).write(execution.componentPath, document._id, null);
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
const closed = (execution, operation) => new ClosedAttempt({
|
|
960
|
+
operation,
|
|
961
|
+
message: `"${operation}" reached mutation ${execution.udfPath} after it returned and its syscalls settled. Await every read and write inside the handler.`
|
|
962
|
+
});
|
|
963
|
+
const transaction = (execution) => {
|
|
964
|
+
if (execution.attempt === void 0) throw new Error("The database is only available in queries and mutations. An action reads and writes through `ctx.runQuery` and `ctx.runMutation`.");
|
|
965
|
+
return execution.attempt;
|
|
966
|
+
};
|
|
967
|
+
const queryAuth = (execution) => ({ getUserIdentity: () => Promise.resolve(execution.identity) });
|
|
968
|
+
const mutationAuth = (execution) => ({
|
|
969
|
+
...queryAuth(execution),
|
|
970
|
+
signJwt: () => Promise.reject(rowPending("JWT signing", 6))
|
|
971
|
+
});
|
|
972
|
+
function validateId(expectedTableName, id) {
|
|
973
|
+
if (typeof id !== "string") throw new Error(`Invalid argument \`id\`, expected string but got '${typeof id}': ${id}`);
|
|
974
|
+
if (expectedTableName === void 0) return;
|
|
975
|
+
if (typeof expectedTableName !== "string") throw new Error(`Invalid argument \`tableName\`, expected string but got '${typeof expectedTableName}': ${expectedTableName}`);
|
|
976
|
+
const actualTableName = tableNameFromId(id);
|
|
977
|
+
if (actualTableName === null) throw new Error(`Invalid argument \`id\`, expected ID value but got '${id}'`);
|
|
978
|
+
if (actualTableName !== expectedTableName) throw new Error(`Invalid argument \`id\`, expected ID in table '${expectedTableName}' but got ID in table '${actualTableName}'`);
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* The backend another library installed as `globalThis.Convex`, which answers
|
|
982
|
+
* every syscall made outside a kernel function.
|
|
983
|
+
*/
|
|
984
|
+
let forwarded;
|
|
985
|
+
const backend = () => executions.getStore()?.world ?? forwarded ?? current().world;
|
|
986
|
+
const dispatcher = {
|
|
987
|
+
[kernelBrand]: true,
|
|
988
|
+
syscall: (op, args) => backend().syscall(op, args),
|
|
989
|
+
asyncSyscall: (op, args) => backend().asyncSyscall(op, args),
|
|
990
|
+
jsSyscall: (op, args) => backend().jsSyscall(op, args)
|
|
991
|
+
};
|
|
992
|
+
/**
|
|
993
|
+
* Convex's server library reaches the backend through `globalThis.Convex`, one
|
|
994
|
+
* global for the whole test runtime. convex-test 0.0.65 assigns its own the
|
|
995
|
+
* first time it builds a world (its `dist/index.js:1936-1951`), whenever that
|
|
996
|
+
* is. The kernel keeps the global an accessor that always reads its dispatcher,
|
|
997
|
+
* and takes an assignment, or the global it found, as the backend for syscalls
|
|
998
|
+
* made outside a kernel function. So a kernel world and a convex-test world
|
|
999
|
+
* stay live together, whichever was built first, and no world restores the
|
|
1000
|
+
* global when it ends. A test that redefines the global removes the accessor,
|
|
1001
|
+
* so every kernel function installs it again over what the test left.
|
|
1002
|
+
*/
|
|
1003
|
+
const installConvexGlobal = () => {
|
|
1004
|
+
const found = globalThis.Convex;
|
|
1005
|
+
if (found?.[kernelBrand] === true) return;
|
|
1006
|
+
forwarded = found;
|
|
1007
|
+
Object.defineProperty(globalThis, "Convex", {
|
|
1008
|
+
get: () => dispatcher,
|
|
1009
|
+
set: (value) => {
|
|
1010
|
+
if (value?.[kernelBrand] !== true) forwarded = value;
|
|
1011
|
+
},
|
|
1012
|
+
configurable: true,
|
|
1013
|
+
enumerable: true
|
|
1014
|
+
});
|
|
1015
|
+
};
|
|
1016
|
+
/**
|
|
1017
|
+
* Whether `globalThis.Convex` is the kernel's, and no other backend installed
|
|
1018
|
+
* one behind it.
|
|
1019
|
+
*/
|
|
1020
|
+
const ownsConvexGlobal = () => globalThis.Convex?.[kernelBrand] === true && forwarded === void 0;
|
|
1021
|
+
//#endregion
|
|
1022
|
+
export { World, anonymous, ownsConvexGlobal, withIdentity };
|
|
1023
|
+
|
|
1024
|
+
//# sourceMappingURL=Execution.js.map
|