@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
|
@@ -0,0 +1,71 @@
|
|
|
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 { DocumentId, FunctionPath, StoredDocument } from "./KernelValues.js";
|
|
5
|
+
/**
|
|
6
|
+
* Convex's system table of scheduled functions. Each component has its own.
|
|
7
|
+
*/
|
|
8
|
+
export declare const table = "_scheduled_functions";
|
|
9
|
+
export type JobState = {
|
|
10
|
+
readonly kind: "pending";
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: "inProgress";
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: "success";
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: "failed";
|
|
17
|
+
readonly error: string;
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: "canceled";
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* What the kernel keeps beside a job's row: the function it calls, which a
|
|
23
|
+
* handle may place in another component, whether a standing source owns the
|
|
24
|
+
* job, and the test world whose invocation scheduled it. The row holds only
|
|
25
|
+
* what Convex's system table holds.
|
|
26
|
+
*/
|
|
27
|
+
export interface Target {
|
|
28
|
+
readonly functionPath: FunctionPath;
|
|
29
|
+
readonly standing: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The job runs as a fresh invocation of this world, with its services, or
|
|
32
|
+
* unbound when no invocation was bound as it was scheduled.
|
|
33
|
+
*/
|
|
34
|
+
readonly world: Invocation.World | undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A pending job as a driver sees it.
|
|
38
|
+
*/
|
|
39
|
+
export interface Job {
|
|
40
|
+
readonly id: DocumentId;
|
|
41
|
+
readonly componentPath: string;
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly scheduledTime: number;
|
|
44
|
+
/**
|
|
45
|
+
* A standing job is never drain work. Time reaches it through `advance`.
|
|
46
|
+
*/
|
|
47
|
+
readonly standing: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A pending job with what running it needs.
|
|
51
|
+
*/
|
|
52
|
+
export interface Due extends Job {
|
|
53
|
+
readonly target: FunctionPath;
|
|
54
|
+
readonly args: Value;
|
|
55
|
+
readonly world: Invocation.World | undefined;
|
|
56
|
+
}
|
|
57
|
+
export declare const row: (id: DocumentId, creationTime: number, name: string, args: Value, scheduledTime: number) => StoredDocument;
|
|
58
|
+
export declare const stateOf: (doc: StoredDocument) => JobState;
|
|
59
|
+
/**
|
|
60
|
+
* Every committed pending job of every component, earliest deadline first. Jobs
|
|
61
|
+
* that share a deadline run in the order they were scheduled.
|
|
62
|
+
*/
|
|
63
|
+
export declare const pending: (state: Kernel.State, targets: ReadonlyMap<DocumentId, Target>) => Array<Due>;
|
|
64
|
+
/**
|
|
65
|
+
* The distinct functions of `jobs`, for a refusal that names what is left.
|
|
66
|
+
*/
|
|
67
|
+
export declare const functions: (jobs: ReadonlyArray<Job>) => Array<{
|
|
68
|
+
readonly componentPath: string;
|
|
69
|
+
readonly name: string;
|
|
70
|
+
}>;
|
|
71
|
+
//# sourceMappingURL=Scheduling.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Scheduling.d.ts","sourceRoot":"","sources":["../../src/internal/Scheduling.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,KAAK,UAAU,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,KAAK,MAAM,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EACZ,cAAc,EACf,MAAM,mBAAmB,CAAC;AAE3B;;GAEG;AACH,eAAO,MAAM,KAAK,yBAAyB,CAAC;AAE5C,MAAM,MAAM,QAAQ,GAChB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GAC/B;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;AAElC;;;;;GAKG;AACH,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,KAAK,GAAG,SAAS,CAAC;CAC9C;AAED;;GAEG;AACH,MAAM,WAAW,GAAG;IAClB,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,GAAI,SAAQ,GAAG;IAC9B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,KAAK,GAAG,SAAS,CAAC;CAC9C;AAED,eAAO,MAAM,GAAG,OACV,UAAU,gBACA,MAAM,QACd,MAAM,QACN,KAAK,iBACI,MAAM,KACpB,cAOD,CAAC;AAEH,eAAO,MAAM,OAAO,QAAS,cAAc,KAAG,QACpB,CAAC;AAK3B;;;GAGG;AACH,eAAO,MAAM,OAAO,UACX,MAAM,CAAC,KAAK,WACV,WAAW,CAAC,UAAU,EAAE,MAAM,CAAC,KACvC,KAAK,CAAC,GAAG,CAwBX,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,SAAS,SACd,aAAa,CAAC,GAAG,CAAC,KACvB,KAAK,CAAC;IAAE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAMjE,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//#region src/internal/Scheduling.ts
|
|
2
|
+
/**
|
|
3
|
+
* Convex's system table of scheduled functions. Each component has its own.
|
|
4
|
+
*/
|
|
5
|
+
const table = "_scheduled_functions";
|
|
6
|
+
const row = (id, creationTime, name, args, scheduledTime) => ({
|
|
7
|
+
_id: id,
|
|
8
|
+
_creationTime: creationTime,
|
|
9
|
+
name,
|
|
10
|
+
args: [args],
|
|
11
|
+
scheduledTime,
|
|
12
|
+
state: { kind: "pending" }
|
|
13
|
+
});
|
|
14
|
+
const stateOf = (doc) => doc["state"];
|
|
15
|
+
const byDeadline = (a, b) => a.scheduledTime - b.scheduledTime || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
16
|
+
/**
|
|
17
|
+
* Every committed pending job of every component, earliest deadline first. Jobs
|
|
18
|
+
* that share a deadline run in the order they were scheduled.
|
|
19
|
+
*/
|
|
20
|
+
const pending = (state, targets) => {
|
|
21
|
+
const due = [];
|
|
22
|
+
for (const [componentPath, tables] of state.tables) for (const { doc } of tables.get("_scheduled_functions")?.values() ?? []) {
|
|
23
|
+
if (stateOf(doc).kind !== "pending") continue;
|
|
24
|
+
const name = doc["name"];
|
|
25
|
+
const target = targets.get(doc._id) ?? {
|
|
26
|
+
functionPath: {
|
|
27
|
+
componentPath,
|
|
28
|
+
udfPath: name
|
|
29
|
+
},
|
|
30
|
+
standing: false,
|
|
31
|
+
world: void 0
|
|
32
|
+
};
|
|
33
|
+
due.push({
|
|
34
|
+
id: doc._id,
|
|
35
|
+
componentPath,
|
|
36
|
+
name,
|
|
37
|
+
scheduledTime: doc["scheduledTime"],
|
|
38
|
+
standing: target.standing,
|
|
39
|
+
target: target.functionPath,
|
|
40
|
+
args: doc["args"][0],
|
|
41
|
+
world: target.world
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return due.sort(byDeadline);
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* The distinct functions of `jobs`, for a refusal that names what is left.
|
|
48
|
+
*/
|
|
49
|
+
const functions = (jobs) => {
|
|
50
|
+
const seen = /* @__PURE__ */ new Map();
|
|
51
|
+
for (const { componentPath, name } of jobs) seen.set(`${componentPath}\u0000${name}`, {
|
|
52
|
+
componentPath,
|
|
53
|
+
name
|
|
54
|
+
});
|
|
55
|
+
return [...seen.values()];
|
|
56
|
+
};
|
|
57
|
+
//#endregion
|
|
58
|
+
export { functions, pending, row, stateOf, table };
|
|
59
|
+
|
|
60
|
+
//# sourceMappingURL=Scheduling.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Scheduling.js","names":[],"sources":["../../src/internal/Scheduling.ts"],"sourcesContent":["import type { Value } from \"convex/values\";\nimport type * as Invocation from \"./Invocation.js\";\nimport type * as Kernel from \"./Kernel.js\";\nimport type {\n DocumentId,\n FunctionPath,\n StoredDocument,\n} from \"./KernelValues.js\";\n\n/**\n * Convex's system table of scheduled functions. Each component has its own.\n */\nexport const table = \"_scheduled_functions\";\n\nexport type JobState =\n | { readonly kind: \"pending\" }\n | { readonly kind: \"inProgress\" }\n | { readonly kind: \"success\" }\n | { readonly kind: \"failed\"; readonly error: string }\n | { readonly kind: \"canceled\" };\n\n/**\n * What the kernel keeps beside a job's row: the function it calls, which a\n * handle may place in another component, whether a standing source owns the\n * job, and the test world whose invocation scheduled it. The row holds only\n * what Convex's system table holds.\n */\nexport interface Target {\n readonly functionPath: FunctionPath;\n readonly standing: boolean;\n /**\n * The job runs as a fresh invocation of this world, with its services, or\n * unbound when no invocation was bound as it was scheduled.\n */\n readonly world: Invocation.World | undefined;\n}\n\n/**\n * A pending job as a driver sees it.\n */\nexport interface Job {\n readonly id: DocumentId;\n readonly componentPath: string;\n readonly name: string;\n readonly scheduledTime: number;\n /**\n * A standing job is never drain work. Time reaches it through `advance`.\n */\n readonly standing: boolean;\n}\n\n/**\n * A pending job with what running it needs.\n */\nexport interface Due extends Job {\n readonly target: FunctionPath;\n readonly args: Value;\n readonly world: Invocation.World | undefined;\n}\n\nexport const row = (\n id: DocumentId,\n creationTime: number,\n name: string,\n args: Value,\n scheduledTime: number,\n): StoredDocument => ({\n _id: id,\n _creationTime: creationTime,\n name,\n args: [args],\n scheduledTime,\n state: { kind: \"pending\" },\n});\n\nexport const stateOf = (doc: StoredDocument): JobState =>\n doc[\"state\"] as JobState;\n\nconst byDeadline = (a: Due, b: Due) =>\n a.scheduledTime - b.scheduledTime || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);\n\n/**\n * Every committed pending job of every component, earliest deadline first. Jobs\n * that share a deadline run in the order they were scheduled.\n */\nexport const pending = (\n state: Kernel.State,\n targets: ReadonlyMap<DocumentId, Target>,\n): Array<Due> => {\n const due: Array<Due> = [];\n for (const [componentPath, tables] of state.tables) {\n for (const { doc } of tables.get(table)?.values() ?? []) {\n if (stateOf(doc).kind !== \"pending\") continue;\n const name = doc[\"name\"] as string;\n const target = targets.get(doc._id) ?? {\n functionPath: { componentPath, udfPath: name },\n standing: false,\n world: undefined,\n };\n due.push({\n id: doc._id,\n componentPath,\n name,\n scheduledTime: doc[\"scheduledTime\"] as number,\n standing: target.standing,\n target: target.functionPath,\n args: (doc[\"args\"] as ReadonlyArray<Value>)[0]!,\n world: target.world,\n });\n }\n }\n return due.sort(byDeadline);\n};\n\n/**\n * The distinct functions of `jobs`, for a refusal that names what is left.\n */\nexport const functions = (\n jobs: ReadonlyArray<Job>,\n): Array<{ readonly componentPath: string; readonly name: string }> => {\n const seen = new Map<string, { componentPath: string; name: string }>();\n for (const { componentPath, name } of jobs) {\n seen.set(`${componentPath}\\u0000${name}`, { componentPath, name });\n }\n return [...seen.values()];\n};\n"],"mappings":";;;;AAYA,MAAa,QAAQ;AAgDrB,MAAa,OACX,IACA,cACA,MACA,MACA,mBACoB;CACpB,KAAK;CACL,eAAe;CACf;CACA,MAAM,CAAC,IAAI;CACX;CACA,OAAO,EAAE,MAAM,UAAU;AAC3B;AAEA,MAAa,WAAW,QACtB,IAAI;AAEN,MAAM,cAAc,GAAQ,MAC1B,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK,EAAE,KAAK,IAAI;;;;;AAM7E,MAAa,WACX,OACA,YACe;CACf,MAAM,MAAkB,CAAC;CACzB,KAAK,MAAM,CAAC,eAAe,WAAW,MAAM,QAC1C,KAAK,MAAM,EAAE,SAAS,OAAO,IAAA,sBAAS,CAAC,EAAE,OAAO,KAAK,CAAC,GAAG;EACvD,IAAI,QAAQ,GAAG,CAAC,CAAC,SAAS,WAAW;EACrC,MAAM,OAAO,IAAI;EACjB,MAAM,SAAS,QAAQ,IAAI,IAAI,GAAG,KAAK;GACrC,cAAc;IAAE;IAAe,SAAS;GAAK;GAC7C,UAAU;GACV,OAAO,KAAA;EACT;EACA,IAAI,KAAK;GACP,IAAI,IAAI;GACR;GACA;GACA,eAAe,IAAI;GACnB,UAAU,OAAO;GACjB,QAAQ,OAAO;GACf,MAAO,IAAI,OAAO,CAA0B;GAC5C,OAAO,OAAO;EAChB,CAAC;CACH;CAEF,OAAO,IAAI,KAAK,UAAU;AAC5B;;;;AAKA,MAAa,aACX,SACqE;CACrE,MAAM,uBAAO,IAAI,IAAqD;CACtE,KAAK,MAAM,EAAE,eAAe,UAAU,MACpC,KAAK,IAAI,GAAG,cAAc,QAAQ,QAAQ;EAAE;EAAe;CAAK,CAAC;CAEnE,OAAO,CAAC,GAAG,KAAK,OAAO,CAAC;AAC1B"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaganref/confect-test",
|
|
3
|
-
"version": "10.3.1-fork.
|
|
3
|
+
"version": "10.3.1-fork.90",
|
|
4
4
|
"peerDependencies": {
|
|
5
|
-
"@confect/core": "npm:@gaganref/confect-core@10.3.1-fork.
|
|
6
|
-
"@confect/server": "npm:@gaganref/confect-server@10.3.1-fork.
|
|
5
|
+
"@confect/core": "npm:@gaganref/confect-core@10.3.1-fork.90",
|
|
6
|
+
"@confect/server": "npm:@gaganref/confect-server@10.3.1-fork.90",
|
|
7
7
|
"convex": "^1.32.0",
|
|
8
8
|
"convex-test": ">=0.0.65 <0.1.0",
|
|
9
9
|
"effect": ">=4.0.0 <4.1.0"
|
package/src/TestConfect.ts
CHANGED
|
@@ -7,7 +7,6 @@ import type {
|
|
|
7
7
|
} from "convex-test";
|
|
8
8
|
import { convexTest } from "convex-test";
|
|
9
9
|
import type {
|
|
10
|
-
GenericDataModel,
|
|
11
10
|
GenericMutationCtx,
|
|
12
11
|
GenericSchema,
|
|
13
12
|
SchemaDefinition,
|
|
@@ -18,17 +17,32 @@ import * as Context from "effect/Context";
|
|
|
18
17
|
import * as Effect from "effect/Effect";
|
|
19
18
|
import * as Layer from "effect/Layer";
|
|
20
19
|
import * as Schema from "effect/Schema";
|
|
20
|
+
import { convexTest as kernelTest } from "./internal/ConvexTest.js";
|
|
21
21
|
import * as MountedHttp from "./internal/MountedHttp.js";
|
|
22
22
|
|
|
23
23
|
declare const process: { env: Record<string, string | undefined> };
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* How a kit registers a component beyond its schema and modules.
|
|
27
|
+
*/
|
|
28
|
+
export type RegisterOptions = {
|
|
29
|
+
/**
|
|
30
|
+
* The component's functions, as `module:export`, that schedule their own next
|
|
31
|
+
* run, such as the crons component's `public:rescheduler`. The test kernel
|
|
32
|
+
* never treats a job that runs one, or that one schedules, as work
|
|
33
|
+
* `finishAllScheduledFunctions` must finish. convex-test ignores it.
|
|
34
|
+
*/
|
|
35
|
+
readonly standing?: ReadonlyArray<string>;
|
|
36
|
+
};
|
|
37
|
+
|
|
25
38
|
/**
|
|
26
39
|
* Register a component's Convex schema and modules at its installation path.
|
|
27
40
|
*/
|
|
28
41
|
export type RegisterComponent = (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
componentPath: string,
|
|
43
|
+
schema: SchemaDefinition<GenericSchema, boolean>,
|
|
44
|
+
modules: Record<string, () => Promise<any>>,
|
|
45
|
+
options?: RegisterOptions,
|
|
32
46
|
) => void;
|
|
33
47
|
|
|
34
48
|
/**
|
|
@@ -39,7 +53,22 @@ export type HttpMount = {
|
|
|
39
53
|
readonly httpPrefix: string;
|
|
40
54
|
};
|
|
41
55
|
|
|
42
|
-
|
|
56
|
+
/**
|
|
57
|
+
* The in-process backend a test layer runs on.
|
|
58
|
+
*/
|
|
59
|
+
export type Target = "convex-test" | "kernel";
|
|
60
|
+
|
|
61
|
+
export type TargetOptions = {
|
|
62
|
+
/**
|
|
63
|
+
* `"kernel"` runs every function and component the layer registers on
|
|
64
|
+
* Confect's test kernel, which runs scheduled functions on its own clock and
|
|
65
|
+
* reads `registerComponent`'s `standing`. Defaults to `"convex-test"`. The
|
|
66
|
+
* option goes once the kernel replaces convex-test.
|
|
67
|
+
*/
|
|
68
|
+
readonly target?: Target;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type LayerOptions = TargetOptions & {
|
|
43
72
|
/**
|
|
44
73
|
* Register component kits synchronously before the test service is available.
|
|
45
74
|
*/
|
|
@@ -106,6 +135,7 @@ export type TestConfect<ConfectSchema extends DatabaseSchema.AnyWithProps> = {
|
|
|
106
135
|
componentPath: string,
|
|
107
136
|
schema: SchemaDefinition<GenericSchema, boolean>,
|
|
108
137
|
modules: Record<string, () => Promise<any>>,
|
|
138
|
+
options?: RegisterOptions,
|
|
109
139
|
) => Effect.Effect<void>;
|
|
110
140
|
withIdentity: (
|
|
111
141
|
userIdentity: Partial<UserIdentity>,
|
|
@@ -261,9 +291,9 @@ class TestConfectImpl<
|
|
|
261
291
|
|
|
262
292
|
readonly registerComponent: TestConfect<ConfectSchema>["registerComponent"] =
|
|
263
293
|
Effect.fn("TestConfect.registerComponent")(
|
|
264
|
-
(componentPath, schema, modules) =>
|
|
294
|
+
(componentPath, schema, modules, options) =>
|
|
265
295
|
Effect.sync(() =>
|
|
266
|
-
this.mountedHttp.register(componentPath, schema, modules),
|
|
296
|
+
this.mountedHttp.register(componentPath, schema, modules, options),
|
|
267
297
|
),
|
|
268
298
|
);
|
|
269
299
|
|
|
@@ -366,9 +396,12 @@ export type DeclaredApp<DatabaseSchema_ extends DatabaseSchema.AnyWithProps> =
|
|
|
366
396
|
/**
|
|
367
397
|
* Build a fresh test backend from a declaration and its generated
|
|
368
398
|
* `confect/_generated/testing` registry, without restating component mounts.
|
|
399
|
+
* Pass `{ target: "kernel" }` to run it on Confect's test kernel instead of
|
|
400
|
+
* convex-test.
|
|
369
401
|
*/
|
|
370
402
|
export function layer<DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(
|
|
371
403
|
app: DeclaredApp<DatabaseSchema_>,
|
|
404
|
+
options?: TargetOptions,
|
|
372
405
|
): Layer.Layer<TestConfect<DatabaseSchema_>>;
|
|
373
406
|
export function layer<
|
|
374
407
|
A extends App.Any,
|
|
@@ -383,6 +416,7 @@ export function layer<
|
|
|
383
416
|
>
|
|
384
417
|
>;
|
|
385
418
|
},
|
|
419
|
+
options?: TargetOptions,
|
|
386
420
|
): Layer.Layer<TestConfect<DatabaseSchema_>>;
|
|
387
421
|
export function layer<DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(
|
|
388
422
|
databaseSchema: DatabaseSchema_,
|
|
@@ -392,20 +426,22 @@ export function layer<DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(
|
|
|
392
426
|
): Layer.Layer<TestConfect<DatabaseSchema_>>;
|
|
393
427
|
export function layer<DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(
|
|
394
428
|
appOrSchema: App.Any | DeclaredApp<DatabaseSchema_> | DatabaseSchema_,
|
|
395
|
-
|
|
429
|
+
second?:
|
|
396
430
|
| Registry<DatabaseSchema_>
|
|
397
|
-
| SchemaDefinition<GenericSchema, true
|
|
398
|
-
|
|
431
|
+
| SchemaDefinition<GenericSchema, true>
|
|
432
|
+
| TargetOptions,
|
|
433
|
+
third?: Record<string, () => Promise<any>> | TargetOptions,
|
|
399
434
|
options: LayerOptions = {},
|
|
400
435
|
): Layer.Layer<TestConfect<DatabaseSchema_>> {
|
|
401
436
|
if (App.isApp(appOrSchema)) {
|
|
402
|
-
const registry =
|
|
437
|
+
const registry = second as Registry<DatabaseSchema_>;
|
|
403
438
|
const mounts = App.mounts(appOrSchema);
|
|
404
439
|
return schemaLayer(
|
|
405
440
|
registry.schema,
|
|
406
441
|
registry.convexSchema,
|
|
407
442
|
registry.modules,
|
|
408
443
|
{
|
|
444
|
+
...targetOf(third as TargetOptions | undefined),
|
|
409
445
|
httpMounts: mounts.flatMap(({ name, httpPrefix }) =>
|
|
410
446
|
httpPrefix === undefined ? [] : [{ componentPath: name, httpPrefix }],
|
|
411
447
|
),
|
|
@@ -427,6 +463,7 @@ export function layer<DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(
|
|
|
427
463
|
if ("httpMounts" in appOrSchema) {
|
|
428
464
|
const app = appOrSchema as DeclaredApp<DatabaseSchema_>;
|
|
429
465
|
return schemaLayer(app.schema, app.convexSchema, app.modules, {
|
|
466
|
+
...targetOf(second as TargetOptions | undefined),
|
|
430
467
|
httpMounts: app.httpMounts,
|
|
431
468
|
register: (registerComponent) => {
|
|
432
469
|
for (const [name, component] of Object.entries(app.components)) {
|
|
@@ -437,12 +474,15 @@ export function layer<DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(
|
|
|
437
474
|
}
|
|
438
475
|
return schemaLayer(
|
|
439
476
|
appOrSchema as DatabaseSchema_,
|
|
440
|
-
|
|
441
|
-
|
|
477
|
+
second as SchemaDefinition<GenericSchema, true>,
|
|
478
|
+
third as Record<string, () => Promise<any>>,
|
|
442
479
|
options,
|
|
443
480
|
);
|
|
444
481
|
}
|
|
445
482
|
|
|
483
|
+
const targetOf = (options: TargetOptions | undefined): TargetOptions =>
|
|
484
|
+
options?.target === undefined ? {} : { target: options.target };
|
|
485
|
+
|
|
446
486
|
const schemaLayer = <DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(
|
|
447
487
|
databaseSchema: DatabaseSchema_,
|
|
448
488
|
convexSchemaDefinition: SchemaDefinition<GenericSchema, true>,
|
|
@@ -452,9 +492,16 @@ const schemaLayer = <DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(
|
|
|
452
492
|
Layer.sync(TestConfect<DatabaseSchema_>(), () => {
|
|
453
493
|
process.env.CONVEX_CLOUD_URL ??= "https://some-deployment.convex.cloud";
|
|
454
494
|
process.env.CONVEX_SITE_URL ??= "https://some.convex.site";
|
|
455
|
-
const testConvex =
|
|
495
|
+
const testConvex = (options.target === "kernel" ? kernelTest : convexTest)(
|
|
496
|
+
convexSchemaDefinition,
|
|
497
|
+
modules,
|
|
498
|
+
);
|
|
499
|
+
// convex-test takes three arguments and ignores the options. The kernel
|
|
500
|
+
// reads `standing`.
|
|
501
|
+
const registerComponent =
|
|
502
|
+
testConvex.registerComponent as unknown as RegisterComponent;
|
|
456
503
|
const mountedHttp = MountedHttp.make(options.httpMounts ?? [], (...args) =>
|
|
457
|
-
|
|
504
|
+
registerComponent(...args),
|
|
458
505
|
);
|
|
459
506
|
options.register?.(mountedHttp.register);
|
|
460
507
|
return new TestConfectImpl(databaseSchema, testConvex as any, mountedHttp);
|
package/src/internal/Attempt.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// oxlint-disable-next-line effecttsgo/node-builtin-import --
|
|
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
2
|
import { setImmediate } from "node:timers/promises";
|
|
3
3
|
import type { Attempt as AttemptNumber } from "./Invocation.js";
|
|
4
4
|
import * as Kernel from "./Kernel.js";
|
|
@@ -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
|
+
});
|
|
@@ -7,6 +7,7 @@ import { componentsGeneric } from "convex/server";
|
|
|
7
7
|
import * as Execution from "./Execution.js";
|
|
8
8
|
import type * as Kernel from "./Kernel.js";
|
|
9
9
|
import * as KernelRefusal from "./KernelRefusal.js";
|
|
10
|
+
import type * as Scheduling from "./Scheduling.js";
|
|
10
11
|
|
|
11
12
|
type Modules = Record<string, () => Promise<any>>;
|
|
12
13
|
|
|
@@ -24,6 +25,40 @@ export interface KernelControls {
|
|
|
24
25
|
) => Promise<Execution.HeldAttempt>;
|
|
25
26
|
readonly apply: (id: string) => Promise<Execution.KernelApply>;
|
|
26
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>;
|
|
27
62
|
}
|
|
28
63
|
|
|
29
64
|
const isSchemaDefinition = (
|
|
@@ -59,10 +94,13 @@ const accessor = (world: Execution.World, caller: Execution.Caller): any => ({
|
|
|
59
94
|
}),
|
|
60
95
|
fetch: () => Promise.reject(KernelRefusal.rowPending("HTTP routes", 6)),
|
|
61
96
|
jwks: () => Promise.reject(KernelRefusal.rowPending("JWT verification", 6)),
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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),
|
|
66
104
|
});
|
|
67
105
|
|
|
68
106
|
/**
|
|
@@ -73,7 +111,8 @@ const accessor = (world: Execution.World, caller: Execution.Caller): any => ({
|
|
|
73
111
|
* Beyond convex-test, `prepare` runs a mutation to its commit point and holds
|
|
74
112
|
* it, and `apply` validates and publishes a held attempt. `mutation` applies
|
|
75
113
|
* each attempt it prepares at once and prepares the next on a conflict, up to
|
|
76
|
-
* attempt 5. `runtime` supplies the clock and commit timestamps.
|
|
114
|
+
* attempt 5. `runtime` supplies the clock and commit timestamps. Scheduled
|
|
115
|
+
* functions run on the kernel's clock, never on a host timer.
|
|
77
116
|
*/
|
|
78
117
|
export function convexTest(
|
|
79
118
|
schemaOrOptions?:
|
|
@@ -110,8 +149,14 @@ export function convexTest(
|
|
|
110
149
|
componentPath: string,
|
|
111
150
|
componentSchema: SchemaDefinition<GenericSchema, boolean>,
|
|
112
151
|
componentModules: Modules,
|
|
152
|
+
options?: RegisterOptions,
|
|
113
153
|
) => {
|
|
114
|
-
world.register(
|
|
154
|
+
world.register(
|
|
155
|
+
componentPath,
|
|
156
|
+
componentSchema,
|
|
157
|
+
componentModules,
|
|
158
|
+
options?.standing,
|
|
159
|
+
);
|
|
115
160
|
return (componentsGeneric() as any)[componentPath];
|
|
116
161
|
},
|
|
117
162
|
};
|