@gaganref/confect-test 10.3.1-fork.92 → 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/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 +138 -1
- package/dist/internal/Execution.d.ts.map +1 -1
- package/dist/internal/Execution.js +595 -182
- package/dist/internal/Execution.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/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/ConvexTest.ts +135 -38
- package/src/internal/Execution.ts +886 -287
- package/src/internal/KernelRefusal.ts +160 -0
- package/src/internal/Redaction.ts +730 -0
- package/src/internal/Snapshot.ts +945 -0
- package/src/internal/Subscriptions.ts +34 -4
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
jsonToConvex,
|
|
17
17
|
type Value,
|
|
18
18
|
} from "convex/values";
|
|
19
|
+
import * as Result from "effect/Result";
|
|
19
20
|
import { KernelAttempt } from "./Attempt.js";
|
|
20
21
|
import * as Clock from "./Clock.js";
|
|
21
22
|
import * as Globals from "./Globals.js";
|
|
@@ -25,7 +26,9 @@ import * as KernelQuery from "./KernelQuery.js";
|
|
|
25
26
|
import * as KernelRefusal from "./KernelRefusal.js";
|
|
26
27
|
import * as Metering from "./Metering.js";
|
|
27
28
|
import type * as ReadSet from "./ReadSet.js";
|
|
29
|
+
import * as Redaction from "./Redaction.js";
|
|
28
30
|
import * as Scheduling from "./Scheduling.js";
|
|
31
|
+
import * as Snapshot from "./Snapshot.js";
|
|
29
32
|
import type {
|
|
30
33
|
DocumentId,
|
|
31
34
|
FunctionPath,
|
|
@@ -119,6 +122,11 @@ export type Outcome =
|
|
|
119
122
|
*/
|
|
120
123
|
export interface HeldAttempt {
|
|
121
124
|
readonly id: string;
|
|
125
|
+
/**
|
|
126
|
+
* The mutation the attempt runs, whose return shape marks its outcome in an
|
|
127
|
+
* artifact.
|
|
128
|
+
*/
|
|
129
|
+
readonly functionPath: FunctionPath;
|
|
122
130
|
readonly request: string;
|
|
123
131
|
readonly number: Invocation.Attempt;
|
|
124
132
|
readonly snapshot: Kernel.Version;
|
|
@@ -298,6 +306,39 @@ const pinnedTime = (): number | undefined => {
|
|
|
298
306
|
*/
|
|
299
307
|
const defaultBudget = 100;
|
|
300
308
|
|
|
309
|
+
const unresolvedWorld = (
|
|
310
|
+
name: string,
|
|
311
|
+
reason: "missing" | "ambiguous",
|
|
312
|
+
): KernelRefusal.SnapshotWorldUnresolved =>
|
|
313
|
+
new KernelRefusal.SnapshotWorldUnresolved({
|
|
314
|
+
name,
|
|
315
|
+
reason,
|
|
316
|
+
message:
|
|
317
|
+
reason === "missing"
|
|
318
|
+
? `A snapshot job runs under the test world "${name}", which no world passed to restore is named. Pass that world.`
|
|
319
|
+
: `Two different test worlds are named "${name}", so a snapshot cannot tell their jobs apart. Name each world uniquely.`,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* `shape` placed at `at`, or no placement when there is no shape.
|
|
324
|
+
*/
|
|
325
|
+
const placed = (
|
|
326
|
+
shape: Redaction.Shape | undefined,
|
|
327
|
+
at: ReadonlyArray<string> = [],
|
|
328
|
+
): ReadonlyArray<Redaction.Placement> =>
|
|
329
|
+
shape === undefined ? [] : [{ at, shape }];
|
|
330
|
+
|
|
331
|
+
const running = (
|
|
332
|
+
registry: KernelRefusal.SnapshotWorldRunning["registry"],
|
|
333
|
+
count: number,
|
|
334
|
+
what: string,
|
|
335
|
+
) =>
|
|
336
|
+
new KernelRefusal.SnapshotWorldRunning({
|
|
337
|
+
registry,
|
|
338
|
+
count,
|
|
339
|
+
message: `The test backend is running ${count} ${what}, whose effects a snapshot would split. Await them first.`,
|
|
340
|
+
});
|
|
341
|
+
|
|
301
342
|
const current = (): Execution => {
|
|
302
343
|
const execution = executions.getStore();
|
|
303
344
|
if (execution === undefined) {
|
|
@@ -480,11 +521,34 @@ export class World {
|
|
|
480
521
|
*/
|
|
481
522
|
private floor = Number.NEGATIVE_INFINITY;
|
|
482
523
|
private readonly targets = new Map<DocumentId, Scheduling.Target>();
|
|
524
|
+
/**
|
|
525
|
+
* The raw secrets this world has seen under a secret schema node. Replay
|
|
526
|
+
* state, kept in memory and never written to an artifact.
|
|
527
|
+
*/
|
|
528
|
+
private secrets = new Redaction.Secrets();
|
|
529
|
+
/**
|
|
530
|
+
* The live registries `Snapshot.contract.live` names, each a count: calls
|
|
531
|
+
* from the test, scheduler walks, function executions (nested and detached
|
|
532
|
+
* ones included) and registered subscribers. Held attempts are `holding`.
|
|
533
|
+
*/
|
|
534
|
+
private readonly live = {
|
|
535
|
+
running: 0,
|
|
536
|
+
walks: 0,
|
|
537
|
+
functions: 0,
|
|
538
|
+
subscribers: 0,
|
|
539
|
+
};
|
|
540
|
+
/**
|
|
541
|
+
* The latest identity version a subscriber's `authenticate` reached. Each
|
|
542
|
+
* subscriber starts at the epoch, and each identity change advances it, so a
|
|
543
|
+
* restored world never hands out a version its source already used.
|
|
544
|
+
*/
|
|
545
|
+
private identityEpoch = 0;
|
|
483
546
|
|
|
484
547
|
constructor(
|
|
485
548
|
schema: SchemaDefinition<GenericSchema, boolean> | null | undefined,
|
|
486
549
|
modules: Modules,
|
|
487
550
|
private readonly runtime: Kernel.KernelRuntime = Kernel.wallClock(),
|
|
551
|
+
private readonly redaction: Redaction.Policy = Redaction.none,
|
|
488
552
|
) {
|
|
489
553
|
installConvexGlobal();
|
|
490
554
|
Clock.install(pinnedTime, () => executions.getStore() !== undefined);
|
|
@@ -600,44 +664,50 @@ export class World {
|
|
|
600
664
|
// Calls from the test.
|
|
601
665
|
|
|
602
666
|
query(caller: Caller, reference: any, args: any): Promise<any> {
|
|
603
|
-
return this.
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
667
|
+
return this.during(() => {
|
|
668
|
+
return this.open().then(() => {
|
|
669
|
+
const from = { caller, request: newRequest(caller) };
|
|
670
|
+
return typeof reference === "function"
|
|
671
|
+
? this.runQuery(
|
|
672
|
+
{ componentPath: ROOT, udfPath: "inline" },
|
|
673
|
+
{ handler: reference, args: { type: "any" }, returns: null },
|
|
674
|
+
args ?? {},
|
|
675
|
+
from,
|
|
676
|
+
)
|
|
677
|
+
: this.queryAt(pathOfReference(reference, ROOT), args, from);
|
|
678
|
+
});
|
|
613
679
|
});
|
|
614
680
|
}
|
|
615
681
|
|
|
616
682
|
mutation(caller: Caller, reference: any, args: any): Promise<any> {
|
|
617
|
-
return this.
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
683
|
+
return this.during(() => {
|
|
684
|
+
return this.open().then(() => {
|
|
685
|
+
const from = { caller, request: newRequest(caller) };
|
|
686
|
+
return typeof reference === "function"
|
|
687
|
+
? this.runMutation(
|
|
688
|
+
{ componentPath: ROOT, udfPath: "inline" },
|
|
689
|
+
{ handler: reference, args: { type: "any" }, returns: null },
|
|
690
|
+
args ?? {},
|
|
691
|
+
from,
|
|
692
|
+
)
|
|
693
|
+
: this.mutationAt(pathOfReference(reference, ROOT), args, from);
|
|
694
|
+
});
|
|
627
695
|
});
|
|
628
696
|
}
|
|
629
697
|
|
|
630
698
|
action(caller: Caller, reference: any, args: any): Promise<any> {
|
|
631
|
-
return this.
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
699
|
+
return this.during(() => {
|
|
700
|
+
return this.open().then(() => {
|
|
701
|
+
const from = { caller, request: newRequest(caller) };
|
|
702
|
+
return typeof reference === "function"
|
|
703
|
+
? this.runAction(
|
|
704
|
+
{ componentPath: ROOT, udfPath: "inline" },
|
|
705
|
+
{ handler: reference, args: { type: "any" }, returns: null },
|
|
706
|
+
args ?? {},
|
|
707
|
+
from,
|
|
708
|
+
)
|
|
709
|
+
: this.actionAt(pathOfReference(reference, ROOT), args, from);
|
|
710
|
+
});
|
|
641
711
|
});
|
|
642
712
|
}
|
|
643
713
|
|
|
@@ -646,14 +716,16 @@ export class World {
|
|
|
646
716
|
componentPath: string,
|
|
647
717
|
handler: (ctx: any) => any,
|
|
648
718
|
): Promise<any> {
|
|
649
|
-
return this.
|
|
650
|
-
this.
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
719
|
+
return this.during(() => {
|
|
720
|
+
return this.open().then(() => {
|
|
721
|
+
this.component(componentPath);
|
|
722
|
+
return this.runMutation(
|
|
723
|
+
{ componentPath, udfPath: "custom" },
|
|
724
|
+
{ handler, args: { type: "any" }, returns: null },
|
|
725
|
+
{},
|
|
726
|
+
{ caller, request: newRequest(caller) },
|
|
727
|
+
);
|
|
728
|
+
});
|
|
657
729
|
});
|
|
658
730
|
}
|
|
659
731
|
|
|
@@ -663,31 +735,36 @@ export class World {
|
|
|
663
735
|
* Run a mutation to its commit point and hold it as attempt 1.
|
|
664
736
|
*/
|
|
665
737
|
prepare(caller: Caller, reference: any, args: any): Promise<HeldAttempt> {
|
|
666
|
-
return this.
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
functionPath,
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
738
|
+
return this.during(() => {
|
|
739
|
+
return this.open().then(() => {
|
|
740
|
+
const functionPath = pathOfReference(reference, ROOT);
|
|
741
|
+
const resolved = this.resolve(functionPath, "mutation");
|
|
742
|
+
this.keepArgs(functionPath, args);
|
|
743
|
+
validateFunctionArgs(resolved.args, args ?? {}, functionPath, false);
|
|
744
|
+
return this.prepareMutation(
|
|
745
|
+
functionPath,
|
|
746
|
+
resolved,
|
|
747
|
+
args,
|
|
748
|
+
{ caller, request: newRequest(caller) },
|
|
749
|
+
1,
|
|
750
|
+
Invocation.current(),
|
|
751
|
+
).then((held) => held.view);
|
|
752
|
+
});
|
|
678
753
|
});
|
|
679
754
|
}
|
|
680
755
|
|
|
681
756
|
apply(id: string): Promise<KernelApply> {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
757
|
+
return this.during(() => {
|
|
758
|
+
const held = this.holding.get(id);
|
|
759
|
+
return held === undefined
|
|
760
|
+
? Promise.reject(
|
|
761
|
+
new KernelRefusal.StaleAttempt({
|
|
762
|
+
id,
|
|
763
|
+
message: `The test kernel holds no attempt "${id}". It was already applied, or never prepared.`,
|
|
764
|
+
}),
|
|
765
|
+
)
|
|
766
|
+
: this.applyHeld(held);
|
|
767
|
+
});
|
|
691
768
|
}
|
|
692
769
|
|
|
693
770
|
/**
|
|
@@ -711,37 +788,308 @@ export class World {
|
|
|
711
788
|
args: any,
|
|
712
789
|
pinned: { readonly time?: number; readonly journal: string | null },
|
|
713
790
|
): Promise<QueryRun> {
|
|
714
|
-
return this.
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
791
|
+
return this.during(() => {
|
|
792
|
+
return this.open().then(() => {
|
|
793
|
+
const functionPath = pathOfReference(reference, ROOT);
|
|
794
|
+
const resolved = this.resolve(functionPath, "query");
|
|
795
|
+
this.keepArgs(functionPath, args);
|
|
796
|
+
validateFunctionArgs(resolved.args, args ?? {}, functionPath, false);
|
|
797
|
+
const top: TopQuery = {
|
|
798
|
+
attempt: new KernelAttempt(this.state),
|
|
799
|
+
pagination: { calls: 0, journal: pinned.journal, end: null },
|
|
800
|
+
startTime: pinned.time,
|
|
801
|
+
};
|
|
802
|
+
const run = (outcome: Outcome): QueryRun => ({
|
|
803
|
+
outcome,
|
|
804
|
+
snapshot: top.attempt.snapshot.version,
|
|
805
|
+
reads: top.attempt.reads,
|
|
806
|
+
observedIdentity: top.attempt.observedIdentity,
|
|
807
|
+
time: top.attempt.observedTime,
|
|
808
|
+
journal: pinned.journal ?? top.pagination.end,
|
|
809
|
+
});
|
|
810
|
+
return this.runQuery(
|
|
811
|
+
functionPath,
|
|
812
|
+
resolved,
|
|
813
|
+
args,
|
|
814
|
+
{ caller, request: newRequest(caller) },
|
|
815
|
+
false,
|
|
816
|
+
top,
|
|
817
|
+
).then(
|
|
818
|
+
(value: Value) => run({ _tag: "returned", value }),
|
|
819
|
+
(error: unknown) => run({ _tag: "failed", error }),
|
|
820
|
+
);
|
|
730
821
|
});
|
|
731
|
-
return this.runQuery(
|
|
732
|
-
functionPath,
|
|
733
|
-
resolved,
|
|
734
|
-
args,
|
|
735
|
-
{ caller, request: newRequest(caller) },
|
|
736
|
-
false,
|
|
737
|
-
top,
|
|
738
|
-
).then(
|
|
739
|
-
(value: Value) => run({ _tag: "returned", value }),
|
|
740
|
-
(error: unknown) => run({ _tag: "failed", error }),
|
|
741
|
-
);
|
|
742
822
|
});
|
|
743
823
|
}
|
|
744
824
|
|
|
825
|
+
/**
|
|
826
|
+
* Run `call`, a call from the test, counted as running until it settles.
|
|
827
|
+
*/
|
|
828
|
+
private during<A>(call: () => Promise<A>): Promise<A> {
|
|
829
|
+
return this.track("running", call);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Run `work` counted in the live registry `registry` until it settles.
|
|
834
|
+
*/
|
|
835
|
+
private track<A>(
|
|
836
|
+
registry: "running" | "walks" | "functions",
|
|
837
|
+
work: () => Promise<A>,
|
|
838
|
+
): Promise<A> {
|
|
839
|
+
this.live[registry]++;
|
|
840
|
+
const settle = () => {
|
|
841
|
+
this.live[registry]--;
|
|
842
|
+
};
|
|
843
|
+
try {
|
|
844
|
+
return work().finally(settle);
|
|
845
|
+
} catch (error) {
|
|
846
|
+
settle();
|
|
847
|
+
throw error;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* How many entries each live registry holds now.
|
|
853
|
+
*/
|
|
854
|
+
liveCounts(): {
|
|
855
|
+
readonly [K in keyof typeof Snapshot.contract.live]: number;
|
|
856
|
+
} {
|
|
857
|
+
return {
|
|
858
|
+
running: this.live.running,
|
|
859
|
+
walks: this.live.walks,
|
|
860
|
+
functions: this.live.functions,
|
|
861
|
+
held: this.holding.size,
|
|
862
|
+
subscribers: this.live.subscribers,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Count a subscriber the shim's registry created. Subscribers live as long as
|
|
868
|
+
* their test, so the count never falls.
|
|
869
|
+
*/
|
|
870
|
+
registerSubscriber() {
|
|
871
|
+
this.live.subscribers++;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Identity versions.
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* The identity version a new subscriber starts at.
|
|
878
|
+
*/
|
|
879
|
+
currentIdentityEpoch(): number {
|
|
880
|
+
return this.identityEpoch;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* The next identity version, for a subscriber whose identity changed.
|
|
885
|
+
*/
|
|
886
|
+
nextIdentityEpoch(): number {
|
|
887
|
+
return ++this.identityEpoch;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Snapshots.
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* The committed state as snapshot bytes: every component's rows and the
|
|
894
|
+
* version that wrote each, the write log, the jobs and their targets, the
|
|
895
|
+
* clock's floor and the identity counters. Each job's world is recorded by
|
|
896
|
+
* name. Two exports of the same state are the same bytes. A world running a
|
|
897
|
+
* call or holding attempts refuses, since a snapshot carries neither.
|
|
898
|
+
*/
|
|
899
|
+
exportSnapshot(): Uint8Array {
|
|
900
|
+
this.refuseBusy(0, "export");
|
|
901
|
+
const names = new Map<string, Invocation.World>();
|
|
902
|
+
const jobs = new Map<DocumentId, Snapshot.JobTarget>();
|
|
903
|
+
for (const byTable of this.state.tables.values()) {
|
|
904
|
+
for (const id of byTable.get(Scheduling.table)?.keys() ?? []) {
|
|
905
|
+
const target = this.targets.get(id);
|
|
906
|
+
if (target === undefined) continue;
|
|
907
|
+
const world = target.world;
|
|
908
|
+
if (world !== undefined) {
|
|
909
|
+
if ((names.get(world.name) ?? world) !== world) {
|
|
910
|
+
throw unresolvedWorld(world.name, "ambiguous");
|
|
911
|
+
}
|
|
912
|
+
names.set(world.name, world);
|
|
913
|
+
}
|
|
914
|
+
jobs.set(id, {
|
|
915
|
+
functionPath: target.functionPath,
|
|
916
|
+
standing: target.standing,
|
|
917
|
+
world: world?.name ?? null,
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return Snapshot.encode(
|
|
922
|
+
{
|
|
923
|
+
components: [...this.components.keys()],
|
|
924
|
+
state: this.state,
|
|
925
|
+
floor: this.floor,
|
|
926
|
+
identity: {
|
|
927
|
+
nextDocumentId: this.nextDocId,
|
|
928
|
+
lastCreationTime: this.lastCreationTime,
|
|
929
|
+
nextAttemptId: this.nextAttemptId,
|
|
930
|
+
identityEpoch: this.identityEpoch,
|
|
931
|
+
},
|
|
932
|
+
jobs,
|
|
933
|
+
},
|
|
934
|
+
(componentPath, table, json) =>
|
|
935
|
+
Redaction.mark(
|
|
936
|
+
json,
|
|
937
|
+
this.documentPlacements(
|
|
938
|
+
componentPath,
|
|
939
|
+
table,
|
|
940
|
+
json as { readonly _id: unknown; readonly name?: unknown },
|
|
941
|
+
),
|
|
942
|
+
this.secrets,
|
|
943
|
+
),
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Replace this world's state with a snapshot's. The world must register the
|
|
949
|
+
* snapshot's components, run nothing, and have committed nothing and hold
|
|
950
|
+
* nothing. Each job's world resolves by name among `worlds`. Bytes whose
|
|
951
|
+
* digest does not match, and state the kernel could not have reached, refuse
|
|
952
|
+
* before anything changes.
|
|
953
|
+
*/
|
|
954
|
+
restoreSnapshot(
|
|
955
|
+
bytes: Uint8Array,
|
|
956
|
+
options: {
|
|
957
|
+
readonly worlds?: ReadonlyArray<Invocation.World>;
|
|
958
|
+
readonly secrets?: Redaction.Secrets;
|
|
959
|
+
} = {},
|
|
960
|
+
): Promise<void> {
|
|
961
|
+
return this.open().then(() =>
|
|
962
|
+
this.restore(
|
|
963
|
+
bytes,
|
|
964
|
+
options.worlds ?? [],
|
|
965
|
+
options.secrets ?? new Redaction.Secrets(),
|
|
966
|
+
),
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
private restore(
|
|
971
|
+
bytes: Uint8Array,
|
|
972
|
+
worlds: ReadonlyArray<Invocation.World>,
|
|
973
|
+
secrets: Redaction.Secrets,
|
|
974
|
+
) {
|
|
975
|
+
this.refuseBusy(this.state.version, "restore");
|
|
976
|
+
const decoded = Snapshot.decode(bytes, (json) =>
|
|
977
|
+
Redaction.unmark(json, secrets),
|
|
978
|
+
);
|
|
979
|
+
if (Result.isFailure(decoded)) throw decoded.failure;
|
|
980
|
+
const image = decoded.success;
|
|
981
|
+
const checked = Snapshot.check(image, Scheduling.table);
|
|
982
|
+
if (Result.isFailure(checked)) throw checked.failure;
|
|
983
|
+
const registered = [...this.components.keys()].sort();
|
|
984
|
+
const snapshot = [...image.components].sort();
|
|
985
|
+
if (registered.join("\u0000") !== snapshot.join("\u0000")) {
|
|
986
|
+
throw new KernelRefusal.SnapshotComponentMismatch({
|
|
987
|
+
snapshot,
|
|
988
|
+
registered,
|
|
989
|
+
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.`,
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
for (const [id, { functionPath }] of image.jobs) {
|
|
993
|
+
if (!this.exports(functionPath)) {
|
|
994
|
+
throw new KernelRefusal.SnapshotJobInconsistent({
|
|
995
|
+
id,
|
|
996
|
+
reason: "function",
|
|
997
|
+
message: `The snapshot's job ${id} targets ${functionPath.udfPath} in "${functionPath.componentPath}", which that component does not export.`,
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
const byName = new Map<string, Invocation.World>();
|
|
1002
|
+
for (const world of worlds) {
|
|
1003
|
+
if ((byName.get(world.name) ?? world) !== world) {
|
|
1004
|
+
throw unresolvedWorld(world.name, "ambiguous");
|
|
1005
|
+
}
|
|
1006
|
+
byName.set(world.name, world);
|
|
1007
|
+
}
|
|
1008
|
+
const targets = Array.from(
|
|
1009
|
+
image.jobs,
|
|
1010
|
+
([id, { functionPath, standing, world }]): [
|
|
1011
|
+
DocumentId,
|
|
1012
|
+
Scheduling.Target,
|
|
1013
|
+
] => {
|
|
1014
|
+
const resolved = world === null ? undefined : byName.get(world);
|
|
1015
|
+
if (world !== null && resolved === undefined) {
|
|
1016
|
+
throw unresolvedWorld(world, "missing");
|
|
1017
|
+
}
|
|
1018
|
+
return [id, { functionPath, standing, world: resolved }];
|
|
1019
|
+
},
|
|
1020
|
+
);
|
|
1021
|
+
this.state = image.state;
|
|
1022
|
+
this.floor = image.floor;
|
|
1023
|
+
this.nextDocId = image.identity.nextDocumentId;
|
|
1024
|
+
this.lastCreationTime = image.identity.lastCreationTime;
|
|
1025
|
+
this.nextAttemptId = image.identity.nextAttemptId;
|
|
1026
|
+
this.identityEpoch = image.identity.identityEpoch;
|
|
1027
|
+
this.targets.clear();
|
|
1028
|
+
for (const [id, target] of targets) this.targets.set(id, target);
|
|
1029
|
+
this.secrets = secrets.copy();
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Whether `functionPath`'s component exports a Convex function there.
|
|
1034
|
+
*/
|
|
1035
|
+
private exports({ componentPath, udfPath }: FunctionPath): boolean {
|
|
1036
|
+
const [modulePath, exportName = "default"] = udfPath.split(":");
|
|
1037
|
+
const func = this.components.get(componentPath)?.loaded.get(modulePath!)?.[
|
|
1038
|
+
exportName
|
|
1039
|
+
];
|
|
1040
|
+
return (
|
|
1041
|
+
func?.isQuery === true ||
|
|
1042
|
+
func?.isMutation === true ||
|
|
1043
|
+
func?.isAction === true
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Refuse a snapshot while any live registry `operation` waits on is
|
|
1049
|
+
* non-empty, or, restoring, once `version` shows the world committed
|
|
1050
|
+
* something of its own.
|
|
1051
|
+
*/
|
|
1052
|
+
private refuseBusy(version: Kernel.Version, operation: "export" | "restore") {
|
|
1053
|
+
const counts = this.liveCounts();
|
|
1054
|
+
const refusals: {
|
|
1055
|
+
readonly [K in keyof typeof counts]: (count: number) => Error;
|
|
1056
|
+
} = {
|
|
1057
|
+
running: (count) => running("running", count, "calls from the test"),
|
|
1058
|
+
walks: (count) => running("walks", count, "scheduler walks"),
|
|
1059
|
+
functions: (count) =>
|
|
1060
|
+
running(
|
|
1061
|
+
"functions",
|
|
1062
|
+
count,
|
|
1063
|
+
"functions, nested and detached ones included",
|
|
1064
|
+
),
|
|
1065
|
+
held: () => {
|
|
1066
|
+
const held = [...this.holding.keys()];
|
|
1067
|
+
return new KernelRefusal.SnapshotWorldBusy({
|
|
1068
|
+
version,
|
|
1069
|
+
held,
|
|
1070
|
+
message: `The test backend holds attempts ${held.join(", ")}, which a snapshot does not carry. Apply them first.`,
|
|
1071
|
+
});
|
|
1072
|
+
},
|
|
1073
|
+
subscribers: (count) =>
|
|
1074
|
+
new KernelRefusal.SnapshotSubscribed({
|
|
1075
|
+
subscribers: count,
|
|
1076
|
+
message: `The test backend has ${count} subscribers, whose identity versions a restore would rewind. Restore into a test backend no client has subscribed to.`,
|
|
1077
|
+
}),
|
|
1078
|
+
};
|
|
1079
|
+
for (const key of Object.keys(refusals) as Array<keyof typeof counts>) {
|
|
1080
|
+
const { blocks } = Snapshot.contract.live[key];
|
|
1081
|
+
if (operation === "export" && blocks === "restore") continue;
|
|
1082
|
+
if (counts[key] > 0) throw refusals[key](counts[key]);
|
|
1083
|
+
}
|
|
1084
|
+
if (operation === "restore" && version !== 0) {
|
|
1085
|
+
throw new KernelRefusal.SnapshotWorldBusy({
|
|
1086
|
+
version,
|
|
1087
|
+
held: [],
|
|
1088
|
+
message: `The test backend has committed ${version} versions of its own. Restore a snapshot into a fresh test backend.`,
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
745
1093
|
// Scheduled functions, on the kernel's clock.
|
|
746
1094
|
|
|
747
1095
|
/**
|
|
@@ -766,12 +1114,14 @@ export class World {
|
|
|
766
1114
|
* crosses no future deadline.
|
|
767
1115
|
*/
|
|
768
1116
|
advance(ms: number, budget = defaultBudget): Promise<void> {
|
|
769
|
-
return this.
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
1117
|
+
return this.track("walks", () => {
|
|
1118
|
+
return this.open().then(() => {
|
|
1119
|
+
const until = this.now() + ms;
|
|
1120
|
+
return this.walk(
|
|
1121
|
+
(pending) => pending.filter((job) => job.scheduledTime <= until),
|
|
1122
|
+
budget,
|
|
1123
|
+
).then(() => this.raiseFloor(until));
|
|
1124
|
+
});
|
|
775
1125
|
});
|
|
776
1126
|
}
|
|
777
1127
|
|
|
@@ -780,9 +1130,11 @@ export class World {
|
|
|
780
1130
|
* standing jobs are pending. Every job run counts against `budget`.
|
|
781
1131
|
*/
|
|
782
1132
|
drain(budget = defaultBudget): Promise<void> {
|
|
783
|
-
return this.
|
|
784
|
-
this.
|
|
785
|
-
|
|
1133
|
+
return this.track("walks", () => {
|
|
1134
|
+
return this.open().then(() =>
|
|
1135
|
+
this.walk((pending) => pending.filter((job) => !job.standing), budget),
|
|
1136
|
+
);
|
|
1137
|
+
});
|
|
786
1138
|
}
|
|
787
1139
|
|
|
788
1140
|
/**
|
|
@@ -923,6 +1275,7 @@ export class World {
|
|
|
923
1275
|
const target = functionPathOf(args, execution.componentPath);
|
|
924
1276
|
this.component(target.componentPath);
|
|
925
1277
|
const jobArgs = jsonToConvex(args.args);
|
|
1278
|
+
this.keepArgs(target, jobArgs);
|
|
926
1279
|
if (hasPendingValues(jobArgs)) {
|
|
927
1280
|
throw new Error(
|
|
928
1281
|
`Invalid arguments for "${target.udfPath}": a scheduled function cannot take an unresolved placeholder value.`,
|
|
@@ -1000,6 +1353,7 @@ export class World {
|
|
|
1000
1353
|
snapshotOnly = false,
|
|
1001
1354
|
) {
|
|
1002
1355
|
const resolved = this.resolve(functionPath, "query");
|
|
1356
|
+
this.keepArgs(functionPath, args);
|
|
1003
1357
|
validateFunctionArgs(
|
|
1004
1358
|
resolved.args,
|
|
1005
1359
|
args ?? {},
|
|
@@ -1011,6 +1365,7 @@ export class World {
|
|
|
1011
1365
|
|
|
1012
1366
|
private mutationAt(functionPath: FunctionPath, args: any, from: From) {
|
|
1013
1367
|
const resolved = this.resolve(functionPath, "mutation");
|
|
1368
|
+
this.keepArgs(functionPath, args);
|
|
1014
1369
|
validateFunctionArgs(
|
|
1015
1370
|
resolved.args,
|
|
1016
1371
|
args ?? {},
|
|
@@ -1022,6 +1377,7 @@ export class World {
|
|
|
1022
1377
|
|
|
1023
1378
|
private actionAt(functionPath: FunctionPath, args: any, from: From) {
|
|
1024
1379
|
const resolved = this.resolve(functionPath, "action");
|
|
1380
|
+
this.keepArgs(functionPath, args);
|
|
1025
1381
|
validateFunctionArgs(resolved.args, args ?? {}, functionPath, false);
|
|
1026
1382
|
return this.runAction(functionPath, resolved, args, from);
|
|
1027
1383
|
}
|
|
@@ -1084,50 +1440,53 @@ export class World {
|
|
|
1084
1440
|
snapshotOnly = false,
|
|
1085
1441
|
top?: TopQuery,
|
|
1086
1442
|
): Promise<any> {
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
`
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1443
|
+
return this.track("functions", () => {
|
|
1444
|
+
const parentAttempt = from.parent?.attempt;
|
|
1445
|
+
const isNested = parentAttempt !== undefined;
|
|
1446
|
+
const attempt =
|
|
1447
|
+
top?.attempt ??
|
|
1448
|
+
(parentAttempt === undefined
|
|
1449
|
+
? new KernelAttempt(this.state)
|
|
1450
|
+
: snapshotOnly
|
|
1451
|
+
? parentAttempt.staleSnapshot()
|
|
1452
|
+
: parentAttempt);
|
|
1453
|
+
const execution = this.execution(
|
|
1454
|
+
functionPath,
|
|
1455
|
+
"query",
|
|
1456
|
+
attempt,
|
|
1457
|
+
from,
|
|
1458
|
+
top?.pagination ??
|
|
1459
|
+
((isNested && from.parent?.pagination) || {
|
|
1460
|
+
calls: 0,
|
|
1461
|
+
journal: null,
|
|
1462
|
+
end: null,
|
|
1463
|
+
}),
|
|
1464
|
+
top?.startTime,
|
|
1465
|
+
);
|
|
1466
|
+
const q = queryGeneric({
|
|
1467
|
+
handler: (ctx: any, a: any) =>
|
|
1468
|
+
resolved.handler({ ...ctx, auth: queryAuth(execution) }, a),
|
|
1469
|
+
}) as unknown as { invokeQuery: (args: string) => Promise<string> };
|
|
1470
|
+
return enter(execution, () =>
|
|
1471
|
+
q.invokeQuery(JSON.stringify(convexToJson([parseArgs(args)]))),
|
|
1472
|
+
)
|
|
1473
|
+
.then((raw) => settled(execution, raw))
|
|
1474
|
+
.then((raw) => {
|
|
1475
|
+
const result = jsonToConvex(JSON.parse(raw));
|
|
1476
|
+
if (!isNested && hasPendingValues(result)) {
|
|
1477
|
+
throw new Error(
|
|
1478
|
+
`Function ${functionPath.udfPath} return value invalid: queries ` +
|
|
1479
|
+
`cannot return an unresolved placeholder value.`,
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
this.keepReturns(functionPath, result);
|
|
1483
|
+
validateReturnValue(resolved.returns, result, "query", functionPath);
|
|
1484
|
+
return result;
|
|
1485
|
+
})
|
|
1486
|
+
.catch((error: unknown) => {
|
|
1487
|
+
throw deserializeConvexErrorData(error);
|
|
1488
|
+
});
|
|
1489
|
+
});
|
|
1131
1490
|
}
|
|
1132
1491
|
|
|
1133
1492
|
private invokeMutation(
|
|
@@ -1145,10 +1504,12 @@ export class World {
|
|
|
1145
1504
|
.then((raw) => settled(execution, raw))
|
|
1146
1505
|
.then((raw) => {
|
|
1147
1506
|
const result = jsonToConvex(JSON.parse(raw));
|
|
1148
|
-
|
|
1507
|
+
const functionPath = {
|
|
1149
1508
|
componentPath: execution.componentPath,
|
|
1150
1509
|
udfPath: execution.udfPath,
|
|
1151
|
-
}
|
|
1510
|
+
};
|
|
1511
|
+
this.keepReturns(functionPath, result);
|
|
1512
|
+
validateReturnValue(resolved.returns, result, "mutation", functionPath);
|
|
1152
1513
|
return result;
|
|
1153
1514
|
});
|
|
1154
1515
|
}
|
|
@@ -1159,42 +1520,44 @@ export class World {
|
|
|
1159
1520
|
args: any,
|
|
1160
1521
|
from: From,
|
|
1161
1522
|
): Promise<any> {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1523
|
+
return this.track("functions", () => {
|
|
1524
|
+
const parentAttempt = from.parent?.attempt;
|
|
1525
|
+
if (parentAttempt !== undefined) {
|
|
1526
|
+
const frame = parentAttempt.child();
|
|
1527
|
+
const execution = this.execution(
|
|
1528
|
+
functionPath,
|
|
1529
|
+
"mutation",
|
|
1530
|
+
frame,
|
|
1531
|
+
from,
|
|
1532
|
+
undefined,
|
|
1533
|
+
);
|
|
1534
|
+
// As at the top level, a nested mutation ends once its function has
|
|
1535
|
+
// returned and every syscall it started has settled. Convex runs a
|
|
1536
|
+
// nested function's syscall queue to the end before it commits its
|
|
1537
|
+
// subtransaction, so a detached fiber's late write lands in this frame
|
|
1538
|
+
// and merges with it. A failed one ends at once and rolls back.
|
|
1539
|
+
return this.invokeMutation(execution, resolved, args).then(
|
|
1540
|
+
(result) =>
|
|
1541
|
+
frame.drained().then(() => {
|
|
1542
|
+
frame.close();
|
|
1543
|
+
frame.merge();
|
|
1544
|
+
return result;
|
|
1545
|
+
}),
|
|
1546
|
+
(error: unknown) => {
|
|
1547
|
+
frame.close();
|
|
1548
|
+
throw deserializeConvexErrorData(error);
|
|
1549
|
+
},
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
return this.prepareMutation(
|
|
1166
1553
|
functionPath,
|
|
1167
|
-
|
|
1168
|
-
|
|
1554
|
+
resolved,
|
|
1555
|
+
args,
|
|
1169
1556
|
from,
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
// nested function's syscall queue to the end before it commits its
|
|
1175
|
-
// subtransaction, so a detached fiber's late write lands in this frame
|
|
1176
|
-
// and merges with it. A failed one ends at once and rolls back.
|
|
1177
|
-
return this.invokeMutation(execution, resolved, args).then(
|
|
1178
|
-
(result) =>
|
|
1179
|
-
frame.drained().then(() => {
|
|
1180
|
-
frame.close();
|
|
1181
|
-
frame.merge();
|
|
1182
|
-
return result;
|
|
1183
|
-
}),
|
|
1184
|
-
(error: unknown) => {
|
|
1185
|
-
frame.close();
|
|
1186
|
-
throw deserializeConvexErrorData(error);
|
|
1187
|
-
},
|
|
1188
|
-
);
|
|
1189
|
-
}
|
|
1190
|
-
return this.prepareMutation(
|
|
1191
|
-
functionPath,
|
|
1192
|
-
resolved,
|
|
1193
|
-
args,
|
|
1194
|
-
from,
|
|
1195
|
-
1,
|
|
1196
|
-
Invocation.current(),
|
|
1197
|
-
).then((held) => this.publish(held));
|
|
1557
|
+
1,
|
|
1558
|
+
Invocation.current(),
|
|
1559
|
+
).then((held) => this.publish(held));
|
|
1560
|
+
});
|
|
1198
1561
|
}
|
|
1199
1562
|
|
|
1200
1563
|
/**
|
|
@@ -1212,53 +1575,58 @@ export class World {
|
|
|
1212
1575
|
number: Invocation.Attempt,
|
|
1213
1576
|
bound: Invocation.Invocation | undefined,
|
|
1214
1577
|
): Promise<Held> {
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
(
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
reads: attempt.reads,
|
|
1246
|
-
outcome,
|
|
1247
|
-
},
|
|
1248
|
-
attempt,
|
|
1249
|
-
again: (next) =>
|
|
1250
|
-
this.prepareMutation(
|
|
1578
|
+
return this.track("functions", () => {
|
|
1579
|
+
const attempt = new KernelAttempt(this.state, number);
|
|
1580
|
+
const execution = this.execution(
|
|
1581
|
+
functionPath,
|
|
1582
|
+
"mutation",
|
|
1583
|
+
attempt,
|
|
1584
|
+
from,
|
|
1585
|
+
undefined,
|
|
1586
|
+
);
|
|
1587
|
+
const run = () => this.invokeMutation(execution, resolved, args);
|
|
1588
|
+
const started =
|
|
1589
|
+
bound === undefined
|
|
1590
|
+
? run()
|
|
1591
|
+
: Invocation.bind({ ...bound, attempt: number }, run);
|
|
1592
|
+
return started
|
|
1593
|
+
.then(
|
|
1594
|
+
(value) =>
|
|
1595
|
+
attempt
|
|
1596
|
+
.drained()
|
|
1597
|
+
.then((): Outcome => ({ _tag: "returned", value })),
|
|
1598
|
+
(error: unknown): Outcome => ({
|
|
1599
|
+
_tag: "failed",
|
|
1600
|
+
error: deserializeConvexErrorData(error),
|
|
1601
|
+
}),
|
|
1602
|
+
)
|
|
1603
|
+
.then((outcome) => {
|
|
1604
|
+
attempt.close();
|
|
1605
|
+
const held: Held = {
|
|
1606
|
+
view: {
|
|
1607
|
+
id: `attempt-${this.nextAttemptId++}`,
|
|
1251
1608
|
functionPath,
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1609
|
+
request: from.request.requestId,
|
|
1610
|
+
number,
|
|
1611
|
+
snapshot: attempt.snapshot.version,
|
|
1612
|
+
reads: attempt.reads,
|
|
1613
|
+
outcome,
|
|
1614
|
+
},
|
|
1615
|
+
attempt,
|
|
1616
|
+
again: (next) =>
|
|
1617
|
+
this.prepareMutation(
|
|
1618
|
+
functionPath,
|
|
1619
|
+
resolved,
|
|
1620
|
+
args,
|
|
1621
|
+
from,
|
|
1622
|
+
next,
|
|
1623
|
+
bound,
|
|
1624
|
+
),
|
|
1625
|
+
};
|
|
1626
|
+
this.holding.set(held.view.id, held);
|
|
1627
|
+
return held;
|
|
1628
|
+
});
|
|
1629
|
+
});
|
|
1262
1630
|
}
|
|
1263
1631
|
|
|
1264
1632
|
/**
|
|
@@ -1341,75 +1709,299 @@ export class World {
|
|
|
1341
1709
|
args: any,
|
|
1342
1710
|
from: From,
|
|
1343
1711
|
): Promise<any> {
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1712
|
+
return this.track("functions", () => {
|
|
1713
|
+
const execution = this.execution(
|
|
1714
|
+
functionPath,
|
|
1715
|
+
"action",
|
|
1716
|
+
undefined,
|
|
1717
|
+
from,
|
|
1718
|
+
undefined,
|
|
1719
|
+
);
|
|
1720
|
+
const inner: From = { request: from.request, parent: execution };
|
|
1721
|
+
const a = actionGeneric({
|
|
1722
|
+
handler: (ctx: any, innerArgs: any) =>
|
|
1723
|
+
resolved.handler(
|
|
1724
|
+
{
|
|
1725
|
+
...ctx,
|
|
1726
|
+
runQuery: (reference: any, queryArgs: any) =>
|
|
1727
|
+
this.queryAt(
|
|
1728
|
+
pathOfReference(reference, functionPath.componentPath),
|
|
1729
|
+
queryArgs,
|
|
1730
|
+
inner,
|
|
1731
|
+
),
|
|
1732
|
+
runMutation: (reference: any, mutationArgs: any) =>
|
|
1733
|
+
this.mutationAt(
|
|
1734
|
+
pathOfReference(reference, functionPath.componentPath),
|
|
1735
|
+
mutationArgs,
|
|
1736
|
+
inner,
|
|
1737
|
+
),
|
|
1738
|
+
runAction: (reference: any, actionArgs: any) =>
|
|
1739
|
+
this.actionAt(
|
|
1740
|
+
pathOfReference(reference, functionPath.componentPath),
|
|
1741
|
+
actionArgs,
|
|
1742
|
+
inner,
|
|
1743
|
+
),
|
|
1744
|
+
auth: queryAuth(execution),
|
|
1745
|
+
},
|
|
1746
|
+
innerArgs,
|
|
1747
|
+
),
|
|
1748
|
+
}) as unknown as {
|
|
1749
|
+
invokeAction: (requestId: string, args: string) => Promise<string>;
|
|
1750
|
+
};
|
|
1751
|
+
const run = () =>
|
|
1752
|
+
enter(execution, () =>
|
|
1753
|
+
a.invokeAction(
|
|
1754
|
+
from.request.requestId,
|
|
1755
|
+
JSON.stringify(convexToJson([parseArgs(args)])),
|
|
1756
|
+
),
|
|
1757
|
+
);
|
|
1758
|
+
// An action another function calls is an invocation of its own, with its
|
|
1759
|
+
// world's services, a start time read from its world's clock and its
|
|
1760
|
+
// world's random stream, as a scheduled job is.
|
|
1761
|
+
const bound =
|
|
1762
|
+
from.parent === undefined ? undefined : Invocation.current();
|
|
1763
|
+
const started =
|
|
1764
|
+
bound === undefined
|
|
1765
|
+
? run()
|
|
1766
|
+
: Invocation.bind(
|
|
1767
|
+
Invocation.fresh(
|
|
1768
|
+
bound.world,
|
|
1769
|
+
functionPath.componentPath,
|
|
1770
|
+
from.request,
|
|
1374
1771
|
),
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
)
|
|
1379
|
-
|
|
1380
|
-
|
|
1772
|
+
run,
|
|
1773
|
+
);
|
|
1774
|
+
return started
|
|
1775
|
+
.then((raw) => {
|
|
1776
|
+
const result = jsonToConvex(JSON.parse(raw));
|
|
1777
|
+
this.keepReturns(functionPath, result);
|
|
1778
|
+
validateReturnValue(resolved.returns, result, "action", functionPath);
|
|
1779
|
+
return result;
|
|
1780
|
+
})
|
|
1781
|
+
.catch((error: unknown) => {
|
|
1782
|
+
throw deserializeConvexErrorData(error);
|
|
1783
|
+
});
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// Secrets.
|
|
1788
|
+
|
|
1789
|
+
private keepArgs(functionPath: FunctionPath, args: unknown) {
|
|
1790
|
+
Redaction.keep(
|
|
1791
|
+
args ?? {},
|
|
1792
|
+
placed(this.redaction.args(functionPath)),
|
|
1793
|
+
this.secrets,
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
private keepReturns(functionPath: FunctionPath, result: Value) {
|
|
1798
|
+
Redaction.keep(
|
|
1799
|
+
result,
|
|
1800
|
+
placed(this.redaction.returns(functionPath)),
|
|
1801
|
+
this.secrets,
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
private keepRow(componentPath: string, table: string, value: unknown) {
|
|
1806
|
+
Redaction.keep(
|
|
1807
|
+
value,
|
|
1808
|
+
placed(this.redaction.table(componentPath, table)),
|
|
1809
|
+
this.secrets,
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/**
|
|
1814
|
+
* Where a stored document holds secrets: its table's shape at its root, and
|
|
1815
|
+
* for a job's row, its target's argument shape at `args.0`.
|
|
1816
|
+
*/
|
|
1817
|
+
private documentPlacements(
|
|
1818
|
+
componentPath: string,
|
|
1819
|
+
table: string,
|
|
1820
|
+
doc: { readonly _id: unknown; readonly name?: unknown },
|
|
1821
|
+
): ReadonlyArray<Redaction.Placement> {
|
|
1822
|
+
const own = placed(this.redaction.table(componentPath, table));
|
|
1823
|
+
if (table !== Scheduling.table) return own;
|
|
1824
|
+
const target = this.targets.get(doc._id as DocumentId)?.functionPath ?? {
|
|
1825
|
+
componentPath,
|
|
1826
|
+
udfPath: String(doc.name),
|
|
1381
1827
|
};
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1828
|
+
return own.concat(placed(this.redaction.args(target), ["args", "0"]));
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
/**
|
|
1832
|
+
* A copy of the raw secrets this world has kept, for a restore of its
|
|
1833
|
+
* snapshots. It lives in memory only.
|
|
1834
|
+
*/
|
|
1835
|
+
keptSecrets(): Redaction.Secrets {
|
|
1836
|
+
return this.secrets.copy();
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
/**
|
|
1840
|
+
* The failure report for `thrown`, whatever was thrown, with every kept
|
|
1841
|
+
* secret marked. `thrown` itself is never changed.
|
|
1842
|
+
*/
|
|
1843
|
+
redactFailure(thrown: unknown): unknown {
|
|
1844
|
+
return Redaction.failure(thrown, this.secrets);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
/**
|
|
1848
|
+
* `outcome` as an artifact writes it: a value marked by the shape of what
|
|
1849
|
+
* `functionPath` returns, a failure as its report.
|
|
1850
|
+
*/
|
|
1851
|
+
redactOutcome(outcome: Outcome, functionPath?: FunctionPath): Outcome {
|
|
1852
|
+
return outcome._tag === "returned"
|
|
1853
|
+
? {
|
|
1854
|
+
_tag: "returned",
|
|
1855
|
+
value: Redaction.markValue(
|
|
1856
|
+
outcome.value,
|
|
1857
|
+
placed(
|
|
1858
|
+
functionPath === undefined
|
|
1859
|
+
? undefined
|
|
1860
|
+
: this.redaction.returns(functionPath),
|
|
1401
1861
|
),
|
|
1402
|
-
|
|
1403
|
-
)
|
|
1404
|
-
|
|
1405
|
-
.
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1862
|
+
this.secrets,
|
|
1863
|
+
),
|
|
1864
|
+
}
|
|
1865
|
+
: { _tag: "failed", error: this.redactFailure(outcome.error) };
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
/**
|
|
1869
|
+
* A changed document as an artifact writes it, marked by its table's shape.
|
|
1870
|
+
*/
|
|
1871
|
+
redactChange(change: Kernel.Change): Kernel.Change {
|
|
1872
|
+
const document = (doc: StoredDocument | null) =>
|
|
1873
|
+
doc === null
|
|
1874
|
+
? null
|
|
1875
|
+
: (Redaction.markValue(
|
|
1876
|
+
doc as Value,
|
|
1877
|
+
this.documentPlacements(
|
|
1878
|
+
change.componentPath,
|
|
1879
|
+
tableNameFromId(doc._id) ?? "",
|
|
1880
|
+
doc,
|
|
1881
|
+
),
|
|
1882
|
+
this.secrets,
|
|
1883
|
+
) as StoredDocument);
|
|
1884
|
+
return {
|
|
1885
|
+
...change,
|
|
1886
|
+
before: document(change.before),
|
|
1887
|
+
after: document(change.after),
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
/**
|
|
1892
|
+
* The function a subscription's or call's reference names, if it names one.
|
|
1893
|
+
*/
|
|
1894
|
+
functionPathOf(reference: unknown): FunctionPath | undefined {
|
|
1895
|
+
try {
|
|
1896
|
+
return typeof reference === "function"
|
|
1897
|
+
? undefined
|
|
1898
|
+
: pathOfReference(reference as FunctionReference<any>, ROOT);
|
|
1899
|
+
} catch {
|
|
1900
|
+
return undefined;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
/**
|
|
1905
|
+
* Read records as an artifact writes them. A key or range bound on a field
|
|
1906
|
+
* its table's shape holds secret is the marker, and so is any string holding
|
|
1907
|
+
* a kept secret.
|
|
1908
|
+
*/
|
|
1909
|
+
projectReads(
|
|
1910
|
+
reads: ReadonlyArray<ReadSet.Read>,
|
|
1911
|
+
): ReadonlyArray<ReadSet.Read> {
|
|
1912
|
+
return reads.map((read) => {
|
|
1913
|
+
if (read.kind === "document") return read;
|
|
1914
|
+
const shape = this.redaction.table(read.componentPath, read.table);
|
|
1915
|
+
const sensitive = (fieldPath: string) =>
|
|
1916
|
+
shape !== undefined &&
|
|
1917
|
+
Redaction.sensitiveAt(shape, fieldPath.split("."));
|
|
1918
|
+
const value = (fieldPath: string, v: Value): Value =>
|
|
1919
|
+
sensitive(fieldPath)
|
|
1920
|
+
? Redaction.marker
|
|
1921
|
+
: Redaction.markValue(v, [], this.secrets);
|
|
1922
|
+
const key = (k: ReadSet.Key | null) =>
|
|
1923
|
+
k === null
|
|
1924
|
+
? null
|
|
1925
|
+
: k.map((v, i) =>
|
|
1926
|
+
v === undefined ? undefined : value(read.fields[i] ?? "", v),
|
|
1927
|
+
);
|
|
1928
|
+
return {
|
|
1929
|
+
...read,
|
|
1930
|
+
range: read.range.map((bound) => ({
|
|
1931
|
+
...bound,
|
|
1932
|
+
value: sensitive(bound.fieldPath)
|
|
1933
|
+
? Redaction.marker
|
|
1934
|
+
: convexToJson(
|
|
1935
|
+
Redaction.markValue(
|
|
1936
|
+
jsonToConvex(bound.value),
|
|
1937
|
+
[],
|
|
1938
|
+
this.secrets,
|
|
1939
|
+
),
|
|
1940
|
+
),
|
|
1941
|
+
})),
|
|
1942
|
+
after: key(read.after),
|
|
1943
|
+
through:
|
|
1944
|
+
read.through === "end" || read.through === null
|
|
1945
|
+
? read.through
|
|
1946
|
+
: key(read.through),
|
|
1947
|
+
};
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
/**
|
|
1952
|
+
* A held attempt as an artifact writes it: its read records projected and its
|
|
1953
|
+
* outcome marked.
|
|
1954
|
+
*/
|
|
1955
|
+
projectHeld(view: HeldAttempt): HeldAttempt {
|
|
1956
|
+
return {
|
|
1957
|
+
...view,
|
|
1958
|
+
reads: this.projectReads(view.reads),
|
|
1959
|
+
outcome: this.redactOutcome(view.outcome, view.functionPath),
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
/**
|
|
1964
|
+
* What `apply` answered as an artifact writes it: an applied result marked by
|
|
1965
|
+
* the shape of what `functionPath` returns, a conflict's next attempt
|
|
1966
|
+
* projected, a refusal as its report.
|
|
1967
|
+
*/
|
|
1968
|
+
projectApplied(
|
|
1969
|
+
applied: KernelApply,
|
|
1970
|
+
functionPath: FunctionPath | undefined,
|
|
1971
|
+
): KernelApply {
|
|
1972
|
+
switch (applied.kind) {
|
|
1973
|
+
case "applied": {
|
|
1974
|
+
const outcome = this.redactOutcome(
|
|
1975
|
+
{ _tag: "returned", value: applied.result },
|
|
1976
|
+
functionPath,
|
|
1977
|
+
);
|
|
1978
|
+
return outcome._tag === "returned"
|
|
1979
|
+
? { ...applied, result: outcome.value }
|
|
1980
|
+
: applied;
|
|
1981
|
+
}
|
|
1982
|
+
case "conflict":
|
|
1983
|
+
return { ...applied, next: this.projectHeld(applied.next) };
|
|
1984
|
+
case "refused":
|
|
1985
|
+
return { ...applied, error: this.redactFailure(applied.error) };
|
|
1986
|
+
case "exhausted":
|
|
1987
|
+
return {
|
|
1988
|
+
...applied,
|
|
1989
|
+
error: this.redactFailure(applied.error) as typeof applied.error,
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
/**
|
|
1995
|
+
* A function's arguments as an artifact writes them, marked by the shape of
|
|
1996
|
+
* its arguments. A command log of provider calls (#137's `commands()`) writes
|
|
1997
|
+
* each command's input through this.
|
|
1998
|
+
*/
|
|
1999
|
+
projectArguments(functionPath: FunctionPath, args: Value): Value {
|
|
2000
|
+
return Redaction.markValue(
|
|
2001
|
+
args,
|
|
2002
|
+
placed(this.redaction.args(functionPath)),
|
|
2003
|
+
this.secrets,
|
|
2004
|
+
);
|
|
1413
2005
|
}
|
|
1414
2006
|
|
|
1415
2007
|
// Syscalls.
|
|
@@ -1767,6 +2359,7 @@ export class World {
|
|
|
1767
2359
|
|
|
1768
2360
|
private insert(execution: Execution, table: string, value: any): DocumentId {
|
|
1769
2361
|
const attempt = transaction(execution);
|
|
2362
|
+
this.keepRow(execution.componentPath, table, value);
|
|
1770
2363
|
KernelQuery.validateDocument(
|
|
1771
2364
|
this.schema(execution.componentPath),
|
|
1772
2365
|
table,
|
|
@@ -1855,6 +2448,7 @@ export class World {
|
|
|
1855
2448
|
const document = this.existing(execution, tableName, id, "patch", value);
|
|
1856
2449
|
const { _id, _creationTime, ...fields } = document;
|
|
1857
2450
|
const merged = { ...fields, ...this.fields(document, value) };
|
|
2451
|
+
this.keepRow(execution.componentPath, tableNameFromId(_id)!, merged);
|
|
1858
2452
|
const next = { _id, _creationTime, ...merged } as StoredDocument;
|
|
1859
2453
|
if (sameDocument(document, next)) return;
|
|
1860
2454
|
KernelQuery.validateDocument(
|
|
@@ -1873,6 +2467,11 @@ export class World {
|
|
|
1873
2467
|
) {
|
|
1874
2468
|
const document = this.existing(execution, tableName, id, "replace", value);
|
|
1875
2469
|
const fields = this.fields(document, value);
|
|
2470
|
+
this.keepRow(
|
|
2471
|
+
execution.componentPath,
|
|
2472
|
+
tableNameFromId(document._id)!,
|
|
2473
|
+
fields,
|
|
2474
|
+
);
|
|
1876
2475
|
const next = {
|
|
1877
2476
|
...fields,
|
|
1878
2477
|
_id: document._id,
|