@gaganref/confect-test 10.3.1-fork.89 → 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 +33 -7
- package/dist/TestConfect.d.ts.map +1 -1
- package/dist/TestConfect.js +11 -6
- package/dist/TestConfect.js.map +1 -1
- package/dist/internal/Attempt.js.map +1 -1
- 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 +33 -1
- package/dist/internal/ConvexTest.d.ts.map +1 -1
- package/dist/internal/ConvexTest.js +8 -5
- package/dist/internal/ConvexTest.js.map +1 -1
- package/dist/internal/Execution.d.ts +85 -5
- package/dist/internal/Execution.d.ts.map +1 -1
- package/dist/internal/Execution.js +285 -51
- package/dist/internal/Execution.js.map +1 -1
- package/dist/internal/Globals.d.ts.map +1 -1
- package/dist/internal/Globals.js +12 -0
- package/dist/internal/Globals.js.map +1 -1
- package/dist/internal/Invocation.d.ts +11 -0
- package/dist/internal/Invocation.d.ts.map +1 -1
- package/dist/internal/Invocation.js +34 -21
- package/dist/internal/Invocation.js.map +1 -1
- package/dist/internal/Kernel.d.ts +3 -1
- package/dist/internal/Kernel.d.ts.map +1 -1
- package/dist/internal/Kernel.js +2 -2
- package/dist/internal/Kernel.js.map +1 -1
- package/dist/internal/KernelRefusal.d.ts +25 -0
- package/dist/internal/KernelRefusal.d.ts.map +1 -1
- package/dist/internal/KernelRefusal.js +22 -1
- package/dist/internal/KernelRefusal.js.map +1 -1
- package/dist/internal/KernelValues.d.ts +6 -4
- package/dist/internal/KernelValues.d.ts.map +1 -1
- package/dist/internal/KernelValues.js +20 -5
- package/dist/internal/KernelValues.js.map +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/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 +3 -3
- package/src/TestConfect.ts +62 -15
- package/src/internal/Attempt.ts +1 -1
- package/src/internal/Clock.ts +95 -0
- package/src/internal/ConvexTest.ts +51 -6
- package/src/internal/Execution.ts +415 -50
- package/src/internal/Globals.ts +20 -0
- package/src/internal/Invocation.ts +38 -13
- package/src/internal/Kernel.ts +5 -5
- package/src/internal/KernelRefusal.ts +24 -0
- package/src/internal/KernelValues.ts +44 -4
- package/src/internal/MountedHttp.ts +13 -6
- package/src/internal/Scheduling.ts +126 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import type { FunctionMetadata, RequestMetadata } from "convex/server";
|
|
3
3
|
import type * as Context from "effect/Context";
|
|
4
|
+
import * as Clock from "./Clock.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Four re-executions after the first try. A conflict on attempt 5 exhausts.
|
|
@@ -78,6 +79,25 @@ interface Bridge {
|
|
|
78
79
|
|
|
79
80
|
const key = Symbol.for("@confect/test/Invocation");
|
|
80
81
|
|
|
82
|
+
/**
|
|
83
|
+
* A fresh invocation of `world` for a request of its own, as an action or a
|
|
84
|
+
* scheduled job starts: attempt 1, the world's services and random stream, and
|
|
85
|
+
* a start time read from the world's clock.
|
|
86
|
+
*/
|
|
87
|
+
export const fresh = (
|
|
88
|
+
world: World,
|
|
89
|
+
componentPath: string,
|
|
90
|
+
{ ip, userAgent, requestId, scheduledFunctionId }: Invocation["request"],
|
|
91
|
+
): Invocation => ({
|
|
92
|
+
world,
|
|
93
|
+
componentPath,
|
|
94
|
+
attempt: 1,
|
|
95
|
+
request: { ip, userAgent, requestId, scheduledFunctionId },
|
|
96
|
+
services: world.services,
|
|
97
|
+
startedAt: world.clock(),
|
|
98
|
+
random: world.random,
|
|
99
|
+
});
|
|
100
|
+
|
|
81
101
|
/**
|
|
82
102
|
* The invocation a function of `functionType` called from `running` joins. A
|
|
83
103
|
* query or mutation an action calls is a transaction of its own, so it starts
|
|
@@ -105,16 +125,7 @@ const resolve = (
|
|
|
105
125
|
request !== undefined &&
|
|
106
126
|
request.scheduledFunctionId !== bound.request.scheduledFunctionId
|
|
107
127
|
) {
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
world: bound.world,
|
|
111
|
-
componentPath,
|
|
112
|
-
attempt: 1,
|
|
113
|
-
request: { ip, userAgent, requestId, scheduledFunctionId },
|
|
114
|
-
services: bound.world.services,
|
|
115
|
-
startedAt: bound.world.clock(),
|
|
116
|
-
random: bound.world.random,
|
|
117
|
-
};
|
|
128
|
+
return fresh(bound.world, componentPath, request);
|
|
118
129
|
}
|
|
119
130
|
return componentPath === bound.componentPath
|
|
120
131
|
? bound
|
|
@@ -202,13 +213,18 @@ const pin = (
|
|
|
202
213
|
): void => {
|
|
203
214
|
const host = globalThis;
|
|
204
215
|
const previous = { Date: host.Date, Math: host.Math, crypto: host.crypto };
|
|
216
|
+
// On the kernel, `Date` already answers the running function's time on the
|
|
217
|
+
// kernel's clock, so `runAfter` schedules on it. The bridge leaves it there.
|
|
218
|
+
const kernelDate = Clock.answersInvocation(previous.Date);
|
|
205
219
|
const pinned = {
|
|
206
|
-
Date:
|
|
220
|
+
Date: kernelDate
|
|
221
|
+
? previous.Date
|
|
222
|
+
: pinnedDate(previous.Date, clockOf(invocation, functionType)),
|
|
207
223
|
Math: drawingMath(previous.Math, invocation.random),
|
|
208
224
|
crypto: drawingCrypto(previous.crypto, invocation.random),
|
|
209
225
|
};
|
|
210
226
|
try {
|
|
211
|
-
host.Date = pinned.Date;
|
|
227
|
+
if (!kernelDate) host.Date = pinned.Date;
|
|
212
228
|
host.Math = pinned.Math;
|
|
213
229
|
host.crypto = pinned.crypto;
|
|
214
230
|
} catch {
|
|
@@ -216,7 +232,7 @@ const pin = (
|
|
|
216
232
|
// refuses the handler.
|
|
217
233
|
}
|
|
218
234
|
const reached = outside(() => ({
|
|
219
|
-
Date: host.Date === pinned.Date,
|
|
235
|
+
Date: !kernelDate && host.Date === pinned.Date,
|
|
220
236
|
Math: host.Math === pinned.Math,
|
|
221
237
|
crypto: host.crypto === pinned.crypto,
|
|
222
238
|
}));
|
|
@@ -284,6 +300,15 @@ export const bind = <A>(
|
|
|
284
300
|
start,
|
|
285
301
|
);
|
|
286
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Run `start` with no invocation bound, so every registered handler it reaches
|
|
305
|
+
* keeps its production runtime, whatever the caller has bound.
|
|
306
|
+
*/
|
|
307
|
+
export const unbound = <A>(start: () => Promise<A>): Promise<A> => {
|
|
308
|
+
const bridge = installed[key];
|
|
309
|
+
return bridge === undefined ? start() : bridge.storage.exit(start);
|
|
310
|
+
};
|
|
311
|
+
|
|
287
312
|
/**
|
|
288
313
|
* The invocation the calling handler runs under, if one is bound.
|
|
289
314
|
*/
|
package/src/internal/Kernel.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as Clock from "./Clock.js";
|
|
2
2
|
import type { DocumentId, StoredDocument } from "./KernelValues.js";
|
|
3
3
|
import { resolveCommittedValues, tableNameFromId } from "./KernelValues.js";
|
|
4
4
|
import * as ReadSet from "./ReadSet.js";
|
|
@@ -158,7 +158,9 @@ export const conflict = (
|
|
|
158
158
|
*/
|
|
159
159
|
export interface KernelRuntime {
|
|
160
160
|
/**
|
|
161
|
-
* The time in milliseconds
|
|
161
|
+
* The time in milliseconds. The kernel's clock never reads earlier than the
|
|
162
|
+
* latest deadline `advance` reached, so a runtime that stands still still
|
|
163
|
+
* lets jobs run on time.
|
|
162
164
|
*/
|
|
163
165
|
readonly now: () => number;
|
|
164
166
|
/**
|
|
@@ -176,9 +178,7 @@ export interface KernelRuntime {
|
|
|
176
178
|
*/
|
|
177
179
|
export const wallClock = (): KernelRuntime => {
|
|
178
180
|
let latest = 0n;
|
|
179
|
-
|
|
180
|
-
// oxlint-disable-next-line effecttsgo/global-date -- The default runtime is the wall clock. A driver that replays supplies its own.
|
|
181
|
-
const now = () => Globals.host(() => Date.now());
|
|
181
|
+
const now = Clock.hostNow;
|
|
182
182
|
return {
|
|
183
183
|
now,
|
|
184
184
|
commitTs: () => {
|
|
@@ -74,6 +74,30 @@ export class CommitTsNotIncreasing extends Schema.TaggedError<CommitTsNotIncreas
|
|
|
74
74
|
},
|
|
75
75
|
) {}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Draining scheduled functions ran its budget of jobs and work still remained.
|
|
79
|
+
* A self-rescheduling function keeps the drain from ending unless its kit
|
|
80
|
+
* declares it standing.
|
|
81
|
+
*/
|
|
82
|
+
export class DrainBudgetExceeded extends Schema.TaggedError<DrainBudgetExceeded>()(
|
|
83
|
+
"DrainBudgetExceeded",
|
|
84
|
+
{
|
|
85
|
+
budget: Schema.Int,
|
|
86
|
+
pending: Schema.Array(
|
|
87
|
+
Schema.Struct({ componentPath: Schema.String, name: Schema.String }),
|
|
88
|
+
),
|
|
89
|
+
message: Schema.String,
|
|
90
|
+
},
|
|
91
|
+
) {}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A component declared a standing source its modules do not export.
|
|
95
|
+
*/
|
|
96
|
+
export class UnknownStandingSource extends Schema.TaggedError<UnknownStandingSource>()(
|
|
97
|
+
"UnknownStandingSource",
|
|
98
|
+
{ componentPath: Schema.String, name: Schema.String, message: Schema.String },
|
|
99
|
+
) {}
|
|
100
|
+
|
|
77
101
|
export const requiresBinary = (operation: RequiresBinary["operation"]) =>
|
|
78
102
|
new RequiresBinary({
|
|
79
103
|
operation,
|
|
@@ -419,6 +419,12 @@ export type ValidatorJSON =
|
|
|
419
419
|
| { type: "array"; value: ValidatorJSON }
|
|
420
420
|
| { type: "object"; value: Record<string, ObjectFieldType> }
|
|
421
421
|
| { type: "record"; keys: ValidatorJSON; values: ObjectFieldType }
|
|
422
|
+
| {
|
|
423
|
+
type: "function";
|
|
424
|
+
fnType?: string;
|
|
425
|
+
args?: ValidatorJSON;
|
|
426
|
+
returns?: ValidatorJSON;
|
|
427
|
+
}
|
|
422
428
|
| { type: "union"; value: ValidatorJSON[] };
|
|
423
429
|
|
|
424
430
|
export function validateValidator(validator: ValidatorJSON, value: any) {
|
|
@@ -568,6 +574,24 @@ export function validateValidator(validator: ValidatorJSON, value: any) {
|
|
|
568
574
|
}
|
|
569
575
|
return;
|
|
570
576
|
}
|
|
577
|
+
case "function": {
|
|
578
|
+
// The pinned binary, `precompiled-2026-10-28-cccbade`, accepts a string
|
|
579
|
+
// that parses as a function handle
|
|
580
|
+
// (`common/src/schemas/validator.rs:156-164`), and its parser requires a
|
|
581
|
+
// document id after the prefix
|
|
582
|
+
// (`common/src/bootstrap_model/components/handles.rs:100-112`). The
|
|
583
|
+
// kernel mints convex-test's handle format instead, so it accepts a
|
|
584
|
+
// string that parses as that format.
|
|
585
|
+
if (
|
|
586
|
+
typeof value !== "string" ||
|
|
587
|
+
readFunctionHandle(value) === undefined
|
|
588
|
+
) {
|
|
589
|
+
throw new Error(
|
|
590
|
+
`Validator error: Expected a function handle, got \`${value}\``,
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
571
595
|
case "record": {
|
|
572
596
|
if (
|
|
573
597
|
value === null ||
|
|
@@ -706,13 +730,29 @@ export function validateReturnValue(
|
|
|
706
730
|
}
|
|
707
731
|
}
|
|
708
732
|
|
|
733
|
+
const FUNCTION_HANDLE_PREFIX = "function://";
|
|
734
|
+
|
|
709
735
|
export function createFunctionHandle(functionPath: FunctionPath) {
|
|
710
|
-
return
|
|
736
|
+
return `${FUNCTION_HANDLE_PREFIX}${functionPath.componentPath};${functionPath.udfPath}`;
|
|
711
737
|
}
|
|
712
738
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
739
|
+
/**
|
|
740
|
+
* The function a handle the kernel minted names, or `undefined` for a string
|
|
741
|
+
* that is not `function://<componentPath>;<udfPath>` with a function path.
|
|
742
|
+
*/
|
|
743
|
+
function readFunctionHandle(handle: string): FunctionPath | undefined {
|
|
744
|
+
if (!handle.startsWith(FUNCTION_HANDLE_PREFIX)) return undefined;
|
|
745
|
+
const parts = handle.slice(FUNCTION_HANDLE_PREFIX.length).split(";");
|
|
746
|
+
if (parts.length !== 2 || parts[1] === "") return undefined;
|
|
747
|
+
return { componentPath: parts[0]!, udfPath: parts[1]! };
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
export function parseFunctionHandle(handle: string): FunctionPath {
|
|
751
|
+
const functionPath = readFunctionHandle(handle);
|
|
752
|
+
if (functionPath === undefined) {
|
|
753
|
+
throw new Error(`Invalid function handle \`${handle}\``);
|
|
754
|
+
}
|
|
755
|
+
return functionPath;
|
|
716
756
|
}
|
|
717
757
|
export function simpleHash(string: string) {
|
|
718
758
|
let hash = 0;
|
|
@@ -102,11 +102,16 @@ export const make = (
|
|
|
102
102
|
const registered = new Set<string>();
|
|
103
103
|
let tail = Promise.resolve();
|
|
104
104
|
|
|
105
|
-
const register: RegisterComponent = (
|
|
105
|
+
const register: RegisterComponent = (
|
|
106
|
+
componentPath,
|
|
107
|
+
schema,
|
|
108
|
+
modules,
|
|
109
|
+
options,
|
|
110
|
+
) => {
|
|
106
111
|
if (
|
|
107
112
|
!normalized.success.some((mount) => mount.componentPath === componentPath)
|
|
108
113
|
) {
|
|
109
|
-
registerComponent(componentPath, schema, modules);
|
|
114
|
+
registerComponent(componentPath, schema, modules, options);
|
|
110
115
|
return;
|
|
111
116
|
}
|
|
112
117
|
const generated = Object.keys(modules).find((key) =>
|
|
@@ -188,10 +193,12 @@ export const make = (
|
|
|
188
193
|
}
|
|
189
194
|
},
|
|
190
195
|
});
|
|
191
|
-
registerComponent(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
196
|
+
registerComponent(
|
|
197
|
+
componentPath,
|
|
198
|
+
schema,
|
|
199
|
+
{ ...modules, [bridgePath]: () => Promise.resolve({ invoke }) },
|
|
200
|
+
options,
|
|
201
|
+
);
|
|
195
202
|
registered.add(componentPath);
|
|
196
203
|
};
|
|
197
204
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Value } from "convex/values";
|
|
2
|
+
import type * as Invocation from "./Invocation.js";
|
|
3
|
+
import type * as Kernel from "./Kernel.js";
|
|
4
|
+
import type {
|
|
5
|
+
DocumentId,
|
|
6
|
+
FunctionPath,
|
|
7
|
+
StoredDocument,
|
|
8
|
+
} from "./KernelValues.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Convex's system table of scheduled functions. Each component has its own.
|
|
12
|
+
*/
|
|
13
|
+
export const table = "_scheduled_functions";
|
|
14
|
+
|
|
15
|
+
export type JobState =
|
|
16
|
+
| { readonly kind: "pending" }
|
|
17
|
+
| { readonly kind: "inProgress" }
|
|
18
|
+
| { readonly kind: "success" }
|
|
19
|
+
| { readonly kind: "failed"; readonly error: string }
|
|
20
|
+
| { readonly kind: "canceled" };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* What the kernel keeps beside a job's row: the function it calls, which a
|
|
24
|
+
* handle may place in another component, whether a standing source owns the
|
|
25
|
+
* job, and the test world whose invocation scheduled it. The row holds only
|
|
26
|
+
* what Convex's system table holds.
|
|
27
|
+
*/
|
|
28
|
+
export interface Target {
|
|
29
|
+
readonly functionPath: FunctionPath;
|
|
30
|
+
readonly standing: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The job runs as a fresh invocation of this world, with its services, or
|
|
33
|
+
* unbound when no invocation was bound as it was scheduled.
|
|
34
|
+
*/
|
|
35
|
+
readonly world: Invocation.World | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A pending job as a driver sees it.
|
|
40
|
+
*/
|
|
41
|
+
export interface Job {
|
|
42
|
+
readonly id: DocumentId;
|
|
43
|
+
readonly componentPath: string;
|
|
44
|
+
readonly name: string;
|
|
45
|
+
readonly scheduledTime: number;
|
|
46
|
+
/**
|
|
47
|
+
* A standing job is never drain work. Time reaches it through `advance`.
|
|
48
|
+
*/
|
|
49
|
+
readonly standing: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A pending job with what running it needs.
|
|
54
|
+
*/
|
|
55
|
+
export interface Due extends Job {
|
|
56
|
+
readonly target: FunctionPath;
|
|
57
|
+
readonly args: Value;
|
|
58
|
+
readonly world: Invocation.World | undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const row = (
|
|
62
|
+
id: DocumentId,
|
|
63
|
+
creationTime: number,
|
|
64
|
+
name: string,
|
|
65
|
+
args: Value,
|
|
66
|
+
scheduledTime: number,
|
|
67
|
+
): StoredDocument => ({
|
|
68
|
+
_id: id,
|
|
69
|
+
_creationTime: creationTime,
|
|
70
|
+
name,
|
|
71
|
+
args: [args],
|
|
72
|
+
scheduledTime,
|
|
73
|
+
state: { kind: "pending" },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export const stateOf = (doc: StoredDocument): JobState =>
|
|
77
|
+
doc["state"] as JobState;
|
|
78
|
+
|
|
79
|
+
const byDeadline = (a: Due, b: Due) =>
|
|
80
|
+
a.scheduledTime - b.scheduledTime || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Every committed pending job of every component, earliest deadline first. Jobs
|
|
84
|
+
* that share a deadline run in the order they were scheduled.
|
|
85
|
+
*/
|
|
86
|
+
export const pending = (
|
|
87
|
+
state: Kernel.State,
|
|
88
|
+
targets: ReadonlyMap<DocumentId, Target>,
|
|
89
|
+
): Array<Due> => {
|
|
90
|
+
const due: Array<Due> = [];
|
|
91
|
+
for (const [componentPath, tables] of state.tables) {
|
|
92
|
+
for (const { doc } of tables.get(table)?.values() ?? []) {
|
|
93
|
+
if (stateOf(doc).kind !== "pending") continue;
|
|
94
|
+
const name = doc["name"] as string;
|
|
95
|
+
const target = targets.get(doc._id) ?? {
|
|
96
|
+
functionPath: { componentPath, udfPath: name },
|
|
97
|
+
standing: false,
|
|
98
|
+
world: undefined,
|
|
99
|
+
};
|
|
100
|
+
due.push({
|
|
101
|
+
id: doc._id,
|
|
102
|
+
componentPath,
|
|
103
|
+
name,
|
|
104
|
+
scheduledTime: doc["scheduledTime"] as number,
|
|
105
|
+
standing: target.standing,
|
|
106
|
+
target: target.functionPath,
|
|
107
|
+
args: (doc["args"] as ReadonlyArray<Value>)[0]!,
|
|
108
|
+
world: target.world,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return due.sort(byDeadline);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The distinct functions of `jobs`, for a refusal that names what is left.
|
|
117
|
+
*/
|
|
118
|
+
export const functions = (
|
|
119
|
+
jobs: ReadonlyArray<Job>,
|
|
120
|
+
): Array<{ readonly componentPath: string; readonly name: string }> => {
|
|
121
|
+
const seen = new Map<string, { componentPath: string; name: string }>();
|
|
122
|
+
for (const { componentPath, name } of jobs) {
|
|
123
|
+
seen.set(`${componentPath}\u0000${name}`, { componentPath, name });
|
|
124
|
+
}
|
|
125
|
+
return [...seen.values()];
|
|
126
|
+
};
|