@gaganref/confect-test 10.3.1-fork.91 → 10.3.1-fork.93
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 +26 -0
- package/dist/TestConfect.d.ts.map +1 -1
- package/dist/TestConfect.js +9 -1
- package/dist/TestConfect.js.map +1 -1
- package/dist/internal/Attempt.d.ts +14 -1
- package/dist/internal/Attempt.d.ts.map +1 -1
- package/dist/internal/Attempt.js +18 -1
- package/dist/internal/Attempt.js.map +1 -1
- package/dist/internal/ConvexTest.d.ts +26 -1
- package/dist/internal/ConvexTest.d.ts.map +1 -1
- package/dist/internal/ConvexTest.js +55 -27
- package/dist/internal/ConvexTest.js.map +1 -1
- package/dist/internal/Execution.d.ts +151 -1
- package/dist/internal/Execution.d.ts.map +1 -1
- package/dist/internal/Execution.js +731 -214
- package/dist/internal/Execution.js.map +1 -1
- package/dist/internal/KernelQuery.d.ts +8 -3
- package/dist/internal/KernelQuery.d.ts.map +1 -1
- package/dist/internal/KernelQuery.js +10 -3
- package/dist/internal/KernelQuery.js.map +1 -1
- package/dist/internal/KernelRefusal.d.ts +134 -0
- package/dist/internal/KernelRefusal.d.ts.map +1 -1
- package/dist/internal/KernelRefusal.js +134 -1
- package/dist/internal/KernelRefusal.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/Redaction.d.ts +142 -0
- package/dist/internal/Redaction.d.ts.map +1 -0
- package/dist/internal/Redaction.js +448 -0
- package/dist/internal/Redaction.js.map +1 -0
- package/dist/internal/Snapshot.d.ts +298 -0
- package/dist/internal/Snapshot.d.ts.map +1 -0
- package/dist/internal/Snapshot.js +532 -0
- package/dist/internal/Snapshot.js.map +1 -0
- package/dist/internal/Subscriptions.d.ts +13 -0
- package/dist/internal/Subscriptions.d.ts.map +1 -1
- package/dist/internal/Subscriptions.js +27 -3
- package/dist/internal/Subscriptions.js.map +1 -1
- package/package.json +3 -3
- package/src/TestConfect.ts +63 -4
- package/src/internal/Attempt.ts +24 -0
- package/src/internal/ConvexTest.ts +135 -38
- package/src/internal/Execution.ts +1141 -324
- package/src/internal/KernelQuery.ts +17 -6
- package/src/internal/KernelRefusal.ts +160 -0
- package/src/internal/Metering.ts +520 -0
- package/src/internal/Redaction.ts +730 -0
- package/src/internal/Snapshot.ts +945 -0
- package/src/internal/Subscriptions.ts +34 -4
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { own } from "./Globals.js";
|
|
2
2
|
import { install } from "./Clock.js";
|
|
3
|
-
import { createFunctionHandle, createTestJwt, deserializeConvexErrorData, evaluateValue, hasPendingValues, parseArgs, parseFunctionHandle, resolveCommittedValues, simpleHash, tableNameFromId, validateFunctionArgs, validateReturnValue } from "./KernelValues.js";
|
|
3
|
+
import { createFunctionHandle, createTestJwt, deserializeConvexErrorData, evaluateValue, hasPendingValues, isSimpleObject, parseArgs, parseFunctionHandle, resolveCommittedValues, simpleHash, tableNameFromId, validateFunctionArgs, validateReturnValue } from "./KernelValues.js";
|
|
4
4
|
import { apply, conflict, empty, wallClock } from "./Kernel.js";
|
|
5
|
+
import { defaults, everything, examined, fromBudget, point } from "./Metering.js";
|
|
5
6
|
import { KernelAttempt } from "./Attempt.js";
|
|
6
7
|
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 { ClosedAttempt, CommitTsNotIncreasing, DrainBudgetExceeded, OptimisticConcurrencyControlFailure, RegisteredAfterOpen, SnapshotComponentMismatch, SnapshotJobInconsistent, SnapshotSubscribed, SnapshotWorldBusy, SnapshotWorldRunning, SnapshotWorldUnresolved, StaleAttempt, UnknownStandingSource, requiresBinary, rowPending } from "./KernelRefusal.js";
|
|
8
9
|
import { exportSchema, paginate, stream, validateDocument } from "./KernelQuery.js";
|
|
10
|
+
import { Secrets, failure, keep, mark, markValue, marker, none, sensitiveAt, unmark } from "./Redaction.js";
|
|
9
11
|
import { functions, pending, row, stateOf, table } from "./Scheduling.js";
|
|
12
|
+
import { check, contract, decode, encode } from "./Snapshot.js";
|
|
10
13
|
import { actionGeneric, getFunctionAddress, mutationGeneric, queryGeneric } from "convex/server";
|
|
11
14
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
12
|
-
import { convexToJson, jsonToConvex } from "convex/values";
|
|
15
|
+
import { CommitTsPlaceholder, compareValues, convexToJson, jsonToConvex } from "convex/values";
|
|
16
|
+
import * as Result from "effect/Result";
|
|
13
17
|
//#region src/internal/Execution.ts
|
|
14
18
|
const ROOT = "";
|
|
15
19
|
const TYPICAL_CONVEX_ID_LENGTH = 32;
|
|
@@ -97,6 +101,23 @@ const pinnedTime = () => {
|
|
|
97
101
|
* `finishAllScheduledFunctions` defaults to.
|
|
98
102
|
*/
|
|
99
103
|
const defaultBudget = 100;
|
|
104
|
+
const unresolvedWorld = (name, reason) => new SnapshotWorldUnresolved({
|
|
105
|
+
name,
|
|
106
|
+
reason,
|
|
107
|
+
message: reason === "missing" ? `A snapshot job runs under the test world "${name}", which no world passed to restore is named. Pass that world.` : `Two different test worlds are named "${name}", so a snapshot cannot tell their jobs apart. Name each world uniquely.`
|
|
108
|
+
});
|
|
109
|
+
/**
|
|
110
|
+
* `shape` placed at `at`, or no placement when there is no shape.
|
|
111
|
+
*/
|
|
112
|
+
const placed = (shape, at = []) => shape === void 0 ? [] : [{
|
|
113
|
+
at,
|
|
114
|
+
shape
|
|
115
|
+
}];
|
|
116
|
+
const running = (registry, count, what) => new SnapshotWorldRunning({
|
|
117
|
+
registry,
|
|
118
|
+
count,
|
|
119
|
+
message: `The test backend is running ${count} ${what}, whose effects a snapshot would split. Await them first.`
|
|
120
|
+
});
|
|
100
121
|
const current = () => {
|
|
101
122
|
const execution = executions.getStore();
|
|
102
123
|
if (execution === void 0) throw new Error("A Convex syscall ran outside any function the test kernel started.");
|
|
@@ -190,6 +211,7 @@ const kindCheck = {
|
|
|
190
211
|
*/
|
|
191
212
|
var World = class {
|
|
192
213
|
runtime;
|
|
214
|
+
redaction;
|
|
193
215
|
components = /* @__PURE__ */ new Map();
|
|
194
216
|
state = empty;
|
|
195
217
|
opened;
|
|
@@ -205,8 +227,31 @@ var World = class {
|
|
|
205
227
|
*/
|
|
206
228
|
floor = Number.NEGATIVE_INFINITY;
|
|
207
229
|
targets = /* @__PURE__ */ new Map();
|
|
208
|
-
|
|
230
|
+
/**
|
|
231
|
+
* The raw secrets this world has seen under a secret schema node. Replay
|
|
232
|
+
* state, kept in memory and never written to an artifact.
|
|
233
|
+
*/
|
|
234
|
+
secrets = new Secrets();
|
|
235
|
+
/**
|
|
236
|
+
* The live registries `Snapshot.contract.live` names, each a count: calls
|
|
237
|
+
* from the test, scheduler walks, function executions (nested and detached
|
|
238
|
+
* ones included) and registered subscribers. Held attempts are `holding`.
|
|
239
|
+
*/
|
|
240
|
+
live = {
|
|
241
|
+
running: 0,
|
|
242
|
+
walks: 0,
|
|
243
|
+
functions: 0,
|
|
244
|
+
subscribers: 0
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* The latest identity version a subscriber's `authenticate` reached. Each
|
|
248
|
+
* subscriber starts at the epoch, and each identity change advances it, so a
|
|
249
|
+
* restored world never hands out a version its source already used.
|
|
250
|
+
*/
|
|
251
|
+
identityEpoch = 0;
|
|
252
|
+
constructor(schema, modules, runtime = wallClock(), redaction = none) {
|
|
209
253
|
this.runtime = runtime;
|
|
254
|
+
this.redaction = redaction;
|
|
210
255
|
installConvexGlobal();
|
|
211
256
|
install(pinnedTime, () => executions.getStore() !== void 0);
|
|
212
257
|
this.components.set(ROOT, makeComponent(ROOT, schema, modules));
|
|
@@ -272,66 +317,74 @@ var World = class {
|
|
|
272
317
|
};
|
|
273
318
|
}
|
|
274
319
|
query(caller, reference, args) {
|
|
275
|
-
return this.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
320
|
+
return this.during(() => {
|
|
321
|
+
return this.open().then(() => {
|
|
322
|
+
const from = {
|
|
323
|
+
caller,
|
|
324
|
+
request: newRequest(caller)
|
|
325
|
+
};
|
|
326
|
+
return typeof reference === "function" ? this.runQuery({
|
|
327
|
+
componentPath: ROOT,
|
|
328
|
+
udfPath: "inline"
|
|
329
|
+
}, {
|
|
330
|
+
handler: reference,
|
|
331
|
+
args: { type: "any" },
|
|
332
|
+
returns: null
|
|
333
|
+
}, args ?? {}, from) : this.queryAt(pathOfReference(reference, ROOT), args, from);
|
|
334
|
+
});
|
|
288
335
|
});
|
|
289
336
|
}
|
|
290
337
|
mutation(caller, reference, args) {
|
|
291
|
-
return this.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
338
|
+
return this.during(() => {
|
|
339
|
+
return this.open().then(() => {
|
|
340
|
+
const from = {
|
|
341
|
+
caller,
|
|
342
|
+
request: newRequest(caller)
|
|
343
|
+
};
|
|
344
|
+
return typeof reference === "function" ? this.runMutation({
|
|
345
|
+
componentPath: ROOT,
|
|
346
|
+
udfPath: "inline"
|
|
347
|
+
}, {
|
|
348
|
+
handler: reference,
|
|
349
|
+
args: { type: "any" },
|
|
350
|
+
returns: null
|
|
351
|
+
}, args ?? {}, from) : this.mutationAt(pathOfReference(reference, ROOT), args, from);
|
|
352
|
+
});
|
|
304
353
|
});
|
|
305
354
|
}
|
|
306
355
|
action(caller, reference, args) {
|
|
307
|
-
return this.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
356
|
+
return this.during(() => {
|
|
357
|
+
return this.open().then(() => {
|
|
358
|
+
const from = {
|
|
359
|
+
caller,
|
|
360
|
+
request: newRequest(caller)
|
|
361
|
+
};
|
|
362
|
+
return typeof reference === "function" ? this.runAction({
|
|
363
|
+
componentPath: ROOT,
|
|
364
|
+
udfPath: "inline"
|
|
365
|
+
}, {
|
|
366
|
+
handler: reference,
|
|
367
|
+
args: { type: "any" },
|
|
368
|
+
returns: null
|
|
369
|
+
}, args ?? {}, from) : this.actionAt(pathOfReference(reference, ROOT), args, from);
|
|
370
|
+
});
|
|
320
371
|
});
|
|
321
372
|
}
|
|
322
373
|
run(caller, componentPath, handler) {
|
|
323
|
-
return this.
|
|
324
|
-
this.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
374
|
+
return this.during(() => {
|
|
375
|
+
return this.open().then(() => {
|
|
376
|
+
this.component(componentPath);
|
|
377
|
+
return this.runMutation({
|
|
378
|
+
componentPath,
|
|
379
|
+
udfPath: "custom"
|
|
380
|
+
}, {
|
|
381
|
+
handler,
|
|
382
|
+
args: { type: "any" },
|
|
383
|
+
returns: null
|
|
384
|
+
}, {}, {
|
|
385
|
+
caller,
|
|
386
|
+
request: newRequest(caller)
|
|
387
|
+
});
|
|
335
388
|
});
|
|
336
389
|
});
|
|
337
390
|
}
|
|
@@ -339,22 +392,27 @@ var World = class {
|
|
|
339
392
|
* Run a mutation to its commit point and hold it as attempt 1.
|
|
340
393
|
*/
|
|
341
394
|
prepare(caller, reference, args) {
|
|
342
|
-
return this.
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
395
|
+
return this.during(() => {
|
|
396
|
+
return this.open().then(() => {
|
|
397
|
+
const functionPath = pathOfReference(reference, ROOT);
|
|
398
|
+
const resolved = this.resolve(functionPath, "mutation");
|
|
399
|
+
this.keepArgs(functionPath, args);
|
|
400
|
+
validateFunctionArgs(resolved.args, args ?? {}, functionPath, false);
|
|
401
|
+
return this.prepareMutation(functionPath, resolved, args, {
|
|
402
|
+
caller,
|
|
403
|
+
request: newRequest(caller)
|
|
404
|
+
}, 1, current$1()).then((held) => held.view);
|
|
405
|
+
});
|
|
350
406
|
});
|
|
351
407
|
}
|
|
352
408
|
apply(id) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
409
|
+
return this.during(() => {
|
|
410
|
+
const held = this.holding.get(id);
|
|
411
|
+
return held === void 0 ? Promise.reject(new StaleAttempt({
|
|
412
|
+
id,
|
|
413
|
+
message: `The test kernel holds no attempt "${id}". It was already applied, or never prepared.`
|
|
414
|
+
})) : this.applyHeld(held);
|
|
415
|
+
});
|
|
358
416
|
}
|
|
359
417
|
/**
|
|
360
418
|
* The held attempts, oldest first.
|
|
@@ -369,37 +427,226 @@ var World = class {
|
|
|
369
427
|
* read where an earlier run's ended.
|
|
370
428
|
*/
|
|
371
429
|
subscribed(caller, reference, args, pinned) {
|
|
372
|
-
return this.
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
430
|
+
return this.during(() => {
|
|
431
|
+
return this.open().then(() => {
|
|
432
|
+
const functionPath = pathOfReference(reference, ROOT);
|
|
433
|
+
const resolved = this.resolve(functionPath, "query");
|
|
434
|
+
this.keepArgs(functionPath, args);
|
|
435
|
+
validateFunctionArgs(resolved.args, args ?? {}, functionPath, false);
|
|
436
|
+
const top = {
|
|
437
|
+
attempt: new KernelAttempt(this.state),
|
|
438
|
+
pagination: {
|
|
439
|
+
calls: 0,
|
|
440
|
+
journal: pinned.journal,
|
|
441
|
+
end: null
|
|
442
|
+
},
|
|
443
|
+
startTime: pinned.time
|
|
444
|
+
};
|
|
445
|
+
const run = (outcome) => ({
|
|
446
|
+
outcome,
|
|
447
|
+
snapshot: top.attempt.snapshot.version,
|
|
448
|
+
reads: top.attempt.reads,
|
|
449
|
+
observedIdentity: top.attempt.observedIdentity,
|
|
450
|
+
time: top.attempt.observedTime,
|
|
451
|
+
journal: pinned.journal ?? top.pagination.end
|
|
452
|
+
});
|
|
453
|
+
return this.runQuery(functionPath, resolved, args, {
|
|
454
|
+
caller,
|
|
455
|
+
request: newRequest(caller)
|
|
456
|
+
}, false, top).then((value) => run({
|
|
457
|
+
_tag: "returned",
|
|
458
|
+
value
|
|
459
|
+
}), (error) => run({
|
|
460
|
+
_tag: "failed",
|
|
461
|
+
error
|
|
462
|
+
}));
|
|
392
463
|
});
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Run `call`, a call from the test, counted as running until it settles.
|
|
468
|
+
*/
|
|
469
|
+
during(call) {
|
|
470
|
+
return this.track("running", call);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Run `work` counted in the live registry `registry` until it settles.
|
|
474
|
+
*/
|
|
475
|
+
track(registry, work) {
|
|
476
|
+
this.live[registry]++;
|
|
477
|
+
const settle = () => {
|
|
478
|
+
this.live[registry]--;
|
|
479
|
+
};
|
|
480
|
+
try {
|
|
481
|
+
return work().finally(settle);
|
|
482
|
+
} catch (error) {
|
|
483
|
+
settle();
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* How many entries each live registry holds now.
|
|
489
|
+
*/
|
|
490
|
+
liveCounts() {
|
|
491
|
+
return {
|
|
492
|
+
running: this.live.running,
|
|
493
|
+
walks: this.live.walks,
|
|
494
|
+
functions: this.live.functions,
|
|
495
|
+
held: this.holding.size,
|
|
496
|
+
subscribers: this.live.subscribers
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Count a subscriber the shim's registry created. Subscribers live as long as
|
|
501
|
+
* their test, so the count never falls.
|
|
502
|
+
*/
|
|
503
|
+
registerSubscriber() {
|
|
504
|
+
this.live.subscribers++;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* The identity version a new subscriber starts at.
|
|
508
|
+
*/
|
|
509
|
+
currentIdentityEpoch() {
|
|
510
|
+
return this.identityEpoch;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* The next identity version, for a subscriber whose identity changed.
|
|
514
|
+
*/
|
|
515
|
+
nextIdentityEpoch() {
|
|
516
|
+
return ++this.identityEpoch;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* The committed state as snapshot bytes: every component's rows and the
|
|
520
|
+
* version that wrote each, the write log, the jobs and their targets, the
|
|
521
|
+
* clock's floor and the identity counters. Each job's world is recorded by
|
|
522
|
+
* name. Two exports of the same state are the same bytes. A world running a
|
|
523
|
+
* call or holding attempts refuses, since a snapshot carries neither.
|
|
524
|
+
*/
|
|
525
|
+
exportSnapshot() {
|
|
526
|
+
this.refuseBusy(0, "export");
|
|
527
|
+
const names = /* @__PURE__ */ new Map();
|
|
528
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
529
|
+
for (const byTable of this.state.tables.values()) for (const id of byTable.get("_scheduled_functions")?.keys() ?? []) {
|
|
530
|
+
const target = this.targets.get(id);
|
|
531
|
+
if (target === void 0) continue;
|
|
532
|
+
const world = target.world;
|
|
533
|
+
if (world !== void 0) {
|
|
534
|
+
if ((names.get(world.name) ?? world) !== world) throw unresolvedWorld(world.name, "ambiguous");
|
|
535
|
+
names.set(world.name, world);
|
|
536
|
+
}
|
|
537
|
+
jobs.set(id, {
|
|
538
|
+
functionPath: target.functionPath,
|
|
539
|
+
standing: target.standing,
|
|
540
|
+
world: world?.name ?? null
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return encode({
|
|
544
|
+
components: [...this.components.keys()],
|
|
545
|
+
state: this.state,
|
|
546
|
+
floor: this.floor,
|
|
547
|
+
identity: {
|
|
548
|
+
nextDocumentId: this.nextDocId,
|
|
549
|
+
lastCreationTime: this.lastCreationTime,
|
|
550
|
+
nextAttemptId: this.nextAttemptId,
|
|
551
|
+
identityEpoch: this.identityEpoch
|
|
552
|
+
},
|
|
553
|
+
jobs
|
|
554
|
+
}, (componentPath, table, json) => mark(json, this.documentPlacements(componentPath, table, json), this.secrets));
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Replace this world's state with a snapshot's. The world must register the
|
|
558
|
+
* snapshot's components, run nothing, and have committed nothing and hold
|
|
559
|
+
* nothing. Each job's world resolves by name among `worlds`. Bytes whose
|
|
560
|
+
* digest does not match, and state the kernel could not have reached, refuse
|
|
561
|
+
* before anything changes.
|
|
562
|
+
*/
|
|
563
|
+
restoreSnapshot(bytes, options = {}) {
|
|
564
|
+
return this.open().then(() => this.restore(bytes, options.worlds ?? [], options.secrets ?? new Secrets()));
|
|
565
|
+
}
|
|
566
|
+
restore(bytes, worlds, secrets) {
|
|
567
|
+
this.refuseBusy(this.state.version, "restore");
|
|
568
|
+
const decoded = decode(bytes, (json) => unmark(json, secrets));
|
|
569
|
+
if (Result.isFailure(decoded)) throw decoded.failure;
|
|
570
|
+
const image = decoded.success;
|
|
571
|
+
const checked = check(image, table);
|
|
572
|
+
if (Result.isFailure(checked)) throw checked.failure;
|
|
573
|
+
const registered = [...this.components.keys()].sort();
|
|
574
|
+
const snapshot = [...image.components].sort();
|
|
575
|
+
if (registered.join("\0") !== snapshot.join("\0")) throw new SnapshotComponentMismatch({
|
|
576
|
+
snapshot,
|
|
577
|
+
registered,
|
|
578
|
+
message: `The snapshot holds components ${snapshot.map((p) => `"${p}"`).join(", ")}, but this test backend registers ${registered.map((p) => `"${p}"`).join(", ")}. Register the same components before restoring.`
|
|
579
|
+
});
|
|
580
|
+
for (const [id, { functionPath }] of image.jobs) if (!this.exports(functionPath)) throw new SnapshotJobInconsistent({
|
|
581
|
+
id,
|
|
582
|
+
reason: "function",
|
|
583
|
+
message: `The snapshot's job ${id} targets ${functionPath.udfPath} in "${functionPath.componentPath}", which that component does not export.`
|
|
584
|
+
});
|
|
585
|
+
const byName = /* @__PURE__ */ new Map();
|
|
586
|
+
for (const world of worlds) {
|
|
587
|
+
if ((byName.get(world.name) ?? world) !== world) throw unresolvedWorld(world.name, "ambiguous");
|
|
588
|
+
byName.set(world.name, world);
|
|
589
|
+
}
|
|
590
|
+
const targets = Array.from(image.jobs, ([id, { functionPath, standing, world }]) => {
|
|
591
|
+
const resolved = world === null ? void 0 : byName.get(world);
|
|
592
|
+
if (world !== null && resolved === void 0) throw unresolvedWorld(world, "missing");
|
|
593
|
+
return [id, {
|
|
594
|
+
functionPath,
|
|
595
|
+
standing,
|
|
596
|
+
world: resolved
|
|
597
|
+
}];
|
|
598
|
+
});
|
|
599
|
+
this.state = image.state;
|
|
600
|
+
this.floor = image.floor;
|
|
601
|
+
this.nextDocId = image.identity.nextDocumentId;
|
|
602
|
+
this.lastCreationTime = image.identity.lastCreationTime;
|
|
603
|
+
this.nextAttemptId = image.identity.nextAttemptId;
|
|
604
|
+
this.identityEpoch = image.identity.identityEpoch;
|
|
605
|
+
this.targets.clear();
|
|
606
|
+
for (const [id, target] of targets) this.targets.set(id, target);
|
|
607
|
+
this.secrets = secrets.copy();
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Whether `functionPath`'s component exports a Convex function there.
|
|
611
|
+
*/
|
|
612
|
+
exports({ componentPath, udfPath }) {
|
|
613
|
+
const [modulePath, exportName = "default"] = udfPath.split(":");
|
|
614
|
+
const func = this.components.get(componentPath)?.loaded.get(modulePath)?.[exportName];
|
|
615
|
+
return func?.isQuery === true || func?.isMutation === true || func?.isAction === true;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Refuse a snapshot while any live registry `operation` waits on is
|
|
619
|
+
* non-empty, or, restoring, once `version` shows the world committed
|
|
620
|
+
* something of its own.
|
|
621
|
+
*/
|
|
622
|
+
refuseBusy(version, operation) {
|
|
623
|
+
const counts = this.liveCounts();
|
|
624
|
+
const refusals = {
|
|
625
|
+
running: (count) => running("running", count, "calls from the test"),
|
|
626
|
+
walks: (count) => running("walks", count, "scheduler walks"),
|
|
627
|
+
functions: (count) => running("functions", count, "functions, nested and detached ones included"),
|
|
628
|
+
held: () => {
|
|
629
|
+
const held = [...this.holding.keys()];
|
|
630
|
+
return new SnapshotWorldBusy({
|
|
631
|
+
version,
|
|
632
|
+
held,
|
|
633
|
+
message: `The test backend holds attempts ${held.join(", ")}, which a snapshot does not carry. Apply them first.`
|
|
634
|
+
});
|
|
635
|
+
},
|
|
636
|
+
subscribers: (count) => new SnapshotSubscribed({
|
|
637
|
+
subscribers: count,
|
|
638
|
+
message: `The test backend has ${count} subscribers, whose identity versions a restore would rewind. Restore into a test backend no client has subscribed to.`
|
|
639
|
+
})
|
|
640
|
+
};
|
|
641
|
+
for (const key of Object.keys(refusals)) {
|
|
642
|
+
const { blocks } = contract.live[key];
|
|
643
|
+
if (operation === "export" && blocks === "restore") continue;
|
|
644
|
+
if (counts[key] > 0) throw refusals[key](counts[key]);
|
|
645
|
+
}
|
|
646
|
+
if (operation === "restore" && version !== 0) throw new SnapshotWorldBusy({
|
|
647
|
+
version,
|
|
648
|
+
held: [],
|
|
649
|
+
message: `The test backend has committed ${version} versions of its own. Restore a snapshot into a fresh test backend.`
|
|
403
650
|
});
|
|
404
651
|
}
|
|
405
652
|
/**
|
|
@@ -421,9 +668,11 @@ var World = class {
|
|
|
421
668
|
* crosses no future deadline.
|
|
422
669
|
*/
|
|
423
670
|
advance(ms, budget = defaultBudget) {
|
|
424
|
-
return this.
|
|
425
|
-
|
|
426
|
-
|
|
671
|
+
return this.track("walks", () => {
|
|
672
|
+
return this.open().then(() => {
|
|
673
|
+
const until = this.now() + ms;
|
|
674
|
+
return this.walk((pending) => pending.filter((job) => job.scheduledTime <= until), budget).then(() => this.raiseFloor(until));
|
|
675
|
+
});
|
|
427
676
|
});
|
|
428
677
|
}
|
|
429
678
|
/**
|
|
@@ -431,7 +680,9 @@ var World = class {
|
|
|
431
680
|
* standing jobs are pending. Every job run counts against `budget`.
|
|
432
681
|
*/
|
|
433
682
|
drain(budget = defaultBudget) {
|
|
434
|
-
return this.
|
|
683
|
+
return this.track("walks", () => {
|
|
684
|
+
return this.open().then(() => this.walk((pending) => pending.filter((job) => !job.standing), budget));
|
|
685
|
+
});
|
|
435
686
|
}
|
|
436
687
|
/**
|
|
437
688
|
* Run the first of `select(pending)` until it is empty, moving the clock to
|
|
@@ -532,8 +783,14 @@ var World = class {
|
|
|
532
783
|
const target = functionPathOf(args, execution.componentPath);
|
|
533
784
|
this.component(target.componentPath);
|
|
534
785
|
const jobArgs = jsonToConvex(args.args);
|
|
786
|
+
this.keepArgs(target, jobArgs);
|
|
535
787
|
if (hasPendingValues(jobArgs)) throw new Error(`Invalid arguments for "${target.udfPath}": a scheduled function cannot take an unresolved placeholder value.`);
|
|
788
|
+
refuse(attempt.meter.schedule(jobArgs, execution.limits));
|
|
789
|
+
const jobsIndex = indexKey(execution.componentPath, table, "by_id");
|
|
790
|
+
const parentJob = execution.request.scheduledFunctionId;
|
|
791
|
+
if (parentJob !== null) refuse(attempt.meter.readInterval(jobsIndex, point([parentJob]), execution.limits));
|
|
536
792
|
const id = this.generateId(table);
|
|
793
|
+
attempt.meter.deriveInterval(jobsIndex, point([id]));
|
|
537
794
|
attempt.write(execution.componentPath, id, row(id, this.creationTime(execution.startTime), target.udfPath, jobArgs, args.ts * 1e3));
|
|
538
795
|
this.targets.set(id, {
|
|
539
796
|
functionPath: target,
|
|
@@ -548,10 +805,10 @@ var World = class {
|
|
|
548
805
|
/**
|
|
549
806
|
* Cancel a job that has not finished. A finished job stays as it ended.
|
|
550
807
|
*/
|
|
551
|
-
cancelJob(attempt,
|
|
808
|
+
cancelJob(attempt, execution, id) {
|
|
809
|
+
const { componentPath } = execution;
|
|
552
810
|
validateId(table, id);
|
|
553
|
-
|
|
554
|
-
const doc = attempt.get(componentPath, id);
|
|
811
|
+
const doc = this.readById(attempt, execution, id);
|
|
555
812
|
if (doc === null) throw new Error(`Patch on non-existent document with ID "${id}"`);
|
|
556
813
|
const { kind } = stateOf(doc);
|
|
557
814
|
if (kind === "pending" || kind === "inProgress") attempt.write(componentPath, id, {
|
|
@@ -564,21 +821,26 @@ var World = class {
|
|
|
564
821
|
}
|
|
565
822
|
queryAt(functionPath, args, from, snapshotOnly = false) {
|
|
566
823
|
const resolved = this.resolve(functionPath, "query");
|
|
824
|
+
this.keepArgs(functionPath, args);
|
|
567
825
|
validateFunctionArgs(resolved.args, args ?? {}, functionPath, from.parent?.attempt !== void 0);
|
|
568
826
|
return this.runQuery(functionPath, resolved, args, from, snapshotOnly);
|
|
569
827
|
}
|
|
570
828
|
mutationAt(functionPath, args, from) {
|
|
571
829
|
const resolved = this.resolve(functionPath, "mutation");
|
|
830
|
+
this.keepArgs(functionPath, args);
|
|
572
831
|
validateFunctionArgs(resolved.args, args ?? {}, functionPath, from.parent?.attempt !== void 0);
|
|
573
832
|
return this.runMutation(functionPath, resolved, args, from);
|
|
574
833
|
}
|
|
575
834
|
actionAt(functionPath, args, from) {
|
|
576
835
|
const resolved = this.resolve(functionPath, "action");
|
|
836
|
+
this.keepArgs(functionPath, args);
|
|
577
837
|
validateFunctionArgs(resolved.args, args ?? {}, functionPath, false);
|
|
578
838
|
return this.runAction(functionPath, resolved, args, from);
|
|
579
839
|
}
|
|
580
840
|
execution(functionPath, udfType, attempt, from, pagination, startTime) {
|
|
581
841
|
const identity = functionPath.componentPath !== ROOT ? null : from.parent !== void 0 ? from.parent.identity : from.caller.identity;
|
|
842
|
+
const inherited = attempt === void 0 || from.parent?.attempt === void 0 ? defaults : from.parent.limits;
|
|
843
|
+
const limits = attempt === void 0 || from.budget === void 0 ? inherited : fromBudget(from.budget, attempt.meter.used(), inherited);
|
|
582
844
|
return {
|
|
583
845
|
world: this,
|
|
584
846
|
componentPath: functionPath.componentPath,
|
|
@@ -589,29 +851,33 @@ var World = class {
|
|
|
589
851
|
request: from.request,
|
|
590
852
|
startTime: startTime ?? (from.parent !== void 0 && from.parent.udfType !== "action" ? from.parent.startTime : this.now()),
|
|
591
853
|
pagination,
|
|
592
|
-
turn: new Turn()
|
|
854
|
+
turn: new Turn(),
|
|
855
|
+
limits
|
|
593
856
|
};
|
|
594
857
|
}
|
|
595
858
|
runQuery(functionPath, resolved, args, from, snapshotOnly = false, top) {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
859
|
+
return this.track("functions", () => {
|
|
860
|
+
const parentAttempt = from.parent?.attempt;
|
|
861
|
+
const isNested = parentAttempt !== void 0;
|
|
862
|
+
const attempt = top?.attempt ?? (parentAttempt === void 0 ? new KernelAttempt(this.state) : snapshotOnly ? parentAttempt.staleSnapshot() : parentAttempt);
|
|
863
|
+
const execution = this.execution(functionPath, "query", attempt, from, top?.pagination ?? (isNested && from.parent?.pagination || {
|
|
864
|
+
calls: 0,
|
|
865
|
+
journal: null,
|
|
866
|
+
end: null
|
|
867
|
+
}), top?.startTime);
|
|
868
|
+
const q = queryGeneric({ handler: (ctx, a) => resolved.handler({
|
|
869
|
+
...ctx,
|
|
870
|
+
auth: queryAuth(execution)
|
|
871
|
+
}, a) });
|
|
872
|
+
return enter(execution, () => q.invokeQuery(JSON.stringify(convexToJson([parseArgs(args)])))).then((raw) => settled(execution, raw)).then((raw) => {
|
|
873
|
+
const result = jsonToConvex(JSON.parse(raw));
|
|
874
|
+
if (!isNested && hasPendingValues(result)) throw new Error(`Function ${functionPath.udfPath} return value invalid: queries cannot return an unresolved placeholder value.`);
|
|
875
|
+
this.keepReturns(functionPath, result);
|
|
876
|
+
validateReturnValue(resolved.returns, result, "query", functionPath);
|
|
877
|
+
return result;
|
|
878
|
+
}).catch((error) => {
|
|
879
|
+
throw deserializeConvexErrorData(error);
|
|
880
|
+
});
|
|
615
881
|
});
|
|
616
882
|
}
|
|
617
883
|
invokeMutation(execution, resolved, args) {
|
|
@@ -621,28 +887,32 @@ var World = class {
|
|
|
621
887
|
}, a) });
|
|
622
888
|
return enter(execution, () => m.invokeMutation(JSON.stringify(convexToJson([parseArgs(args)])))).then((raw) => settled(execution, raw)).then((raw) => {
|
|
623
889
|
const result = jsonToConvex(JSON.parse(raw));
|
|
624
|
-
|
|
890
|
+
const functionPath = {
|
|
625
891
|
componentPath: execution.componentPath,
|
|
626
892
|
udfPath: execution.udfPath
|
|
627
|
-
}
|
|
893
|
+
};
|
|
894
|
+
this.keepReturns(functionPath, result);
|
|
895
|
+
validateReturnValue(resolved.returns, result, "mutation", functionPath);
|
|
628
896
|
return result;
|
|
629
897
|
});
|
|
630
898
|
}
|
|
631
899
|
runMutation(functionPath, resolved, args, from) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
frame.
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
900
|
+
return this.track("functions", () => {
|
|
901
|
+
const parentAttempt = from.parent?.attempt;
|
|
902
|
+
if (parentAttempt !== void 0) {
|
|
903
|
+
const frame = parentAttempt.child();
|
|
904
|
+
const execution = this.execution(functionPath, "mutation", frame, from, void 0);
|
|
905
|
+
return this.invokeMutation(execution, resolved, args).then((result) => frame.drained().then(() => {
|
|
906
|
+
frame.close();
|
|
907
|
+
frame.merge();
|
|
908
|
+
return result;
|
|
909
|
+
}), (error) => {
|
|
910
|
+
frame.close();
|
|
911
|
+
throw deserializeConvexErrorData(error);
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
return this.prepareMutation(functionPath, resolved, args, from, 1, current$1()).then((held) => this.publish(held));
|
|
915
|
+
});
|
|
646
916
|
}
|
|
647
917
|
/**
|
|
648
918
|
* Run one attempt of a top-level mutation against the latest commit, then
|
|
@@ -652,34 +922,37 @@ var World = class {
|
|
|
652
922
|
* number.
|
|
653
923
|
*/
|
|
654
924
|
prepareMutation(functionPath, resolved, args, from, number, bound) {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
925
|
+
return this.track("functions", () => {
|
|
926
|
+
const attempt = new KernelAttempt(this.state, number);
|
|
927
|
+
const execution = this.execution(functionPath, "mutation", attempt, from, void 0);
|
|
928
|
+
const run = () => this.invokeMutation(execution, resolved, args);
|
|
929
|
+
return (bound === void 0 ? run() : bind({
|
|
930
|
+
...bound,
|
|
931
|
+
attempt: number
|
|
932
|
+
}, run)).then((value) => attempt.drained().then(() => ({
|
|
933
|
+
_tag: "returned",
|
|
934
|
+
value
|
|
935
|
+
})), (error) => ({
|
|
936
|
+
_tag: "failed",
|
|
937
|
+
error: deserializeConvexErrorData(error)
|
|
938
|
+
})).then((outcome) => {
|
|
939
|
+
attempt.close();
|
|
940
|
+
const held = {
|
|
941
|
+
view: {
|
|
942
|
+
id: `attempt-${this.nextAttemptId++}`,
|
|
943
|
+
functionPath,
|
|
944
|
+
request: from.request.requestId,
|
|
945
|
+
number,
|
|
946
|
+
snapshot: attempt.snapshot.version,
|
|
947
|
+
reads: attempt.reads,
|
|
948
|
+
outcome
|
|
949
|
+
},
|
|
950
|
+
attempt,
|
|
951
|
+
again: (next) => this.prepareMutation(functionPath, resolved, args, from, next, bound)
|
|
952
|
+
};
|
|
953
|
+
this.holding.set(held.view.id, held);
|
|
954
|
+
return held;
|
|
955
|
+
});
|
|
683
956
|
});
|
|
684
957
|
}
|
|
685
958
|
/**
|
|
@@ -744,43 +1017,223 @@ var World = class {
|
|
|
744
1017
|
});
|
|
745
1018
|
}
|
|
746
1019
|
runAction(functionPath, resolved, args, from) {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1020
|
+
return this.track("functions", () => {
|
|
1021
|
+
const execution = this.execution(functionPath, "action", void 0, from, void 0);
|
|
1022
|
+
const inner = {
|
|
1023
|
+
request: from.request,
|
|
1024
|
+
parent: execution
|
|
1025
|
+
};
|
|
1026
|
+
const a = actionGeneric({ handler: (ctx, innerArgs) => resolved.handler({
|
|
1027
|
+
...ctx,
|
|
1028
|
+
runQuery: (reference, queryArgs) => this.queryAt(pathOfReference(reference, functionPath.componentPath), queryArgs, inner),
|
|
1029
|
+
runMutation: (reference, mutationArgs) => this.mutationAt(pathOfReference(reference, functionPath.componentPath), mutationArgs, inner),
|
|
1030
|
+
runAction: (reference, actionArgs) => this.actionAt(pathOfReference(reference, functionPath.componentPath), actionArgs, inner),
|
|
1031
|
+
auth: queryAuth(execution)
|
|
1032
|
+
}, innerArgs) });
|
|
1033
|
+
const run = () => enter(execution, () => a.invokeAction(from.request.requestId, JSON.stringify(convexToJson([parseArgs(args)]))));
|
|
1034
|
+
const bound = from.parent === void 0 ? void 0 : current$1();
|
|
1035
|
+
return (bound === void 0 ? run() : bind(fresh(bound.world, functionPath.componentPath, from.request), run)).then((raw) => {
|
|
1036
|
+
const result = jsonToConvex(JSON.parse(raw));
|
|
1037
|
+
this.keepReturns(functionPath, result);
|
|
1038
|
+
validateReturnValue(resolved.returns, result, "action", functionPath);
|
|
1039
|
+
return result;
|
|
1040
|
+
}).catch((error) => {
|
|
1041
|
+
throw deserializeConvexErrorData(error);
|
|
1042
|
+
});
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
keepArgs(functionPath, args) {
|
|
1046
|
+
keep(args ?? {}, placed(this.redaction.args(functionPath)), this.secrets);
|
|
1047
|
+
}
|
|
1048
|
+
keepReturns(functionPath, result) {
|
|
1049
|
+
keep(result, placed(this.redaction.returns(functionPath)), this.secrets);
|
|
1050
|
+
}
|
|
1051
|
+
keepRow(componentPath, table, value) {
|
|
1052
|
+
keep(value, placed(this.redaction.table(componentPath, table)), this.secrets);
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Where a stored document holds secrets: its table's shape at its root, and
|
|
1056
|
+
* for a job's row, its target's argument shape at `args.0`.
|
|
1057
|
+
*/
|
|
1058
|
+
documentPlacements(componentPath, table$1, doc) {
|
|
1059
|
+
const own = placed(this.redaction.table(componentPath, table$1));
|
|
1060
|
+
if (table$1 !== "_scheduled_functions") return own;
|
|
1061
|
+
const target = this.targets.get(doc._id)?.functionPath ?? {
|
|
1062
|
+
componentPath,
|
|
1063
|
+
udfPath: String(doc.name)
|
|
751
1064
|
};
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1065
|
+
return own.concat(placed(this.redaction.args(target), ["args", "0"]));
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* A copy of the raw secrets this world has kept, for a restore of its
|
|
1069
|
+
* snapshots. It lives in memory only.
|
|
1070
|
+
*/
|
|
1071
|
+
keptSecrets() {
|
|
1072
|
+
return this.secrets.copy();
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* The failure report for `thrown`, whatever was thrown, with every kept
|
|
1076
|
+
* secret marked. `thrown` itself is never changed.
|
|
1077
|
+
*/
|
|
1078
|
+
redactFailure(thrown) {
|
|
1079
|
+
return failure(thrown, this.secrets);
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* `outcome` as an artifact writes it: a value marked by the shape of what
|
|
1083
|
+
* `functionPath` returns, a failure as its report.
|
|
1084
|
+
*/
|
|
1085
|
+
redactOutcome(outcome, functionPath) {
|
|
1086
|
+
return outcome._tag === "returned" ? {
|
|
1087
|
+
_tag: "returned",
|
|
1088
|
+
value: markValue(outcome.value, placed(functionPath === void 0 ? void 0 : this.redaction.returns(functionPath)), this.secrets)
|
|
1089
|
+
} : {
|
|
1090
|
+
_tag: "failed",
|
|
1091
|
+
error: this.redactFailure(outcome.error)
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* A changed document as an artifact writes it, marked by its table's shape.
|
|
1096
|
+
*/
|
|
1097
|
+
redactChange(change) {
|
|
1098
|
+
const document = (doc) => doc === null ? null : markValue(doc, this.documentPlacements(change.componentPath, tableNameFromId(doc._id) ?? "", doc), this.secrets);
|
|
1099
|
+
return {
|
|
1100
|
+
...change,
|
|
1101
|
+
before: document(change.before),
|
|
1102
|
+
after: document(change.after)
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* The function a subscription's or call's reference names, if it names one.
|
|
1107
|
+
*/
|
|
1108
|
+
functionPathOf(reference) {
|
|
1109
|
+
try {
|
|
1110
|
+
return typeof reference === "function" ? void 0 : pathOfReference(reference, ROOT);
|
|
1111
|
+
} catch {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Read records as an artifact writes them. A key or range bound on a field
|
|
1117
|
+
* its table's shape holds secret is the marker, and so is any string holding
|
|
1118
|
+
* a kept secret.
|
|
1119
|
+
*/
|
|
1120
|
+
projectReads(reads) {
|
|
1121
|
+
return reads.map((read) => {
|
|
1122
|
+
if (read.kind === "document") return read;
|
|
1123
|
+
const shape = this.redaction.table(read.componentPath, read.table);
|
|
1124
|
+
const sensitive = (fieldPath) => shape !== void 0 && sensitiveAt(shape, fieldPath.split("."));
|
|
1125
|
+
const value = (fieldPath, v) => sensitive(fieldPath) ? marker : markValue(v, [], this.secrets);
|
|
1126
|
+
const key = (k) => k === null ? null : k.map((v, i) => v === void 0 ? void 0 : value(read.fields[i] ?? "", v));
|
|
1127
|
+
return {
|
|
1128
|
+
...read,
|
|
1129
|
+
range: read.range.map((bound) => ({
|
|
1130
|
+
...bound,
|
|
1131
|
+
value: sensitive(bound.fieldPath) ? marker : convexToJson(markValue(jsonToConvex(bound.value), [], this.secrets))
|
|
1132
|
+
})),
|
|
1133
|
+
after: key(read.after),
|
|
1134
|
+
through: read.through === "end" || read.through === null ? read.through : key(read.through)
|
|
1135
|
+
};
|
|
767
1136
|
});
|
|
768
1137
|
}
|
|
1138
|
+
/**
|
|
1139
|
+
* A held attempt as an artifact writes it: its read records projected and its
|
|
1140
|
+
* outcome marked.
|
|
1141
|
+
*/
|
|
1142
|
+
projectHeld(view) {
|
|
1143
|
+
return {
|
|
1144
|
+
...view,
|
|
1145
|
+
reads: this.projectReads(view.reads),
|
|
1146
|
+
outcome: this.redactOutcome(view.outcome, view.functionPath)
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* What `apply` answered as an artifact writes it: an applied result marked by
|
|
1151
|
+
* the shape of what `functionPath` returns, a conflict's next attempt
|
|
1152
|
+
* projected, a refusal as its report.
|
|
1153
|
+
*/
|
|
1154
|
+
projectApplied(applied, functionPath) {
|
|
1155
|
+
switch (applied.kind) {
|
|
1156
|
+
case "applied": {
|
|
1157
|
+
const outcome = this.redactOutcome({
|
|
1158
|
+
_tag: "returned",
|
|
1159
|
+
value: applied.result
|
|
1160
|
+
}, functionPath);
|
|
1161
|
+
return outcome._tag === "returned" ? {
|
|
1162
|
+
...applied,
|
|
1163
|
+
result: outcome.value
|
|
1164
|
+
} : applied;
|
|
1165
|
+
}
|
|
1166
|
+
case "conflict": return {
|
|
1167
|
+
...applied,
|
|
1168
|
+
next: this.projectHeld(applied.next)
|
|
1169
|
+
};
|
|
1170
|
+
case "refused": return {
|
|
1171
|
+
...applied,
|
|
1172
|
+
error: this.redactFailure(applied.error)
|
|
1173
|
+
};
|
|
1174
|
+
case "exhausted": return {
|
|
1175
|
+
...applied,
|
|
1176
|
+
error: this.redactFailure(applied.error)
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* A function's arguments as an artifact writes them, marked by the shape of
|
|
1182
|
+
* its arguments. A command log of provider calls (#137's `commands()`) writes
|
|
1183
|
+
* each command's input through this.
|
|
1184
|
+
*/
|
|
1185
|
+
projectArguments(functionPath, args) {
|
|
1186
|
+
return markValue(args, placed(this.redaction.args(functionPath)), this.secrets);
|
|
1187
|
+
}
|
|
769
1188
|
schema(componentPath) {
|
|
770
1189
|
return this.component(componentPath).schema;
|
|
771
1190
|
}
|
|
772
1191
|
view(execution) {
|
|
773
1192
|
const attempt = transaction(execution);
|
|
1193
|
+
const { componentPath, limits } = execution;
|
|
774
1194
|
return {
|
|
775
|
-
schema: this.schema(
|
|
776
|
-
scan: (tableName) => attempt.scan(
|
|
777
|
-
read: (interval, after) =>
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1195
|
+
schema: this.schema(componentPath),
|
|
1196
|
+
scan: (tableName) => attempt.scan(componentPath, tableName),
|
|
1197
|
+
read: ({ index, ...interval }, after) => {
|
|
1198
|
+
const advance = attempt.readInterval({
|
|
1199
|
+
...interval,
|
|
1200
|
+
componentPath,
|
|
1201
|
+
after
|
|
1202
|
+
});
|
|
1203
|
+
return (through) => {
|
|
1204
|
+
advance(through);
|
|
1205
|
+
const examined$1 = examined(interval.range, interval.order, after, through);
|
|
1206
|
+
if (examined$1 !== null) refuse(attempt.meter.readInterval(indexKey(componentPath, interval.table, index), examined$1, limits));
|
|
1207
|
+
};
|
|
1208
|
+
},
|
|
1209
|
+
examine: (table, doc) => refuse(attempt.meter.readDocument(doc, isSystemTable(table), limits))
|
|
782
1210
|
};
|
|
783
1211
|
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Read one document by ID, as the binary's `get_inner` does for a get and for
|
|
1214
|
+
* the document a patch, replace, delete or cancel changes: it reads the ID's
|
|
1215
|
+
* interval of the table's `by_id` index, then charges the document if there
|
|
1216
|
+
* is one (`crates/database/src/transaction.rs:1188-1250`).
|
|
1217
|
+
*/
|
|
1218
|
+
readById(attempt, execution, id) {
|
|
1219
|
+
const { componentPath, limits } = execution;
|
|
1220
|
+
const table = tableNameFromId(id);
|
|
1221
|
+
attempt.readDocument(componentPath, id);
|
|
1222
|
+
refuse(attempt.meter.readInterval(indexKey(componentPath, table, "by_id"), point([id]), limits));
|
|
1223
|
+
const doc = attempt.get(componentPath, id);
|
|
1224
|
+
if (doc !== null) refuse(attempt.meter.readDocument(doc, isSystemTable(table), limits));
|
|
1225
|
+
return doc;
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Stage a write to a user table, counted against the execution's limits
|
|
1229
|
+
* first. A write over a limit is not staged (`Writes::update`,
|
|
1230
|
+
* `crates/database/src/writes.rs:241-310`).
|
|
1231
|
+
*/
|
|
1232
|
+
stage(execution, id, doc) {
|
|
1233
|
+
const attempt = transaction(execution);
|
|
1234
|
+
refuse(attempt.meter.write(doc, execution.limits));
|
|
1235
|
+
attempt.write(execution.componentPath, id, doc);
|
|
1236
|
+
}
|
|
784
1237
|
generateId(table) {
|
|
785
1238
|
return String(this.nextDocId++).padStart(TYPICAL_CONVEX_ID_LENGTH - table.length, "0") + table;
|
|
786
1239
|
}
|
|
@@ -869,6 +1322,7 @@ var World = class {
|
|
|
869
1322
|
return JSON.stringify({});
|
|
870
1323
|
case "1.0/count": {
|
|
871
1324
|
const attempt = transaction(execution);
|
|
1325
|
+
refuse(attempt.meter.readInterval(indexKey(execution.componentPath, args.table, "by_id"), everything, execution.limits));
|
|
872
1326
|
attempt.readInterval({
|
|
873
1327
|
kind: "interval",
|
|
874
1328
|
componentPath: execution.componentPath,
|
|
@@ -903,10 +1357,11 @@ var World = class {
|
|
|
903
1357
|
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)));
|
|
904
1358
|
}
|
|
905
1359
|
case "1.0/runUdf": {
|
|
906
|
-
const { udfType, args: udfArgs } = args;
|
|
1360
|
+
const { udfType, args: udfArgs, transactionLimits } = args;
|
|
907
1361
|
const from = {
|
|
908
1362
|
request: execution.request,
|
|
909
|
-
parent: execution
|
|
1363
|
+
parent: execution,
|
|
1364
|
+
budget: budgetOf(transactionLimits)
|
|
910
1365
|
};
|
|
911
1366
|
const functionPath = functionPathOf(args, execution.componentPath);
|
|
912
1367
|
const value = jsonToConvex(udfArgs);
|
|
@@ -921,15 +1376,17 @@ var World = class {
|
|
|
921
1376
|
case "1.0/schedule": return JSON.stringify(convexToJson(this.scheduleJob(transaction(execution), execution, args)));
|
|
922
1377
|
case "1.0/actions/schedule": return JSON.stringify(convexToJson(this.commitNow((attempt) => this.scheduleJob(attempt, execution, args))));
|
|
923
1378
|
case "1.0/cancel_job":
|
|
924
|
-
this.cancelJob(transaction(execution), execution
|
|
1379
|
+
this.cancelJob(transaction(execution), execution, args.id);
|
|
925
1380
|
return JSON.stringify({});
|
|
926
1381
|
case "1.0/actions/cancel_job":
|
|
927
|
-
this.commitNow((attempt) => this.cancelJob(attempt, execution
|
|
1382
|
+
this.commitNow((attempt) => this.cancelJob(attempt, execution, args.id));
|
|
928
1383
|
return JSON.stringify({});
|
|
929
1384
|
case "1.0/storageDelete":
|
|
930
1385
|
case "1.0/storageGetUrl":
|
|
931
1386
|
case "1.0/storageGenerateUploadUrl": throw rowPending("storage", 6);
|
|
932
|
-
case "1.0/getTransactionMetrics":
|
|
1387
|
+
case "1.0/getTransactionMetrics":
|
|
1388
|
+
if (execution.attempt === void 0) throw new Error(`Unknown async operation ${op}`);
|
|
1389
|
+
return JSON.stringify(execution.attempt.meter.metrics(execution.limits));
|
|
933
1390
|
case "1.0/actions/vectorSearch": throw requiresBinary("vectorSearch");
|
|
934
1391
|
default: throw new Error(`The test kernel does not support async syscall: "${op}"`);
|
|
935
1392
|
}
|
|
@@ -939,15 +1396,15 @@ var World = class {
|
|
|
939
1396
|
}
|
|
940
1397
|
get(execution, tableName, id) {
|
|
941
1398
|
validateId(tableName, id);
|
|
942
|
-
|
|
943
|
-
attempt.readDocument(execution.componentPath, id);
|
|
944
|
-
return attempt.get(execution.componentPath, id);
|
|
1399
|
+
return this.readById(transaction(execution), execution, id);
|
|
945
1400
|
}
|
|
946
1401
|
insert(execution, table, value) {
|
|
947
1402
|
const attempt = transaction(execution);
|
|
1403
|
+
this.keepRow(execution.componentPath, table, value);
|
|
948
1404
|
validateDocument(this.schema(execution.componentPath), table, value);
|
|
949
1405
|
const _id = this.generateId(table);
|
|
950
|
-
attempt.
|
|
1406
|
+
attempt.meter.deriveInterval(indexKey(execution.componentPath, table, "by_id"), point([_id]));
|
|
1407
|
+
this.stage(execution, _id, {
|
|
951
1408
|
...value,
|
|
952
1409
|
_id,
|
|
953
1410
|
_creationTime: this.creationTime(execution.startTime)
|
|
@@ -966,9 +1423,7 @@ var World = class {
|
|
|
966
1423
|
existing(execution, tableName, id, write, value) {
|
|
967
1424
|
validateId(tableName, id);
|
|
968
1425
|
if (value !== void 0 && typeof value !== "object") throw new Error(`Invalid argument \`value\` in \`db.${write}\`, expected object but got '${typeof value}': ${value}`);
|
|
969
|
-
const
|
|
970
|
-
attempt.readDocument(execution.componentPath, id);
|
|
971
|
-
const document = attempt.get(execution.componentPath, id);
|
|
1426
|
+
const document = this.readById(transaction(execution), execution, id);
|
|
972
1427
|
if (document === null) throw new Error(write === "delete" ? "Delete on non-existent doc" : `${write === "patch" ? "Patch" : "Replace"} on non-existent document with ID "${id}"`);
|
|
973
1428
|
return document;
|
|
974
1429
|
}
|
|
@@ -986,27 +1441,89 @@ var World = class {
|
|
|
986
1441
|
...fields,
|
|
987
1442
|
...this.fields(document, value)
|
|
988
1443
|
};
|
|
989
|
-
|
|
990
|
-
|
|
1444
|
+
this.keepRow(execution.componentPath, tableNameFromId(_id), merged);
|
|
1445
|
+
const next = {
|
|
991
1446
|
_id,
|
|
992
1447
|
_creationTime,
|
|
993
1448
|
...merged
|
|
994
|
-
}
|
|
1449
|
+
};
|
|
1450
|
+
if (sameDocument(document, next)) return;
|
|
1451
|
+
validateDocument(this.schema(execution.componentPath), tableNameFromId(_id), merged);
|
|
1452
|
+
this.stage(execution, _id, next);
|
|
995
1453
|
}
|
|
996
1454
|
replace(execution, tableName, id, value) {
|
|
997
1455
|
const document = this.existing(execution, tableName, id, "replace", value);
|
|
998
1456
|
const fields = this.fields(document, value);
|
|
999
|
-
|
|
1000
|
-
|
|
1457
|
+
this.keepRow(execution.componentPath, tableNameFromId(document._id), fields);
|
|
1458
|
+
const next = {
|
|
1001
1459
|
...fields,
|
|
1002
1460
|
_id: document._id,
|
|
1003
1461
|
_creationTime: document._creationTime
|
|
1004
|
-
}
|
|
1462
|
+
};
|
|
1463
|
+
if (sameDocument(document, next)) return;
|
|
1464
|
+
validateDocument(this.schema(execution.componentPath), tableNameFromId(document._id), fields);
|
|
1465
|
+
this.stage(execution, document._id, next);
|
|
1005
1466
|
}
|
|
1006
1467
|
remove(execution, tableName, id) {
|
|
1007
1468
|
const document = this.existing(execution, tableName, id, "delete", void 0);
|
|
1008
|
-
|
|
1469
|
+
this.stage(execution, document._id, null);
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
const refuse = (error) => {
|
|
1473
|
+
if (error !== void 0) throw error;
|
|
1474
|
+
};
|
|
1475
|
+
/**
|
|
1476
|
+
* One index of one table in one component, as the binary keys a read set.
|
|
1477
|
+
*/
|
|
1478
|
+
const indexKey = (componentPath, table, index) => `${componentPath}\u0000${table}\u0000${index}`;
|
|
1479
|
+
const isSystemTable = (table) => table.startsWith("_");
|
|
1480
|
+
const isFieldMap = (value) => typeof value === "object" && value !== null && isSimpleObject(value);
|
|
1481
|
+
/**
|
|
1482
|
+
* Whether two stored values are the same value, an unresolved commit timestamp
|
|
1483
|
+
* included. The binary compares pending values structurally, so a pending
|
|
1484
|
+
* timestamp equals only another pending timestamp, at any depth (`PendingValue`
|
|
1485
|
+
* derives `PartialEq`, `crates/value/src/pending.rs:56-75`, as
|
|
1486
|
+
* `PendingDocument` does, `crates/common/src/document.rs:624-632`). Concrete
|
|
1487
|
+
* values compare by `ConvexValue`'s total order
|
|
1488
|
+
* (`crates/value/src/sorting.rs:468-472`), which `compareValues` follows.
|
|
1489
|
+
* `compareValues` alone ranks a pending timestamp as the largest int64, so it
|
|
1490
|
+
* cannot tell the two apart. An object field set to `undefined` is absent.
|
|
1491
|
+
*/
|
|
1492
|
+
const sameValue = (a, b) => {
|
|
1493
|
+
const aPending = a instanceof CommitTsPlaceholder;
|
|
1494
|
+
const bPending = b instanceof CommitTsPlaceholder;
|
|
1495
|
+
if (aPending || bPending) return aPending && bPending;
|
|
1496
|
+
if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((value, i) => sameValue(value, b[i]));
|
|
1497
|
+
if (isFieldMap(a) || isFieldMap(b)) {
|
|
1498
|
+
if (!isFieldMap(a) || !isFieldMap(b)) return false;
|
|
1499
|
+
const fields = (value) => Object.entries(value).filter(([, field]) => field !== void 0);
|
|
1500
|
+
const aFields = fields(a);
|
|
1501
|
+
const bFields = new Map(fields(b));
|
|
1502
|
+
return aFields.length === bFields.size && aFields.every(([key, field]) => bFields.has(key) && sameValue(field, bFields.get(key)));
|
|
1503
|
+
}
|
|
1504
|
+
return compareValues(a, b) === 0;
|
|
1505
|
+
};
|
|
1506
|
+
/**
|
|
1507
|
+
* Whether a patch or replace leaves a document as it was. The binary then
|
|
1508
|
+
* writes nothing (`patch_inner` and `replace_inner`,
|
|
1509
|
+
* `crates/database/src/transaction.rs:713-790`).
|
|
1510
|
+
*/
|
|
1511
|
+
const sameDocument = (before, after) => sameValue(before, after);
|
|
1512
|
+
/**
|
|
1513
|
+
* A nested call's `transactionLimits`. The binary reads each field as an
|
|
1514
|
+
* unsigned integer and ignores fields it does not know (`TransactionLimits`,
|
|
1515
|
+
* `crates/database/src/execution_size.rs:62-95`).
|
|
1516
|
+
*/
|
|
1517
|
+
const budgetOf = (limits) => {
|
|
1518
|
+
if (limits === void 0 || limits === null) return void 0;
|
|
1519
|
+
const budget = {};
|
|
1520
|
+
for (const metric of Object.keys(defaults)) {
|
|
1521
|
+
const value = limits[metric];
|
|
1522
|
+
if (value === void 0) continue;
|
|
1523
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throw new Error(`Invalid arguments for \`runUdf\`: transactionLimits.${metric} must be a non-negative integer, got ${JSON.stringify(value)}`);
|
|
1524
|
+
budget[metric] = value;
|
|
1009
1525
|
}
|
|
1526
|
+
return budget;
|
|
1010
1527
|
};
|
|
1011
1528
|
const closed = (execution, operation) => new ClosedAttempt({
|
|
1012
1529
|
operation,
|