@gaganref/confect-test 10.3.1-fork.9 → 10.3.1-fork.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/TestConfect.d.ts +37 -11
- package/dist/TestConfect.d.ts.map +1 -1
- package/dist/TestConfect.js +22 -7
- package/dist/TestConfect.js.map +1 -1
- package/dist/internal/Attempt.d.ts +77 -0
- package/dist/internal/Attempt.d.ts.map +1 -0
- package/dist/internal/Attempt.js +154 -0
- package/dist/internal/Attempt.js.map +1 -0
- package/dist/internal/Clock.d.ts +29 -0
- package/dist/internal/Clock.d.ts.map +1 -0
- package/dist/internal/Clock.js +66 -0
- package/dist/internal/Clock.js.map +1 -0
- package/dist/internal/ConvexTest.d.ts +66 -0
- package/dist/internal/ConvexTest.d.ts.map +1 -0
- package/dist/internal/ConvexTest.js +62 -0
- package/dist/internal/ConvexTest.js.map +1 -0
- package/dist/internal/Execution.d.ts +247 -0
- package/dist/internal/Execution.d.ts.map +1 -0
- package/dist/internal/Execution.js +1024 -0
- package/dist/internal/Execution.js.map +1 -0
- package/dist/internal/Globals.d.ts +11 -0
- package/dist/internal/Globals.d.ts.map +1 -0
- package/dist/internal/Globals.js +88 -0
- package/dist/internal/Globals.js.map +1 -0
- package/dist/internal/Invocation.d.ts +68 -0
- package/dist/internal/Invocation.d.ts.map +1 -0
- package/dist/internal/Invocation.js +158 -0
- package/dist/internal/Invocation.js.map +1 -0
- package/dist/internal/Kernel.d.ts +95 -0
- package/dist/internal/Kernel.d.ts.map +1 -0
- package/dist/internal/Kernel.js +91 -0
- package/dist/internal/Kernel.js.map +1 -0
- package/dist/internal/KernelQuery.d.ts +55 -0
- package/dist/internal/KernelQuery.d.ts.map +1 -0
- package/dist/internal/KernelQuery.js +205 -0
- package/dist/internal/KernelQuery.js.map +1 -0
- package/dist/internal/KernelRefusal.d.ts +102 -0
- package/dist/internal/KernelRefusal.d.ts.map +1 -0
- package/dist/internal/KernelRefusal.js +94 -0
- package/dist/internal/KernelRefusal.js.map +1 -0
- package/dist/internal/KernelValues.d.ts +138 -0
- package/dist/internal/KernelValues.d.ts.map +1 -0
- package/dist/internal/KernelValues.js +281 -0
- package/dist/internal/KernelValues.js.map +1 -0
- package/dist/internal/MountedHttp.d.ts +1 -1
- package/dist/internal/MountedHttp.d.ts.map +1 -1
- package/dist/internal/MountedHttp.js +3 -3
- package/dist/internal/MountedHttp.js.map +1 -1
- package/dist/internal/ReadSet.d.ts +55 -0
- package/dist/internal/ReadSet.d.ts.map +1 -0
- package/dist/internal/ReadSet.js +29 -0
- package/dist/internal/ReadSet.js.map +1 -0
- package/dist/internal/Scheduling.d.ts +71 -0
- package/dist/internal/Scheduling.d.ts.map +1 -0
- package/dist/internal/Scheduling.js +60 -0
- package/dist/internal/Scheduling.js.map +1 -0
- package/package.json +5 -5
- package/src/TestConfect.ts +84 -19
- package/src/internal/Attempt.ts +185 -0
- package/src/internal/Clock.ts +95 -0
- package/src/internal/ConvexTest.ts +163 -0
- package/src/internal/Execution.ts +1795 -0
- package/src/internal/Globals.ts +113 -0
- package/src/internal/Invocation.ts +316 -0
- package/src/internal/Kernel.ts +190 -0
- package/src/internal/KernelQuery.ts +405 -0
- package/src/internal/KernelRefusal.ts +112 -0
- package/src/internal/KernelValues.ts +765 -0
- package/src/internal/MountedHttp.ts +13 -6
- package/src/internal/ReadSet.ts +108 -0
- package/src/internal/Scheduling.ts +126 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// oxlint-disable-next-line effecttsgo/node-builtin-import, no-restricted-imports -- An attempt's drain waits one real turn of the event loop, which is no deadline. Fake timers in a test replace the global `setImmediate`, not this one.
|
|
2
|
+
import { setImmediate } from "node:timers/promises";
|
|
3
|
+
import type { Attempt as AttemptNumber } from "./Invocation.js";
|
|
4
|
+
import * as Kernel from "./Kernel.js";
|
|
5
|
+
import type { DocumentId, StoredDocument } from "./KernelValues.js";
|
|
6
|
+
import { tableNameFromId } from "./KernelValues.js";
|
|
7
|
+
import type * as ReadSet from "./ReadSet.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Pending writes, keyed by component path, then document ID. `null` deletes.
|
|
11
|
+
*/
|
|
12
|
+
type Overlay = Map<string, Map<DocumentId, StoredDocument | null>>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* How long one frame takes syscalls: those it has running, those it has
|
|
16
|
+
* started, and whether it still takes them.
|
|
17
|
+
*/
|
|
18
|
+
interface Life {
|
|
19
|
+
running: number;
|
|
20
|
+
started: number;
|
|
21
|
+
open: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* One frame of an attempt at a query or mutation: the committed state the
|
|
26
|
+
* attempt started from, its caller's frame and its own pending writes. The
|
|
27
|
+
* outermost frame belongs to the top-level function. Each nested mutation opens
|
|
28
|
+
* a child frame, which merges into its caller's frame when it returns and is
|
|
29
|
+
* dropped when it fails. Reads belong to the whole attempt and are never
|
|
30
|
+
* dropped: a failed nested mutation's reads stay in the attempt.
|
|
31
|
+
*
|
|
32
|
+
* Each frame has its own lifetime. A frame takes reads and writes until it is
|
|
33
|
+
* closed. A mutation's frame closes once its function has returned and no
|
|
34
|
+
* syscall it started is still running, so a write a detached fiber makes after
|
|
35
|
+
* the handler settled still lands in it, as on the binary. A nested mutation's
|
|
36
|
+
* frame closes before it merges into its caller's, so no write reaches it after
|
|
37
|
+
* its writes became its caller's.
|
|
38
|
+
*
|
|
39
|
+
* A frame belongs to one invocation, which alone writes to it, so a caller's
|
|
40
|
+
* write made while its nested mutation is in flight stays the caller's.
|
|
41
|
+
* `Kernel.apply` publishes the outermost frame's writes.
|
|
42
|
+
*/
|
|
43
|
+
export class KernelAttempt {
|
|
44
|
+
private readonly overlay: Overlay = new Map();
|
|
45
|
+
private readonly life: Life = { running: 0, started: 0, open: true };
|
|
46
|
+
private readonly allReads: Array<ReadSet.Read>;
|
|
47
|
+
|
|
48
|
+
constructor(
|
|
49
|
+
readonly snapshot: Kernel.State,
|
|
50
|
+
readonly number: AttemptNumber = 1,
|
|
51
|
+
private readonly parent?: KernelAttempt,
|
|
52
|
+
) {
|
|
53
|
+
this.allReads = parent?.allReads ?? [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get reads(): ReadonlyArray<ReadSet.Read> {
|
|
57
|
+
return this.allReads;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get closed(): boolean {
|
|
61
|
+
return !this.life.open;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
readDocument(componentPath: string, id: DocumentId) {
|
|
65
|
+
this.allReads.push({ kind: "document", componentPath, id });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Record a read of an interval. The returned function advances how far the
|
|
70
|
+
* query examined it.
|
|
71
|
+
*/
|
|
72
|
+
readInterval(
|
|
73
|
+
read: Omit<ReadSet.IntervalRead, "through">,
|
|
74
|
+
): (through: ReadSet.Key | "end") => void {
|
|
75
|
+
const interval: ReadSet.IntervalRead = { ...read, through: null };
|
|
76
|
+
this.allReads.push(interval);
|
|
77
|
+
return (through) => {
|
|
78
|
+
interval.through = through;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A syscall began. `end` runs when it settles.
|
|
84
|
+
*/
|
|
85
|
+
begin(): () => void {
|
|
86
|
+
const life = this.life;
|
|
87
|
+
life.running++;
|
|
88
|
+
life.started++;
|
|
89
|
+
return () => {
|
|
90
|
+
life.running--;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolves once a whole turn of the event loop passes with no syscall of this
|
|
96
|
+
* frame running or started. A nested call is a syscall of its caller's frame
|
|
97
|
+
* until it has drained.
|
|
98
|
+
*/
|
|
99
|
+
drained(): Promise<void> {
|
|
100
|
+
const before = this.life.started;
|
|
101
|
+
return setImmediate().then(() =>
|
|
102
|
+
this.life.running === 0 && this.life.started === before
|
|
103
|
+
? undefined
|
|
104
|
+
: this.drained(),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
close() {
|
|
109
|
+
this.life.open = false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The pending writes this frame sees, from the outermost frame to this one.
|
|
114
|
+
*/
|
|
115
|
+
private overlays(): Array<Overlay> {
|
|
116
|
+
return this.parent === undefined
|
|
117
|
+
? [this.overlay]
|
|
118
|
+
: [...this.parent.overlays(), this.overlay];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
get(componentPath: string, id: DocumentId): StoredDocument | null {
|
|
122
|
+
for (const overlay of this.overlays().toReversed()) {
|
|
123
|
+
const write = overlay.get(componentPath)?.get(id);
|
|
124
|
+
if (write !== undefined) return write;
|
|
125
|
+
}
|
|
126
|
+
return Kernel.get(this.snapshot, componentPath, id)?.doc ?? null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The table as this frame sees it, committed rows under every pending write
|
|
131
|
+
* from the outermost frame to this one.
|
|
132
|
+
*/
|
|
133
|
+
scan(componentPath: string, tableName: string): Array<StoredDocument> {
|
|
134
|
+
const ids = new Set<DocumentId>();
|
|
135
|
+
for (const row of Kernel.rows(this.snapshot, componentPath, tableName))
|
|
136
|
+
ids.add(row.doc._id);
|
|
137
|
+
for (const overlay of this.overlays())
|
|
138
|
+
for (const id of overlay.get(componentPath)?.keys() ?? [])
|
|
139
|
+
if (tableNameFromId(id) === tableName) ids.add(id);
|
|
140
|
+
const docs: Array<StoredDocument> = [];
|
|
141
|
+
for (const id of ids) {
|
|
142
|
+
const doc = this.get(componentPath, id);
|
|
143
|
+
if (doc !== null) docs.push(doc);
|
|
144
|
+
}
|
|
145
|
+
return docs;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
write(componentPath: string, id: DocumentId, doc: StoredDocument | null) {
|
|
149
|
+
const writes = this.overlay.get(componentPath) ?? new Map();
|
|
150
|
+
this.overlay.set(componentPath, writes);
|
|
151
|
+
writes.set(id, doc);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A nested mutation begins, in a frame of its own. Dropping a failed one is
|
|
156
|
+
* forgetting it.
|
|
157
|
+
*/
|
|
158
|
+
child(): KernelAttempt {
|
|
159
|
+
return new KernelAttempt(this.snapshot, this.number, this);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* A nested mutation returned and its frame closed. Its writes become its
|
|
164
|
+
* caller's.
|
|
165
|
+
*/
|
|
166
|
+
merge() {
|
|
167
|
+
if (this.parent === undefined) {
|
|
168
|
+
throw new Error("The outermost frame of an attempt has no caller.");
|
|
169
|
+
}
|
|
170
|
+
if (this.life.open) {
|
|
171
|
+
throw new Error("A nested mutation's frame merges only once closed.");
|
|
172
|
+
}
|
|
173
|
+
for (const [componentPath, writes] of this.overlay)
|
|
174
|
+
for (const [id, doc] of writes) this.parent.write(componentPath, id, doc);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The outermost frame's writes, for `Kernel.apply`.
|
|
179
|
+
*/
|
|
180
|
+
writes(): Array<Kernel.Write> {
|
|
181
|
+
return Array.from(this.overlay, ([componentPath, writes]) =>
|
|
182
|
+
Array.from(writes, ([id, doc]) => ({ componentPath, id, doc })),
|
|
183
|
+
).flat();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convex pins `Date` inside a query or mutation to the time it started, and its
|
|
3
|
+
* scheduler reads `Date.now()` for `runAfter`. The kernel wraps the realm's
|
|
4
|
+
* `Date` so it answers a kernel invocation's time inside one and the host's
|
|
5
|
+
* everywhere else. It sets no host timer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as Globals from "./Globals.js";
|
|
9
|
+
|
|
10
|
+
type DateConstructor = typeof globalThis.Date;
|
|
11
|
+
|
|
12
|
+
interface State {
|
|
13
|
+
/**
|
|
14
|
+
* Each wrapper the kernel installed, and the `Date` it wraps.
|
|
15
|
+
*/
|
|
16
|
+
readonly bases: WeakMap<DateConstructor, DateConstructor>;
|
|
17
|
+
readonly pinned: () => number | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Whether a kernel invocation is running, without reading its time.
|
|
20
|
+
*/
|
|
21
|
+
readonly inside: () => boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const key = Symbol.for("@confect/test/Clock");
|
|
25
|
+
|
|
26
|
+
type Global = { [key]?: State; Date: DateConstructor };
|
|
27
|
+
|
|
28
|
+
const wrap = (
|
|
29
|
+
base: DateConstructor,
|
|
30
|
+
pinned: () => number | undefined,
|
|
31
|
+
): DateConstructor => {
|
|
32
|
+
const proxy: DateConstructor = new Proxy(base, {
|
|
33
|
+
get: (target, property) =>
|
|
34
|
+
property === "now"
|
|
35
|
+
? () => pinned() ?? target.now()
|
|
36
|
+
: Reflect.get(target, property, target),
|
|
37
|
+
construct: (target, args, newTarget) => {
|
|
38
|
+
const at = args.length === 0 ? pinned() : undefined;
|
|
39
|
+
return Reflect.construct(
|
|
40
|
+
target,
|
|
41
|
+
at === undefined ? args : [at],
|
|
42
|
+
newTarget === proxy ? target : newTarget,
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
apply: (target, self, args) => {
|
|
46
|
+
const at = pinned();
|
|
47
|
+
return at === undefined
|
|
48
|
+
? Reflect.apply(target, self, args)
|
|
49
|
+
: new target(at).toString();
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
return proxy;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Wrap the realm's current `Date`, unless the kernel already did. `pinned`
|
|
57
|
+
* answers the running invocation's time, or `undefined` outside one, and
|
|
58
|
+
* `inside` says whether one is running. The first install's callbacks answer
|
|
59
|
+
* for every later one, as with `globalThis.Convex`. A test that fakes `Date`
|
|
60
|
+
* replaces the wrapper while the fake is installed, and the next install wraps
|
|
61
|
+
* the `Date` the test restores.
|
|
62
|
+
*/
|
|
63
|
+
export const install = (
|
|
64
|
+
pinned: () => number | undefined,
|
|
65
|
+
inside: () => boolean,
|
|
66
|
+
) => {
|
|
67
|
+
const global = globalThis as Global;
|
|
68
|
+
const state = (global[key] ??= { bases: new WeakMap(), pinned, inside });
|
|
69
|
+
const current = global.Date;
|
|
70
|
+
if (state.bases.has(current)) return;
|
|
71
|
+
const wrapper = wrap(current, () => state.pinned());
|
|
72
|
+
state.bases.set(wrapper, current);
|
|
73
|
+
global.Date = wrapper;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether `date` is the kernel's wrapper inside a running kernel invocation, so
|
|
78
|
+
* it already answers that invocation's time on the kernel's clock.
|
|
79
|
+
*/
|
|
80
|
+
export const answersInvocation = (date: DateConstructor): boolean => {
|
|
81
|
+
const state = (globalThis as Global)[key];
|
|
82
|
+
return state !== undefined && state.bases.has(date) && state.inside();
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The host's time in milliseconds, never a kernel invocation's: the real clock,
|
|
87
|
+
* or a test's fake one while it is installed. It reads the test environment's
|
|
88
|
+
* `Date`, never the copy a bound handler pinned.
|
|
89
|
+
*/
|
|
90
|
+
export const hostNow = (): number =>
|
|
91
|
+
Globals.host(() => {
|
|
92
|
+
const global = globalThis as Global;
|
|
93
|
+
const current = global.Date;
|
|
94
|
+
return (global[key]?.bases.get(current) ?? current).now();
|
|
95
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GenericSchema,
|
|
3
|
+
SchemaDefinition,
|
|
4
|
+
UserIdentity,
|
|
5
|
+
} from "convex/server";
|
|
6
|
+
import { componentsGeneric } from "convex/server";
|
|
7
|
+
import * as Execution from "./Execution.js";
|
|
8
|
+
import type * as Kernel from "./Kernel.js";
|
|
9
|
+
import * as KernelRefusal from "./KernelRefusal.js";
|
|
10
|
+
import type * as Scheduling from "./Scheduling.js";
|
|
11
|
+
|
|
12
|
+
type Modules = Record<string, () => Promise<any>>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What the kernel adds to convex-test's surface, typed. The shim itself returns
|
|
16
|
+
* `any`, as convex-test's callers expect.
|
|
17
|
+
*/
|
|
18
|
+
export interface KernelControls {
|
|
19
|
+
readonly query: (reference: any, args?: any) => Promise<any>;
|
|
20
|
+
readonly mutation: (reference: any, args?: any) => Promise<any>;
|
|
21
|
+
readonly action: (reference: any, args?: any) => Promise<any>;
|
|
22
|
+
readonly prepare: (
|
|
23
|
+
reference: any,
|
|
24
|
+
args?: any,
|
|
25
|
+
) => Promise<Execution.HeldAttempt>;
|
|
26
|
+
readonly apply: (id: string) => Promise<Execution.KernelApply>;
|
|
27
|
+
readonly held: () => ReadonlyArray<Execution.HeldAttempt>;
|
|
28
|
+
/**
|
|
29
|
+
* Move the kernel's clock forward, running each job due on the way at its
|
|
30
|
+
* deadline.
|
|
31
|
+
*/
|
|
32
|
+
readonly advance: (ms: number) => Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* The pending jobs, earliest deadline first.
|
|
35
|
+
*/
|
|
36
|
+
readonly jobs: () => ReadonlyArray<Scheduling.Job>;
|
|
37
|
+
/**
|
|
38
|
+
* Run the jobs due now. No future deadline is crossed.
|
|
39
|
+
*/
|
|
40
|
+
readonly finishInProgressScheduledFunctions: () => Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Run jobs at their deadlines until only standing jobs remain, or fail with
|
|
43
|
+
* `DrainBudgetExceeded` after `maxIterations` jobs. The kernel keeps its own
|
|
44
|
+
* clock, so it never calls `advanceTimers`.
|
|
45
|
+
*/
|
|
46
|
+
readonly finishAllScheduledFunctions: (
|
|
47
|
+
advanceTimers?: () => void,
|
|
48
|
+
maxIterations?: number,
|
|
49
|
+
) => Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* How a kit registers a component beyond its schema and modules.
|
|
54
|
+
*/
|
|
55
|
+
export interface RegisterOptions {
|
|
56
|
+
/**
|
|
57
|
+
* The component's functions, as `module:export`, that schedule their own next
|
|
58
|
+
* run, such as the crons component's `public:rescheduler`. A job that runs
|
|
59
|
+
* one, or that one schedules, is never drain work.
|
|
60
|
+
*/
|
|
61
|
+
readonly standing?: ReadonlyArray<string>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const isSchemaDefinition = (
|
|
65
|
+
value: unknown,
|
|
66
|
+
): value is SchemaDefinition<GenericSchema, boolean> =>
|
|
67
|
+
value !== null &&
|
|
68
|
+
typeof value === "object" &&
|
|
69
|
+
"tables" in value &&
|
|
70
|
+
typeof (value as { tables: unknown }).tables === "object";
|
|
71
|
+
|
|
72
|
+
const accessor = (world: Execution.World, caller: Execution.Caller): any => ({
|
|
73
|
+
query: (reference: any, args?: any) => world.query(caller, reference, args),
|
|
74
|
+
mutation: (reference: any, args?: any) =>
|
|
75
|
+
world.mutation(caller, reference, args),
|
|
76
|
+
action: (reference: any, args?: any) => world.action(caller, reference, args),
|
|
77
|
+
prepare: (reference: any, args?: any) =>
|
|
78
|
+
world.prepare(caller, reference, args),
|
|
79
|
+
apply: (id: string) => world.apply(id),
|
|
80
|
+
held: () => world.held(),
|
|
81
|
+
run: (handler: (ctx: any) => any) => world.run(caller, "", handler),
|
|
82
|
+
runInComponent: (componentPath: string, handler: (ctx: any) => any) =>
|
|
83
|
+
world.run(caller, componentPath, handler),
|
|
84
|
+
withIdentity: (identity: Partial<UserIdentity>) =>
|
|
85
|
+
accessor(world, Execution.withIdentity(caller, identity)),
|
|
86
|
+
withRequestMetadata: (metadata: {
|
|
87
|
+
readonly ip?: string | null;
|
|
88
|
+
readonly userAgent?: string | null;
|
|
89
|
+
}) =>
|
|
90
|
+
accessor(world, {
|
|
91
|
+
...caller,
|
|
92
|
+
ip: metadata.ip ?? null,
|
|
93
|
+
userAgent: metadata.userAgent ?? null,
|
|
94
|
+
}),
|
|
95
|
+
fetch: () => Promise.reject(KernelRefusal.rowPending("HTTP routes", 6)),
|
|
96
|
+
jwks: () => Promise.reject(KernelRefusal.rowPending("JWT verification", 6)),
|
|
97
|
+
advance: (ms: number) => world.advance(ms),
|
|
98
|
+
jobs: () => world.jobs(),
|
|
99
|
+
finishInProgressScheduledFunctions: () => world.advance(0),
|
|
100
|
+
finishAllScheduledFunctions: (
|
|
101
|
+
_advanceTimers?: () => void,
|
|
102
|
+
maxIterations?: number,
|
|
103
|
+
) => world.drain(maxIterations),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The test kernel under convex-test's `convexTest` signature. The fork's kernel
|
|
108
|
+
* lane aliases `convex-test` to this module, so every suite that calls
|
|
109
|
+
* `convexTest` directly, and `TestConfect` itself, runs on the kernel.
|
|
110
|
+
*
|
|
111
|
+
* Beyond convex-test, `prepare` runs a mutation to its commit point and holds
|
|
112
|
+
* it, and `apply` validates and publishes a held attempt. `mutation` applies
|
|
113
|
+
* each attempt it prepares at once and prepares the next on a conflict, up to
|
|
114
|
+
* attempt 5. `runtime` supplies the clock and commit timestamps. Scheduled
|
|
115
|
+
* functions run on the kernel's clock, never on a host timer.
|
|
116
|
+
*/
|
|
117
|
+
export function convexTest(
|
|
118
|
+
schemaOrOptions?:
|
|
119
|
+
| SchemaDefinition<GenericSchema, boolean>
|
|
120
|
+
| {
|
|
121
|
+
readonly schema?: SchemaDefinition<GenericSchema, boolean>;
|
|
122
|
+
readonly modules?: Modules;
|
|
123
|
+
readonly transactionLimits?: unknown;
|
|
124
|
+
readonly runtime?: Kernel.KernelRuntime;
|
|
125
|
+
},
|
|
126
|
+
modules?: Modules,
|
|
127
|
+
): any {
|
|
128
|
+
let schema: SchemaDefinition<GenericSchema, boolean> | undefined;
|
|
129
|
+
let runtime: Kernel.KernelRuntime | undefined;
|
|
130
|
+
if (isSchemaDefinition(schemaOrOptions)) {
|
|
131
|
+
schema = schemaOrOptions;
|
|
132
|
+
} else if (schemaOrOptions !== undefined) {
|
|
133
|
+
if (schemaOrOptions.transactionLimits !== undefined) {
|
|
134
|
+
throw KernelRefusal.rowPending("transaction limits", 6);
|
|
135
|
+
}
|
|
136
|
+
schema = schemaOrOptions.schema;
|
|
137
|
+
modules = schemaOrOptions.modules;
|
|
138
|
+
runtime = schemaOrOptions.runtime;
|
|
139
|
+
}
|
|
140
|
+
if (modules === undefined) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
"The test kernel needs the modules map: pass `import.meta.glob(...)` as the second argument.",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const world = new Execution.World(schema, modules, runtime);
|
|
146
|
+
return {
|
|
147
|
+
...accessor(world, Execution.anonymous),
|
|
148
|
+
registerComponent: (
|
|
149
|
+
componentPath: string,
|
|
150
|
+
componentSchema: SchemaDefinition<GenericSchema, boolean>,
|
|
151
|
+
componentModules: Modules,
|
|
152
|
+
options?: RegisterOptions,
|
|
153
|
+
) => {
|
|
154
|
+
world.register(
|
|
155
|
+
componentPath,
|
|
156
|
+
componentSchema,
|
|
157
|
+
componentModules,
|
|
158
|
+
options?.standing,
|
|
159
|
+
);
|
|
160
|
+
return (componentsGeneric() as any)[componentPath];
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|