@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.
Files changed (56) hide show
  1. package/dist/TestConfect.d.ts +33 -7
  2. package/dist/TestConfect.d.ts.map +1 -1
  3. package/dist/TestConfect.js +11 -6
  4. package/dist/TestConfect.js.map +1 -1
  5. package/dist/internal/Attempt.js.map +1 -1
  6. package/dist/internal/Clock.d.ts +29 -0
  7. package/dist/internal/Clock.d.ts.map +1 -0
  8. package/dist/internal/Clock.js +66 -0
  9. package/dist/internal/Clock.js.map +1 -0
  10. package/dist/internal/ConvexTest.d.ts +33 -1
  11. package/dist/internal/ConvexTest.d.ts.map +1 -1
  12. package/dist/internal/ConvexTest.js +8 -5
  13. package/dist/internal/ConvexTest.js.map +1 -1
  14. package/dist/internal/Execution.d.ts +85 -5
  15. package/dist/internal/Execution.d.ts.map +1 -1
  16. package/dist/internal/Execution.js +285 -51
  17. package/dist/internal/Execution.js.map +1 -1
  18. package/dist/internal/Globals.d.ts.map +1 -1
  19. package/dist/internal/Globals.js +12 -0
  20. package/dist/internal/Globals.js.map +1 -1
  21. package/dist/internal/Invocation.d.ts +11 -0
  22. package/dist/internal/Invocation.d.ts.map +1 -1
  23. package/dist/internal/Invocation.js +34 -21
  24. package/dist/internal/Invocation.js.map +1 -1
  25. package/dist/internal/Kernel.d.ts +3 -1
  26. package/dist/internal/Kernel.d.ts.map +1 -1
  27. package/dist/internal/Kernel.js +2 -2
  28. package/dist/internal/Kernel.js.map +1 -1
  29. package/dist/internal/KernelRefusal.d.ts +25 -0
  30. package/dist/internal/KernelRefusal.d.ts.map +1 -1
  31. package/dist/internal/KernelRefusal.js +22 -1
  32. package/dist/internal/KernelRefusal.js.map +1 -1
  33. package/dist/internal/KernelValues.d.ts +6 -4
  34. package/dist/internal/KernelValues.d.ts.map +1 -1
  35. package/dist/internal/KernelValues.js +20 -5
  36. package/dist/internal/KernelValues.js.map +1 -1
  37. package/dist/internal/MountedHttp.d.ts.map +1 -1
  38. package/dist/internal/MountedHttp.js +3 -3
  39. package/dist/internal/MountedHttp.js.map +1 -1
  40. package/dist/internal/Scheduling.d.ts +71 -0
  41. package/dist/internal/Scheduling.d.ts.map +1 -0
  42. package/dist/internal/Scheduling.js +60 -0
  43. package/dist/internal/Scheduling.js.map +1 -0
  44. package/package.json +3 -3
  45. package/src/TestConfect.ts +62 -15
  46. package/src/internal/Attempt.ts +1 -1
  47. package/src/internal/Clock.ts +95 -0
  48. package/src/internal/ConvexTest.ts +51 -6
  49. package/src/internal/Execution.ts +415 -50
  50. package/src/internal/Globals.ts +20 -0
  51. package/src/internal/Invocation.ts +38 -13
  52. package/src/internal/Kernel.ts +5 -5
  53. package/src/internal/KernelRefusal.ts +24 -0
  54. package/src/internal/KernelValues.ts +44 -4
  55. package/src/internal/MountedHttp.ts +13 -6
  56. 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.89",
3
+ "version": "10.3.1-fork.90",
4
4
  "peerDependencies": {
5
- "@confect/core": "npm:@gaganref/confect-core@10.3.1-fork.89",
6
- "@confect/server": "npm:@gaganref/confect-server@10.3.1-fork.89",
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"
@@ -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
- ...args: Parameters<
30
- TestConvexForDataModelAndIdentity<GenericDataModel>["registerComponent"]
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
- export type LayerOptions = {
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
- registryOrSchema?:
429
+ second?:
396
430
  | Registry<DatabaseSchema_>
397
- | SchemaDefinition<GenericSchema, true>,
398
- modules?: Record<string, () => Promise<any>>,
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 = registryOrSchema as Registry<DatabaseSchema_>;
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
- registryOrSchema as SchemaDefinition<GenericSchema, true>,
441
- modules!,
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 = convexTest(convexSchemaDefinition, modules);
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
- testConvex.registerComponent(...args),
504
+ registerComponent(...args),
458
505
  );
459
506
  options.register?.(mountedHttp.register);
460
507
  return new TestConfectImpl(databaseSchema, testConvex as any, mountedHttp);
@@ -1,4 +1,4 @@
1
- // oxlint-disable-next-line effecttsgo/node-builtin-import -- The drain waits a real turn of the event loop. Fake timers in a test replace the global `setImmediate`, not this one.
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
- finishInProgressScheduledFunctions: () =>
63
- Promise.reject(KernelRefusal.rowPending("the scheduler", 5)),
64
- finishAllScheduledFunctions: () =>
65
- Promise.reject(KernelRefusal.rowPending("the scheduler", 5)),
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(componentPath, componentSchema, componentModules);
154
+ world.register(
155
+ componentPath,
156
+ componentSchema,
157
+ componentModules,
158
+ options?.standing,
159
+ );
115
160
  return (componentsGeneric() as any)[componentPath];
116
161
  },
117
162
  };