@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.
- package/dist/TestConfect.d.ts +26 -0
- package/dist/TestConfect.d.ts.map +1 -1
- package/dist/TestConfect.js +9 -1
- package/dist/TestConfect.js.map +1 -1
- package/dist/internal/ConvexTest.d.ts +26 -1
- package/dist/internal/ConvexTest.d.ts.map +1 -1
- package/dist/internal/ConvexTest.js +55 -27
- package/dist/internal/ConvexTest.js.map +1 -1
- package/dist/internal/Execution.d.ts +138 -1
- package/dist/internal/Execution.d.ts.map +1 -1
- package/dist/internal/Execution.js +595 -182
- package/dist/internal/Execution.js.map +1 -1
- package/dist/internal/KernelRefusal.d.ts +134 -0
- package/dist/internal/KernelRefusal.d.ts.map +1 -1
- package/dist/internal/KernelRefusal.js +134 -1
- package/dist/internal/KernelRefusal.js.map +1 -1
- package/dist/internal/Redaction.d.ts +142 -0
- package/dist/internal/Redaction.d.ts.map +1 -0
- package/dist/internal/Redaction.js +448 -0
- package/dist/internal/Redaction.js.map +1 -0
- package/dist/internal/Snapshot.d.ts +298 -0
- package/dist/internal/Snapshot.d.ts.map +1 -0
- package/dist/internal/Snapshot.js +532 -0
- package/dist/internal/Snapshot.js.map +1 -0
- package/dist/internal/Subscriptions.d.ts +13 -0
- package/dist/internal/Subscriptions.d.ts.map +1 -1
- package/dist/internal/Subscriptions.js +27 -3
- package/dist/internal/Subscriptions.js.map +1 -1
- package/package.json +3 -3
- package/src/TestConfect.ts +63 -4
- package/src/internal/ConvexTest.ts +135 -38
- package/src/internal/Execution.ts +886 -287
- package/src/internal/KernelRefusal.ts +160 -0
- package/src/internal/Redaction.ts +730 -0
- package/src/internal/Snapshot.ts +945 -0
- package/src/internal/Subscriptions.ts +34 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Subscriptions.d.ts","sourceRoot":"","sources":["../../src/internal/Subscriptions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAC;AAC5C,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC;;;GAGG;AACH,MAAM,WAAW,GAAI,SAAQ,SAAS,CAAC,QAAQ;IAC7C,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAA;CAAE,GAC3D;IACE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB,CAAC;AAEN;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,QAClB,GAAG,SACD,MAAM,CAAC,KAAK,mBACF,MAAM,KACtB,YAAY,GAAG,SAOjB,CAAC;AA0BF;;;;GAIG;AACH,eAAO,MAAM,WAAW,MACnB,SAAS,CAAC,OAAO,KACjB,SAAS,CAAC,OAAO,KACnB,OAGiE,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC;IACjC,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC,OAAO,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,OAAO,CAAC;IACxC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC,OAAO,CAAC;CACvC;AAiBD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,IAAI,CAAC;CAClC;AAUD;;;GAGG;AACH,qBAAa,UAAU;IAInB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,GAAG;IAJb,OAAO,CAAC,OAAO,
|
|
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
|
|
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: () =>
|
|
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.
|
|
3
|
+
"version": "10.3.1-fork.93",
|
|
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.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"
|
package/src/TestConfect.ts
CHANGED
|
@@ -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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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: (
|