@gaganref/confect-test 10.3.1-fork.92 → 10.3.1-fork.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/TestConfect.d.ts +26 -0
  2. package/dist/TestConfect.d.ts.map +1 -1
  3. package/dist/TestConfect.js +9 -1
  4. package/dist/TestConfect.js.map +1 -1
  5. package/dist/internal/ConvexTest.d.ts +26 -1
  6. package/dist/internal/ConvexTest.d.ts.map +1 -1
  7. package/dist/internal/ConvexTest.js +55 -27
  8. package/dist/internal/ConvexTest.js.map +1 -1
  9. package/dist/internal/Execution.d.ts +138 -1
  10. package/dist/internal/Execution.d.ts.map +1 -1
  11. package/dist/internal/Execution.js +595 -182
  12. package/dist/internal/Execution.js.map +1 -1
  13. package/dist/internal/KernelRefusal.d.ts +134 -0
  14. package/dist/internal/KernelRefusal.d.ts.map +1 -1
  15. package/dist/internal/KernelRefusal.js +134 -1
  16. package/dist/internal/KernelRefusal.js.map +1 -1
  17. package/dist/internal/Redaction.d.ts +142 -0
  18. package/dist/internal/Redaction.d.ts.map +1 -0
  19. package/dist/internal/Redaction.js +448 -0
  20. package/dist/internal/Redaction.js.map +1 -0
  21. package/dist/internal/Snapshot.d.ts +298 -0
  22. package/dist/internal/Snapshot.d.ts.map +1 -0
  23. package/dist/internal/Snapshot.js +532 -0
  24. package/dist/internal/Snapshot.js.map +1 -0
  25. package/dist/internal/Subscriptions.d.ts +13 -0
  26. package/dist/internal/Subscriptions.d.ts.map +1 -1
  27. package/dist/internal/Subscriptions.js +27 -3
  28. package/dist/internal/Subscriptions.js.map +1 -1
  29. package/package.json +3 -3
  30. package/src/TestConfect.ts +63 -4
  31. package/src/internal/ConvexTest.ts +135 -38
  32. package/src/internal/Execution.ts +886 -287
  33. package/src/internal/KernelRefusal.ts +160 -0
  34. package/src/internal/Redaction.ts +730 -0
  35. package/src/internal/Snapshot.ts +945 -0
  36. package/src/internal/Subscriptions.ts +34 -4
@@ -1 +1 @@
1
- {"version":3,"file":"Subscriptions.d.ts","sourceRoot":"","sources":["../../src/internal/Subscriptions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Subscriptions.d.ts","sourceRoot":"","sources":["../../src/internal/Subscriptions.ts"],"names":[],"mappings":"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"}
@@ -55,10 +55,11 @@ const inTurn = (items, f) => items.reduce((previous, item) => previous.then((kep
55
55
  var Subscriber = class {
56
56
  registry;
57
57
  who;
58
- version = 0;
58
+ version;
59
59
  constructor(registry, who) {
60
60
  this.registry = registry;
61
61
  this.who = who;
62
+ this.version = registry.identityEpoch();
62
63
  }
63
64
  get caller() {
64
65
  return this.who;
@@ -79,7 +80,7 @@ var Subscriber = class {
79
80
  identity: null,
80
81
  token: null
81
82
  } : withIdentity(this.who, identity);
82
- this.version++;
83
+ this.version = this.registry.nextIdentityEpoch();
83
84
  }
84
85
  };
85
86
  /**
@@ -95,8 +96,28 @@ var Registry = class {
95
96
  this.world = world;
96
97
  }
97
98
  subscriber(caller) {
99
+ this.world.registerSubscriber();
98
100
  return new Subscriber(this, caller);
99
101
  }
102
+ /**
103
+ * The reference a live subscription runs, for marking its diffs by the shape
104
+ * of what its query returns.
105
+ */
106
+ referenceOf(subscription) {
107
+ return this.live.get(subscription)?.reference;
108
+ }
109
+ /**
110
+ * The world's identity epoch, where a new subscriber's version starts.
111
+ */
112
+ identityEpoch() {
113
+ return this.world.currentIdentityEpoch();
114
+ }
115
+ /**
116
+ * Advance the world's identity epoch for a subscriber whose identity changed.
117
+ */
118
+ nextIdentityEpoch() {
119
+ return this.world.nextIdentityEpoch();
120
+ }
100
121
  subscribe(subscriber, reference, args) {
101
122
  return this.run(subscriber, reference, args, { journal: null }).then((run) => {
102
123
  const live = {
@@ -109,7 +130,10 @@ var Registry = class {
109
130
  this.live.set(live.id, live);
110
131
  return {
111
132
  id: live.id,
112
- current: () => live.run,
133
+ current: () => ({
134
+ ...live.run,
135
+ reads: this.world.projectReads(live.run.reads)
136
+ }),
113
137
  unsubscribe: () => {
114
138
  this.live.delete(live.id);
115
139
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Subscriptions.js","names":["Kernel.conflict","Execution.withIdentity"],"sources":["../../src/internal/Subscriptions.ts"],"sourcesContent":["import type { UserIdentity } from \"convex/server\";\nimport { compareValues, convexToJson, type Value } from \"convex/values\";\nimport * as Execution from \"./Execution.js\";\nimport * as Kernel from \"./Kernel.js\";\n\n/**\n * One run of a subscribed query, and the identity version its subscriber had\n * when it ran.\n */\nexport interface Run extends Execution.QueryRun {\n readonly identityVersion: number;\n}\n\n/**\n * Why a run no longer shows what its query answers now.\n */\nexport type Invalidation =\n | { readonly kind: \"commit\"; readonly change: Kernel.Change }\n | {\n readonly kind: \"identity\";\n readonly from: number;\n readonly to: number;\n };\n\n/**\n * Why `run` is stale, if it is: a commit since its snapshot changed something\n * it read, as an attempt's read set conflicts, or it read its subscriber's\n * identity and that identity has changed. The clock never makes a run stale.\n * The binary does not rerun a query because time passed, so a run keeps the\n * time it read until something else invalidates it.\n */\nexport const invalidation = (\n run: Run,\n state: Kernel.State,\n identityVersion: number,\n): Invalidation | undefined => {\n const change = Kernel.conflict(state, run.snapshot, run.reads);\n if (change !== undefined) return { kind: \"commit\", change };\n if (run.observedIdentity && run.identityVersion !== identityVersion) {\n return { kind: \"identity\", from: run.identityVersion, to: identityVersion };\n }\n return undefined;\n};\n\n/**\n * A thrown value as JSON: a Convex value as Convex writes it, and anything else\n * as its string, beside its type so `\"1\"` and `1` differ.\n */\nconst thrownKey = (thrown: unknown): unknown => {\n try {\n return [typeof thrown, convexToJson(thrown as Value)];\n } catch {\n return [typeof thrown, String(thrown)];\n }\n};\n\nconst errorKey = (error: unknown): string =>\n JSON.stringify(\n error instanceof Error\n ? [\n \"error\",\n error.name,\n error.message,\n thrownKey((error as { readonly data?: unknown }).data),\n ]\n : [\"thrown\", thrownKey(error)],\n );\n\n/**\n * Whether two outcomes show a client the same thing: equal values, failures\n * with errors of the same name, message and data, or failures that threw equal\n * bare values.\n */\nexport const sameOutcome = (\n a: Execution.Outcome,\n b: Execution.Outcome,\n): boolean =>\n a._tag === \"returned\"\n ? b._tag === \"returned\" && compareValues(a.value, b.value) === 0\n : b._tag === \"failed\" && errorKey(a.error) === errorKey(b.error);\n\n/**\n * A subscription's value changed when a refresh reran it.\n */\nexport interface Transition {\n readonly subscription: string;\n readonly from: Execution.Outcome;\n readonly to: Execution.Outcome;\n readonly because: Invalidation;\n}\n\n/**\n * A subscription whose value differs from what a full rerun of its query\n * answers.\n */\nexport interface Disagreement {\n readonly subscription: string;\n readonly incremental: Execution.Outcome;\n readonly reference: Execution.Outcome;\n}\n\n/**\n * Run `f` over `items` one at a time, in order, and keep what it answers.\n */\nconst inTurn = <A, B>(\n items: ReadonlyArray<A>,\n f: (item: A) => Promise<B | undefined>,\n): Promise<Array<B>> =>\n items.reduce<Promise<Array<B>>>(\n (previous, item) =>\n previous.then((kept) =>\n f(item).then((b) => (b === undefined ? kept : [...kept, b])),\n ),\n Promise.resolve([]),\n );\n\nexport interface Subscription {\n readonly id: string;\n /**\n * The latest run: its value and the dependencies it took.\n */\n readonly current: () => Run;\n readonly unsubscribe: () => void;\n}\n\ninterface Live {\n readonly id: string;\n readonly subscriber: Subscriber;\n readonly reference: unknown;\n readonly args: unknown;\n run: Run;\n}\n\n/**\n * One client's query set. Its subscriptions share one caller, whose identity\n * `authenticate` replaces, bumping the identity version.\n */\nexport class Subscriber {\n private version = 0;\n\n constructor(\n private readonly registry: Registry,\n private who: Execution.Caller,\n ) {}\n\n get caller(): Execution.Caller {\n return this.who;\n }\n\n get identityVersion(): number {\n return this.version;\n }\n\n subscribe(reference: unknown, args?: unknown): Promise<Subscription> {\n return this.registry.subscribe(this, reference, args ?? {});\n }\n\n /**\n * Replace the caller's identity, or sign it out with `null`. Each\n * subscription that read the identity is stale until the next refresh.\n */\n authenticate(identity: Partial<UserIdentity> | null) {\n this.who =\n identity === null\n ? { ...this.who, identity: null, token: null }\n : Execution.withIdentity(this.who, identity);\n this.version++;\n }\n}\n\n/**\n * Every live subscription on one world. A commit or an identity change makes a\n * subscription stale. `refresh` reruns the stale ones, and `reference` reruns\n * every one from scratch to check that none was missed.\n */\nexport class Registry {\n private readonly live = new Map<string, Live>();\n private nextId = 1;\n\n constructor(private readonly world: Execution.World) {}\n\n subscriber(caller: Execution.Caller): Subscriber {\n return new Subscriber(this, caller);\n }\n\n subscribe(\n subscriber: Subscriber,\n reference: unknown,\n args: unknown,\n ): Promise<Subscription> {\n return this.run(subscriber, reference, args, { journal: null }).then(\n (run) => {\n const live: Live = {\n id: `subscription-${this.nextId++}`,\n subscriber,\n reference,\n args,\n run,\n };\n this.live.set(live.id, live);\n return {\n id: live.id,\n current: () => live.run,\n unsubscribe: () => {\n this.live.delete(live.id);\n },\n };\n },\n );\n }\n\n /**\n * Rerun each stale subscription at the latest commit and the kernel's time\n * now, oldest first, until none is stale. Lists each one whose value\n * changed.\n */\n refresh(): Promise<ReadonlyArray<Transition>> {\n const stale = Array.from(this.live.values()).flatMap((live) => {\n const because = invalidation(\n live.run,\n this.world.state,\n live.subscriber.identityVersion,\n );\n return because === undefined ? [] : [{ live, because }];\n });\n if (stale.length === 0) return Promise.resolve([]);\n return inTurn(stale, ({ live, because }) =>\n this.run(live.subscriber, live.reference, live.args, {\n journal: live.run.journal,\n }).then((run): Transition | undefined => {\n if (this.live.get(live.id) !== live) return undefined;\n const from = live.run.outcome;\n live.run = run;\n return sameOutcome(from, run.outcome)\n ? undefined\n : { subscription: live.id, from, to: run.outcome, because };\n }),\n ).then((transitions) =>\n this.refresh().then((later) => [...transitions, ...later]),\n );\n }\n\n /**\n * Rerun every subscription's query from scratch, at the latest commit and\n * under its subscriber's identity now, and list each whose value differs. A\n * paginated run keeps its page end.\n */\n reference(): Promise<ReadonlyArray<Disagreement>> {\n return inTurn(Array.from(this.live.values()), (live) =>\n this.rerun(live).then(({ outcome }): Disagreement | undefined =>\n sameOutcome(live.run.outcome, outcome)\n ? undefined\n : {\n subscription: live.id,\n incremental: live.run.outcome,\n reference: outcome,\n },\n ),\n );\n }\n\n /**\n * A full rerun of `live`'s query. A run that read the time first reruns at\n * that time, since the clock alone never invalidates it. When that rerun's\n * own reads show the run was owed a rerun, it reruns again at the kernel's\n * time now, as `refresh` would. The rerun was owed when a commit since the\n * run changed something the rerun read, or the rerun read an identity that\n * has changed since. The check reads neither the run's recorded reads nor\n * `invalidation`, so a dependency `refresh` missed still disagrees.\n */\n private rerun(live: Live): Promise<Run> {\n const { run } = live;\n const now = () =>\n this.run(live.subscriber, live.reference, live.args, {\n journal: run.journal,\n });\n if (run.time === null) return now();\n return this.run(live.subscriber, live.reference, live.args, {\n journal: run.journal,\n time: run.time,\n }).then((atRunTime) =>\n Kernel.conflict(this.world.state, run.snapshot, atRunTime.reads) !==\n undefined ||\n (atRunTime.observedIdentity &&\n atRunTime.identityVersion !== run.identityVersion)\n ? now()\n : atRunTime,\n );\n }\n\n private run(\n subscriber: Subscriber,\n reference: unknown,\n args: unknown,\n pinned: { readonly time?: number; readonly journal: string | null },\n ): Promise<Run> {\n const identityVersion = subscriber.identityVersion;\n return this.world\n .subscribed(subscriber.caller, reference, args, pinned)\n .then((run) => ({ ...run, identityVersion }));\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Subscriptions.js","names":["Kernel.conflict","Execution.withIdentity"],"sources":["../../src/internal/Subscriptions.ts"],"sourcesContent":["import type { UserIdentity } from \"convex/server\";\nimport { compareValues, convexToJson, type Value } from \"convex/values\";\nimport * as Execution from \"./Execution.js\";\nimport * as Kernel from \"./Kernel.js\";\n\n/**\n * One run of a subscribed query, and the identity version its subscriber had\n * when it ran.\n */\nexport interface Run extends Execution.QueryRun {\n readonly identityVersion: number;\n}\n\n/**\n * Why a run no longer shows what its query answers now.\n */\nexport type Invalidation =\n | { readonly kind: \"commit\"; readonly change: Kernel.Change }\n | {\n readonly kind: \"identity\";\n readonly from: number;\n readonly to: number;\n };\n\n/**\n * Why `run` is stale, if it is: a commit since its snapshot changed something\n * it read, as an attempt's read set conflicts, or it read its subscriber's\n * identity and that identity has changed. The clock never makes a run stale.\n * The binary does not rerun a query because time passed, so a run keeps the\n * time it read until something else invalidates it.\n */\nexport const invalidation = (\n run: Run,\n state: Kernel.State,\n identityVersion: number,\n): Invalidation | undefined => {\n const change = Kernel.conflict(state, run.snapshot, run.reads);\n if (change !== undefined) return { kind: \"commit\", change };\n if (run.observedIdentity && run.identityVersion !== identityVersion) {\n return { kind: \"identity\", from: run.identityVersion, to: identityVersion };\n }\n return undefined;\n};\n\n/**\n * A thrown value as JSON: a Convex value as Convex writes it, and anything else\n * as its string, beside its type so `\"1\"` and `1` differ.\n */\nconst thrownKey = (thrown: unknown): unknown => {\n try {\n return [typeof thrown, convexToJson(thrown as Value)];\n } catch {\n return [typeof thrown, String(thrown)];\n }\n};\n\nconst errorKey = (error: unknown): string =>\n JSON.stringify(\n error instanceof Error\n ? [\n \"error\",\n error.name,\n error.message,\n thrownKey((error as { readonly data?: unknown }).data),\n ]\n : [\"thrown\", thrownKey(error)],\n );\n\n/**\n * Whether two outcomes show a client the same thing: equal values, failures\n * with errors of the same name, message and data, or failures that threw equal\n * bare values.\n */\nexport const sameOutcome = (\n a: Execution.Outcome,\n b: Execution.Outcome,\n): boolean =>\n a._tag === \"returned\"\n ? b._tag === \"returned\" && compareValues(a.value, b.value) === 0\n : b._tag === \"failed\" && errorKey(a.error) === errorKey(b.error);\n\n/**\n * A subscription's value changed when a refresh reran it.\n */\nexport interface Transition {\n readonly subscription: string;\n readonly from: Execution.Outcome;\n readonly to: Execution.Outcome;\n readonly because: Invalidation;\n}\n\n/**\n * A subscription whose value differs from what a full rerun of its query\n * answers.\n */\nexport interface Disagreement {\n readonly subscription: string;\n readonly incremental: Execution.Outcome;\n readonly reference: Execution.Outcome;\n}\n\n/**\n * Run `f` over `items` one at a time, in order, and keep what it answers.\n */\nconst inTurn = <A, B>(\n items: ReadonlyArray<A>,\n f: (item: A) => Promise<B | undefined>,\n): Promise<Array<B>> =>\n items.reduce<Promise<Array<B>>>(\n (previous, item) =>\n previous.then((kept) =>\n f(item).then((b) => (b === undefined ? kept : [...kept, b])),\n ),\n Promise.resolve([]),\n );\n\nexport interface Subscription {\n readonly id: string;\n /**\n * The latest run: its value and the dependencies it took.\n */\n readonly current: () => Run;\n readonly unsubscribe: () => void;\n}\n\ninterface Live {\n readonly id: string;\n readonly subscriber: Subscriber;\n readonly reference: unknown;\n readonly args: unknown;\n run: Run;\n}\n\n/**\n * One client's query set. Its subscriptions share one caller, whose identity\n * `authenticate` replaces, bumping the identity version.\n */\nexport class Subscriber {\n private version: number;\n\n constructor(\n private readonly registry: Registry,\n private who: Execution.Caller,\n ) {\n this.version = registry.identityEpoch();\n }\n\n get caller(): Execution.Caller {\n return this.who;\n }\n\n get identityVersion(): number {\n return this.version;\n }\n\n subscribe(reference: unknown, args?: unknown): Promise<Subscription> {\n return this.registry.subscribe(this, reference, args ?? {});\n }\n\n /**\n * Replace the caller's identity, or sign it out with `null`. Each\n * subscription that read the identity is stale until the next refresh.\n */\n authenticate(identity: Partial<UserIdentity> | null) {\n this.who =\n identity === null\n ? { ...this.who, identity: null, token: null }\n : Execution.withIdentity(this.who, identity);\n this.version = this.registry.nextIdentityEpoch();\n }\n}\n\n/**\n * Every live subscription on one world. A commit or an identity change makes a\n * subscription stale. `refresh` reruns the stale ones, and `reference` reruns\n * every one from scratch to check that none was missed.\n */\nexport class Registry {\n private readonly live = new Map<string, Live>();\n private nextId = 1;\n\n constructor(private readonly world: Execution.World) {}\n\n subscriber(caller: Execution.Caller): Subscriber {\n this.world.registerSubscriber();\n return new Subscriber(this, caller);\n }\n\n /**\n * The reference a live subscription runs, for marking its diffs by the shape\n * of what its query returns.\n */\n referenceOf(subscription: string): unknown {\n return this.live.get(subscription)?.reference;\n }\n\n /**\n * The world's identity epoch, where a new subscriber's version starts.\n */\n identityEpoch(): number {\n return this.world.currentIdentityEpoch();\n }\n\n /**\n * Advance the world's identity epoch for a subscriber whose identity changed.\n */\n nextIdentityEpoch(): number {\n return this.world.nextIdentityEpoch();\n }\n\n subscribe(\n subscriber: Subscriber,\n reference: unknown,\n args: unknown,\n ): Promise<Subscription> {\n return this.run(subscriber, reference, args, { journal: null }).then(\n (run) => {\n const live: Live = {\n id: `subscription-${this.nextId++}`,\n subscriber,\n reference,\n args,\n run,\n };\n this.live.set(live.id, live);\n return {\n id: live.id,\n // The run's value is the client's own, so it stays raw. Its read\n // records are the kernel's, so they leave projected.\n current: () => ({\n ...live.run,\n reads: this.world.projectReads(live.run.reads),\n }),\n unsubscribe: () => {\n this.live.delete(live.id);\n },\n };\n },\n );\n }\n\n /**\n * Rerun each stale subscription at the latest commit and the kernel's time\n * now, oldest first, until none is stale. Lists each one whose value\n * changed.\n */\n refresh(): Promise<ReadonlyArray<Transition>> {\n const stale = Array.from(this.live.values()).flatMap((live) => {\n const because = invalidation(\n live.run,\n this.world.state,\n live.subscriber.identityVersion,\n );\n return because === undefined ? [] : [{ live, because }];\n });\n if (stale.length === 0) return Promise.resolve([]);\n return inTurn(stale, ({ live, because }) =>\n this.run(live.subscriber, live.reference, live.args, {\n journal: live.run.journal,\n }).then((run): Transition | undefined => {\n if (this.live.get(live.id) !== live) return undefined;\n const from = live.run.outcome;\n live.run = run;\n return sameOutcome(from, run.outcome)\n ? undefined\n : { subscription: live.id, from, to: run.outcome, because };\n }),\n ).then((transitions) =>\n this.refresh().then((later) => [...transitions, ...later]),\n );\n }\n\n /**\n * Rerun every subscription's query from scratch, at the latest commit and\n * under its subscriber's identity now, and list each whose value differs. A\n * paginated run keeps its page end.\n */\n reference(): Promise<ReadonlyArray<Disagreement>> {\n return inTurn(Array.from(this.live.values()), (live) =>\n this.rerun(live).then(({ outcome }): Disagreement | undefined =>\n sameOutcome(live.run.outcome, outcome)\n ? undefined\n : {\n subscription: live.id,\n incremental: live.run.outcome,\n reference: outcome,\n },\n ),\n );\n }\n\n /**\n * A full rerun of `live`'s query. A run that read the time first reruns at\n * that time, since the clock alone never invalidates it. When that rerun's\n * own reads show the run was owed a rerun, it reruns again at the kernel's\n * time now, as `refresh` would. The rerun was owed when a commit since the\n * run changed something the rerun read, or the rerun read an identity that\n * has changed since. The check reads neither the run's recorded reads nor\n * `invalidation`, so a dependency `refresh` missed still disagrees.\n */\n private rerun(live: Live): Promise<Run> {\n const { run } = live;\n const now = () =>\n this.run(live.subscriber, live.reference, live.args, {\n journal: run.journal,\n });\n if (run.time === null) return now();\n return this.run(live.subscriber, live.reference, live.args, {\n journal: run.journal,\n time: run.time,\n }).then((atRunTime) =>\n Kernel.conflict(this.world.state, run.snapshot, atRunTime.reads) !==\n undefined ||\n (atRunTime.observedIdentity &&\n atRunTime.identityVersion !== run.identityVersion)\n ? now()\n : atRunTime,\n );\n }\n\n private run(\n subscriber: Subscriber,\n reference: unknown,\n args: unknown,\n pinned: { readonly time?: number; readonly journal: string | null },\n ): Promise<Run> {\n const identityVersion = subscriber.identityVersion;\n return this.world\n .subscribed(subscriber.caller, reference, args, pinned)\n .then((run) => ({ ...run, identityVersion }));\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@gaganref/confect-test",
3
- "version": "10.3.1-fork.92",
3
+ "version": "10.3.1-fork.93",
4
4
  "peerDependencies": {
5
- "@confect/core": "npm:@gaganref/confect-core@10.3.1-fork.92",
6
- "@confect/server": "npm:@gaganref/confect-server@10.3.1-fork.92",
5
+ "@confect/core": "npm:@gaganref/confect-core@10.3.1-fork.93",
6
+ "@confect/server": "npm:@gaganref/confect-server@10.3.1-fork.93",
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"
@@ -19,6 +19,7 @@ import * as Layer from "effect/Layer";
19
19
  import * as Schema from "effect/Schema";
20
20
  import { convexTest as kernelTest } from "./internal/ConvexTest.js";
21
21
  import * as MountedHttp from "./internal/MountedHttp.js";
22
+ import * as Redaction from "./internal/Redaction.js";
22
23
 
23
24
  declare const process: { env: Record<string, string | undefined> };
24
25
 
@@ -68,6 +69,36 @@ export type TargetOptions = {
68
69
  readonly target?: Target;
69
70
  };
70
71
 
72
+ /**
73
+ * Which values a kernel-target layer treats as secret, by component path, then
74
+ * table name or `module:export`: each entry a thunk of the Confect schema whose
75
+ * `Schema.Redacted` nodes mark the secrets. A value under one reaches the
76
+ * kernel's artifacts (snapshots, failure reports, diffs) only as a marker. The
77
+ * layer derives the app's own tables from its database schema, so this adds
78
+ * function arguments and returns and component tables. Codegen fills it from
79
+ * the Confect specs as `secrets` in the generated registry, which replaces a
80
+ * hand-written one.
81
+ */
82
+ export type SecretPolicy = {
83
+ readonly tables?: Readonly<
84
+ Record<string, Readonly<Record<string, () => Schema.Top>>>
85
+ >;
86
+ readonly functions?: Readonly<
87
+ Record<
88
+ string,
89
+ Readonly<
90
+ Record<
91
+ string,
92
+ {
93
+ readonly args?: () => Schema.Top;
94
+ readonly returns?: () => Schema.Top;
95
+ }
96
+ >
97
+ >
98
+ >
99
+ >;
100
+ };
101
+
71
102
  export type LayerOptions = TargetOptions & {
72
103
  /**
73
104
  * Register component kits synchronously before the test service is available.
@@ -77,6 +108,10 @@ export type LayerOptions = TargetOptions & {
77
108
  * Explicit HTTP mounts. The test layer does not evaluate convex.config.ts.
78
109
  */
79
110
  readonly httpMounts?: ReadonlyArray<HttpMount>;
111
+ /**
112
+ * The secret policy, read by the kernel target only.
113
+ */
114
+ readonly secrets?: SecretPolicy;
80
115
  };
81
116
 
82
117
  export type TestConfectWithoutIdentity<
@@ -383,6 +418,11 @@ export type Registry<DatabaseSchema_ extends DatabaseSchema.AnyWithProps> = {
383
418
  readonly convexSchema: SchemaDefinition<GenericSchema, true>;
384
419
  readonly modules: Record<string, () => Promise<any>>;
385
420
  readonly components: Readonly<Record<string, ComponentKit>>;
421
+ /**
422
+ * The secret policy codegen derives from the Confect specs, read by the
423
+ * kernel target only.
424
+ */
425
+ readonly secrets?: SecretPolicy;
386
426
  };
387
427
 
388
428
  /**
@@ -442,6 +482,7 @@ export function layer<DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(
442
482
  registry.modules,
443
483
  {
444
484
  ...targetOf(third as TargetOptions | undefined),
485
+ ...secretsOf(registry),
445
486
  httpMounts: mounts.flatMap(({ name, httpPrefix }) =>
446
487
  httpPrefix === undefined ? [] : [{ componentPath: name, httpPrefix }],
447
488
  ),
@@ -464,6 +505,7 @@ export function layer<DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(
464
505
  const app = appOrSchema as DeclaredApp<DatabaseSchema_>;
465
506
  return schemaLayer(app.schema, app.convexSchema, app.modules, {
466
507
  ...targetOf(second as TargetOptions | undefined),
508
+ ...secretsOf(app),
467
509
  httpMounts: app.httpMounts,
468
510
  register: (registerComponent) => {
469
511
  for (const [name, component] of Object.entries(app.components)) {
@@ -483,6 +525,11 @@ export function layer<DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(
483
525
  const targetOf = (options: TargetOptions | undefined): TargetOptions =>
484
526
  options?.target === undefined ? {} : { target: options.target };
485
527
 
528
+ const secretsOf = (registry: {
529
+ readonly secrets?: SecretPolicy;
530
+ }): Pick<LayerOptions, "secrets"> =>
531
+ registry.secrets === undefined ? {} : { secrets: registry.secrets };
532
+
486
533
  const schemaLayer = <DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(
487
534
  databaseSchema: DatabaseSchema_,
488
535
  convexSchemaDefinition: SchemaDefinition<GenericSchema, true>,
@@ -492,10 +539,22 @@ const schemaLayer = <DatabaseSchema_ extends DatabaseSchema.AnyWithProps>(
492
539
  Layer.sync(TestConfect<DatabaseSchema_>(), () => {
493
540
  process.env.CONVEX_CLOUD_URL ??= "https://some-deployment.convex.cloud";
494
541
  process.env.CONVEX_SITE_URL ??= "https://some.convex.site";
495
- const testConvex = (options.target === "kernel" ? kernelTest : convexTest)(
496
- convexSchemaDefinition,
497
- modules,
498
- );
542
+ // On the kernel, a value under a secret node of the app's table schemas,
543
+ // or of the policy's functions and component tables, reaches every
544
+ // artifact as a marker.
545
+ const testConvex =
546
+ options.target === "kernel"
547
+ ? kernelTest({
548
+ schema: convexSchemaDefinition,
549
+ modules,
550
+ redaction: Redaction.policy(
551
+ Redaction.merge(
552
+ { tables: { "": Redaction.tablesOf(databaseSchema) } },
553
+ options.secrets ?? {},
554
+ ),
555
+ ),
556
+ })
557
+ : convexTest(convexSchemaDefinition, modules);
499
558
  // convex-test takes three arguments and ignores the options. The kernel
500
559
  // reads `standing`.
501
560
  const registerComponent =
@@ -5,6 +5,8 @@ import type {
5
5
  } from "convex/server";
6
6
  import { componentsGeneric } from "convex/server";
7
7
  import * as Execution from "./Execution.js";
8
+ import type * as Invocation from "./Invocation.js";
9
+ import type * as Redaction from "./Redaction.js";
8
10
  import type * as Kernel from "./Kernel.js";
9
11
  import * as KernelRefusal from "./KernelRefusal.js";
10
12
  import * as Subscriptions from "./Subscriptions.js";
@@ -66,6 +68,29 @@ export interface KernelControls {
66
68
  advanceTimers?: () => void,
67
69
  maxIterations?: number,
68
70
  ) => Promise<void>;
71
+ /**
72
+ * The committed state as snapshot bytes, bound to their SHA-256 digest. Two
73
+ * exports of the same state are the same bytes.
74
+ */
75
+ readonly exportSnapshot: () => Uint8Array;
76
+ /**
77
+ * Replace a fresh backend's state with a snapshot's. Each job's world
78
+ * resolves by name among `worlds`. Tampered bytes refuse with
79
+ * `SnapshotDigestMismatch`.
80
+ */
81
+ readonly restoreSnapshot: (
82
+ bytes: Uint8Array,
83
+ options?: {
84
+ readonly worlds?: ReadonlyArray<Invocation.World>;
85
+ readonly secrets?: Redaction.Secrets;
86
+ },
87
+ ) => Promise<void>;
88
+ /**
89
+ * A copy of the raw secrets the backend has seen under a secret schema node.
90
+ * A snapshot marks each one, and a restore given this copy puts them back. It
91
+ * lives in memory and is never written to an artifact.
92
+ */
93
+ readonly secrets: () => Redaction.Secrets;
69
94
  }
70
95
 
71
96
  /**
@@ -92,42 +117,109 @@ const accessor = (
92
117
  world: Execution.World,
93
118
  subscriptions: Subscriptions.Registry,
94
119
  caller: Execution.Caller,
95
- ): any => ({
96
- query: (reference: any, args?: any) => world.query(caller, reference, args),
97
- mutation: (reference: any, args?: any) =>
98
- world.mutation(caller, reference, args),
99
- action: (reference: any, args?: any) => world.action(caller, reference, args),
100
- prepare: (reference: any, args?: any) =>
101
- world.prepare(caller, reference, args),
102
- apply: (id: string) => world.apply(id),
103
- held: () => world.held(),
104
- subscriber: () => subscriptions.subscriber(caller),
105
- refresh: () => subscriptions.refresh(),
106
- reference: () => subscriptions.reference(),
107
- run: (handler: (ctx: any) => any) => world.run(caller, "", handler),
108
- runInComponent: (componentPath: string, handler: (ctx: any) => any) =>
109
- world.run(caller, componentPath, handler),
110
- withIdentity: (identity: Partial<UserIdentity>) =>
111
- accessor(world, subscriptions, Execution.withIdentity(caller, identity)),
112
- withRequestMetadata: (metadata: {
113
- readonly ip?: string | null;
114
- readonly userAgent?: string | null;
115
- }) =>
116
- accessor(world, subscriptions, {
117
- ...caller,
118
- ip: metadata.ip ?? null,
119
- userAgent: metadata.userAgent ?? null,
120
- }),
121
- fetch: () => Promise.reject(KernelRefusal.rowPending("HTTP routes", 6)),
122
- jwks: () => Promise.reject(KernelRefusal.rowPending("JWT verification", 6)),
123
- advance: (ms: number) => world.advance(ms),
124
- jobs: () => world.jobs(),
125
- finishInProgressScheduledFunctions: () => world.advance(0),
126
- finishAllScheduledFunctions: (
127
- _advanceTimers?: () => void,
128
- maxIterations?: number,
129
- ) => world.drain(maxIterations),
130
- });
120
+ ): any => {
121
+ // Every failure the kernel hands back is a report a test runner prints, so
122
+ // each leaves through the world's redaction.
123
+ const reported = <A>(run: () => Promise<A>): Promise<A> =>
124
+ run().catch((error: unknown) => {
125
+ throw world.redactFailure(error);
126
+ });
127
+ // A diff names its subscription; its outcomes are marked by the shape of
128
+ // what that subscription's query returns.
129
+ const returnsOf = (subscription: string) =>
130
+ world.functionPathOf(subscriptions.referenceOf(subscription));
131
+ return {
132
+ query: (reference: any, args?: any) =>
133
+ reported(() => world.query(caller, reference, args)),
134
+ mutation: (reference: any, args?: any) =>
135
+ reported(() => world.mutation(caller, reference, args)),
136
+ action: (reference: any, args?: any) =>
137
+ reported(() => world.action(caller, reference, args)),
138
+ prepare: (reference: any, args?: any) =>
139
+ reported(() => world.prepare(caller, reference, args)).then((view) =>
140
+ world.projectHeld(view),
141
+ ),
142
+ apply: (id: string) => {
143
+ // The attempt leaves `held` once applied, so its function is read first.
144
+ const functionPath = world
145
+ .held()
146
+ .find((view) => view.id === id)?.functionPath;
147
+ return reported(() => world.apply(id)).then((applied) =>
148
+ world.projectApplied(applied, functionPath),
149
+ );
150
+ },
151
+ held: () => world.held().map((view) => world.projectHeld(view)),
152
+ subscriber: () => subscriptions.subscriber(caller),
153
+ refresh: () =>
154
+ reported(() => subscriptions.refresh()).then((transitions) =>
155
+ transitions.map((transition): Subscriptions.Transition => ({
156
+ ...transition,
157
+ from: world.redactOutcome(
158
+ transition.from,
159
+ returnsOf(transition.subscription),
160
+ ),
161
+ to: world.redactOutcome(
162
+ transition.to,
163
+ returnsOf(transition.subscription),
164
+ ),
165
+ because:
166
+ transition.because.kind === "commit"
167
+ ? {
168
+ kind: "commit",
169
+ change: world.redactChange(transition.because.change),
170
+ }
171
+ : transition.because,
172
+ })),
173
+ ),
174
+ reference: () =>
175
+ reported(() => subscriptions.reference()).then((disagreements) =>
176
+ disagreements.map((disagreement): Subscriptions.Disagreement => ({
177
+ ...disagreement,
178
+ incremental: world.redactOutcome(
179
+ disagreement.incremental,
180
+ returnsOf(disagreement.subscription),
181
+ ),
182
+ reference: world.redactOutcome(
183
+ disagreement.reference,
184
+ returnsOf(disagreement.subscription),
185
+ ),
186
+ })),
187
+ ),
188
+ run: (handler: (ctx: any) => any) =>
189
+ reported(() => world.run(caller, "", handler)),
190
+ runInComponent: (componentPath: string, handler: (ctx: any) => any) =>
191
+ reported(() => world.run(caller, componentPath, handler)),
192
+ withIdentity: (identity: Partial<UserIdentity>) =>
193
+ accessor(world, subscriptions, Execution.withIdentity(caller, identity)),
194
+ withRequestMetadata: (metadata: {
195
+ readonly ip?: string | null;
196
+ readonly userAgent?: string | null;
197
+ }) =>
198
+ accessor(world, subscriptions, {
199
+ ...caller,
200
+ ip: metadata.ip ?? null,
201
+ userAgent: metadata.userAgent ?? null,
202
+ }),
203
+ fetch: () => Promise.reject(KernelRefusal.rowPending("HTTP routes", 6)),
204
+ jwks: () => Promise.reject(KernelRefusal.rowPending("JWT verification", 6)),
205
+ advance: (ms: number) => reported(() => world.advance(ms)),
206
+ jobs: () => world.jobs(),
207
+ finishInProgressScheduledFunctions: () => reported(() => world.advance(0)),
208
+ finishAllScheduledFunctions: (
209
+ _advanceTimers?: () => void,
210
+ maxIterations?: number,
211
+ ) => reported(() => world.drain(maxIterations)),
212
+ exportSnapshot: () => world.exportSnapshot(),
213
+ restoreSnapshot: (
214
+ bytes: Uint8Array,
215
+ options?: {
216
+ readonly worlds?: ReadonlyArray<Invocation.World>;
217
+ readonly secrets?: Redaction.Secrets;
218
+ },
219
+ ) => world.restoreSnapshot(bytes, options),
220
+ secrets: () => world.keptSecrets(),
221
+ };
222
+ };
131
223
 
132
224
  /**
133
225
  * The test kernel under convex-test's `convexTest` signature. The fork's kernel
@@ -140,7 +232,9 @@ const accessor = (
140
232
  * attempt 5. `runtime` supplies the clock and commit timestamps. Scheduled
141
233
  * functions run on the kernel's clock, never on a host timer. A subscriber's
142
234
  * subscriptions go stale when a commit changes what they read or the identity
143
- * they read changes, and `refresh` reruns them.
235
+ * they read changes, and `refresh` reruns them. `redaction` names the secret
236
+ * schema nodes of tables and functions: a value under one reaches a snapshot, a
237
+ * failure report or a diff only as a marker.
144
238
  */
145
239
  export function convexTest(
146
240
  schemaOrOptions?:
@@ -150,11 +244,13 @@ export function convexTest(
150
244
  readonly modules?: Modules;
151
245
  readonly transactionLimits?: unknown;
152
246
  readonly runtime?: Kernel.KernelRuntime;
247
+ readonly redaction?: Redaction.Policy;
153
248
  },
154
249
  modules?: Modules,
155
250
  ): any {
156
251
  let schema: SchemaDefinition<GenericSchema, boolean> | undefined;
157
252
  let runtime: Kernel.KernelRuntime | undefined;
253
+ let redaction: Redaction.Policy | undefined;
158
254
  if (isSchemaDefinition(schemaOrOptions)) {
159
255
  schema = schemaOrOptions;
160
256
  } else if (schemaOrOptions !== undefined) {
@@ -164,13 +260,14 @@ export function convexTest(
164
260
  schema = schemaOrOptions.schema;
165
261
  modules = schemaOrOptions.modules;
166
262
  runtime = schemaOrOptions.runtime;
263
+ redaction = schemaOrOptions.redaction;
167
264
  }
168
265
  if (modules === undefined) {
169
266
  throw new Error(
170
267
  "The test kernel needs the modules map: pass `import.meta.glob(...)` as the second argument.",
171
268
  );
172
269
  }
173
- const world = new Execution.World(schema, modules, runtime);
270
+ const world = new Execution.World(schema, modules, runtime, redaction);
174
271
  return {
175
272
  ...accessor(world, new Subscriptions.Registry(world), Execution.anonymous),
176
273
  registerComponent: (