@gaganref/confect-test 10.3.1-fork.90 → 10.3.1-fork.92
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/internal/Attempt.d.ts +29 -1
- package/dist/internal/Attempt.d.ts.map +1 -1
- package/dist/internal/Attempt.js +45 -1
- package/dist/internal/Attempt.js.map +1 -1
- package/dist/internal/ConvexTest.d.ts +23 -2
- package/dist/internal/ConvexTest.d.ts.map +1 -1
- package/dist/internal/ConvexTest.js +11 -5
- package/dist/internal/ConvexTest.js.map +1 -1
- package/dist/internal/Execution.d.ts +43 -0
- package/dist/internal/Execution.d.ts.map +1 -1
- package/dist/internal/Execution.js +200 -41
- package/dist/internal/Execution.js.map +1 -1
- package/dist/internal/Invocation.d.ts.map +1 -1
- package/dist/internal/Invocation.js.map +1 -1
- package/dist/internal/KernelQuery.d.ts +8 -3
- package/dist/internal/KernelQuery.d.ts.map +1 -1
- package/dist/internal/KernelQuery.js +10 -3
- package/dist/internal/KernelQuery.js.map +1 -1
- package/dist/internal/Metering.d.ts +186 -0
- package/dist/internal/Metering.d.ts.map +1 -0
- package/dist/internal/Metering.js +401 -0
- package/dist/internal/Metering.js.map +1 -0
- package/dist/internal/Subscriptions.d.ts +116 -0
- package/dist/internal/Subscriptions.d.ts.map +1 -0
- package/dist/internal/Subscriptions.js +186 -0
- package/dist/internal/Subscriptions.js.map +1 -0
- package/package.json +3 -3
- package/src/internal/Attempt.ts +52 -0
- package/src/internal/ConvexTest.ts +33 -5
- package/src/internal/Execution.ts +375 -54
- package/src/internal/Invocation.ts +2 -1
- package/src/internal/KernelQuery.ts +17 -6
- package/src/internal/Metering.ts +520 -0
- package/src/internal/Subscriptions.ts +302 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { conflict } from "./Kernel.js";
|
|
2
|
+
import { withIdentity } from "./Execution.js";
|
|
3
|
+
import { compareValues, convexToJson } from "convex/values";
|
|
4
|
+
//#region src/internal/Subscriptions.ts
|
|
5
|
+
/**
|
|
6
|
+
* Why `run` is stale, if it is: a commit since its snapshot changed something
|
|
7
|
+
* it read, as an attempt's read set conflicts, or it read its subscriber's
|
|
8
|
+
* identity and that identity has changed. The clock never makes a run stale.
|
|
9
|
+
* The binary does not rerun a query because time passed, so a run keeps the
|
|
10
|
+
* time it read until something else invalidates it.
|
|
11
|
+
*/
|
|
12
|
+
const invalidation = (run, state, identityVersion) => {
|
|
13
|
+
const change = conflict(state, run.snapshot, run.reads);
|
|
14
|
+
if (change !== void 0) return {
|
|
15
|
+
kind: "commit",
|
|
16
|
+
change
|
|
17
|
+
};
|
|
18
|
+
if (run.observedIdentity && run.identityVersion !== identityVersion) return {
|
|
19
|
+
kind: "identity",
|
|
20
|
+
from: run.identityVersion,
|
|
21
|
+
to: identityVersion
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* A thrown value as JSON: a Convex value as Convex writes it, and anything else
|
|
26
|
+
* as its string, beside its type so `"1"` and `1` differ.
|
|
27
|
+
*/
|
|
28
|
+
const thrownKey = (thrown) => {
|
|
29
|
+
try {
|
|
30
|
+
return [typeof thrown, convexToJson(thrown)];
|
|
31
|
+
} catch {
|
|
32
|
+
return [typeof thrown, String(thrown)];
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const errorKey = (error) => JSON.stringify(error instanceof Error ? [
|
|
36
|
+
"error",
|
|
37
|
+
error.name,
|
|
38
|
+
error.message,
|
|
39
|
+
thrownKey(error.data)
|
|
40
|
+
] : ["thrown", thrownKey(error)]);
|
|
41
|
+
/**
|
|
42
|
+
* Whether two outcomes show a client the same thing: equal values, failures
|
|
43
|
+
* with errors of the same name, message and data, or failures that threw equal
|
|
44
|
+
* bare values.
|
|
45
|
+
*/
|
|
46
|
+
const sameOutcome = (a, b) => a._tag === "returned" ? b._tag === "returned" && compareValues(a.value, b.value) === 0 : b._tag === "failed" && errorKey(a.error) === errorKey(b.error);
|
|
47
|
+
/**
|
|
48
|
+
* Run `f` over `items` one at a time, in order, and keep what it answers.
|
|
49
|
+
*/
|
|
50
|
+
const inTurn = (items, f) => items.reduce((previous, item) => previous.then((kept) => f(item).then((b) => b === void 0 ? kept : [...kept, b])), Promise.resolve([]));
|
|
51
|
+
/**
|
|
52
|
+
* One client's query set. Its subscriptions share one caller, whose identity
|
|
53
|
+
* `authenticate` replaces, bumping the identity version.
|
|
54
|
+
*/
|
|
55
|
+
var Subscriber = class {
|
|
56
|
+
registry;
|
|
57
|
+
who;
|
|
58
|
+
version = 0;
|
|
59
|
+
constructor(registry, who) {
|
|
60
|
+
this.registry = registry;
|
|
61
|
+
this.who = who;
|
|
62
|
+
}
|
|
63
|
+
get caller() {
|
|
64
|
+
return this.who;
|
|
65
|
+
}
|
|
66
|
+
get identityVersion() {
|
|
67
|
+
return this.version;
|
|
68
|
+
}
|
|
69
|
+
subscribe(reference, args) {
|
|
70
|
+
return this.registry.subscribe(this, reference, args ?? {});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Replace the caller's identity, or sign it out with `null`. Each
|
|
74
|
+
* subscription that read the identity is stale until the next refresh.
|
|
75
|
+
*/
|
|
76
|
+
authenticate(identity) {
|
|
77
|
+
this.who = identity === null ? {
|
|
78
|
+
...this.who,
|
|
79
|
+
identity: null,
|
|
80
|
+
token: null
|
|
81
|
+
} : withIdentity(this.who, identity);
|
|
82
|
+
this.version++;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Every live subscription on one world. A commit or an identity change makes a
|
|
87
|
+
* subscription stale. `refresh` reruns the stale ones, and `reference` reruns
|
|
88
|
+
* every one from scratch to check that none was missed.
|
|
89
|
+
*/
|
|
90
|
+
var Registry = class {
|
|
91
|
+
world;
|
|
92
|
+
live = /* @__PURE__ */ new Map();
|
|
93
|
+
nextId = 1;
|
|
94
|
+
constructor(world) {
|
|
95
|
+
this.world = world;
|
|
96
|
+
}
|
|
97
|
+
subscriber(caller) {
|
|
98
|
+
return new Subscriber(this, caller);
|
|
99
|
+
}
|
|
100
|
+
subscribe(subscriber, reference, args) {
|
|
101
|
+
return this.run(subscriber, reference, args, { journal: null }).then((run) => {
|
|
102
|
+
const live = {
|
|
103
|
+
id: `subscription-${this.nextId++}`,
|
|
104
|
+
subscriber,
|
|
105
|
+
reference,
|
|
106
|
+
args,
|
|
107
|
+
run
|
|
108
|
+
};
|
|
109
|
+
this.live.set(live.id, live);
|
|
110
|
+
return {
|
|
111
|
+
id: live.id,
|
|
112
|
+
current: () => live.run,
|
|
113
|
+
unsubscribe: () => {
|
|
114
|
+
this.live.delete(live.id);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Rerun each stale subscription at the latest commit and the kernel's time
|
|
121
|
+
* now, oldest first, until none is stale. Lists each one whose value
|
|
122
|
+
* changed.
|
|
123
|
+
*/
|
|
124
|
+
refresh() {
|
|
125
|
+
const stale = Array.from(this.live.values()).flatMap((live) => {
|
|
126
|
+
const because = invalidation(live.run, this.world.state, live.subscriber.identityVersion);
|
|
127
|
+
return because === void 0 ? [] : [{
|
|
128
|
+
live,
|
|
129
|
+
because
|
|
130
|
+
}];
|
|
131
|
+
});
|
|
132
|
+
if (stale.length === 0) return Promise.resolve([]);
|
|
133
|
+
return inTurn(stale, ({ live, because }) => this.run(live.subscriber, live.reference, live.args, { journal: live.run.journal }).then((run) => {
|
|
134
|
+
if (this.live.get(live.id) !== live) return void 0;
|
|
135
|
+
const from = live.run.outcome;
|
|
136
|
+
live.run = run;
|
|
137
|
+
return sameOutcome(from, run.outcome) ? void 0 : {
|
|
138
|
+
subscription: live.id,
|
|
139
|
+
from,
|
|
140
|
+
to: run.outcome,
|
|
141
|
+
because
|
|
142
|
+
};
|
|
143
|
+
})).then((transitions) => this.refresh().then((later) => [...transitions, ...later]));
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Rerun every subscription's query from scratch, at the latest commit and
|
|
147
|
+
* under its subscriber's identity now, and list each whose value differs. A
|
|
148
|
+
* paginated run keeps its page end.
|
|
149
|
+
*/
|
|
150
|
+
reference() {
|
|
151
|
+
return inTurn(Array.from(this.live.values()), (live) => this.rerun(live).then(({ outcome }) => sameOutcome(live.run.outcome, outcome) ? void 0 : {
|
|
152
|
+
subscription: live.id,
|
|
153
|
+
incremental: live.run.outcome,
|
|
154
|
+
reference: outcome
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* A full rerun of `live`'s query. A run that read the time first reruns at
|
|
159
|
+
* that time, since the clock alone never invalidates it. When that rerun's
|
|
160
|
+
* own reads show the run was owed a rerun, it reruns again at the kernel's
|
|
161
|
+
* time now, as `refresh` would. The rerun was owed when a commit since the
|
|
162
|
+
* run changed something the rerun read, or the rerun read an identity that
|
|
163
|
+
* has changed since. The check reads neither the run's recorded reads nor
|
|
164
|
+
* `invalidation`, so a dependency `refresh` missed still disagrees.
|
|
165
|
+
*/
|
|
166
|
+
rerun(live) {
|
|
167
|
+
const { run } = live;
|
|
168
|
+
const now = () => this.run(live.subscriber, live.reference, live.args, { journal: run.journal });
|
|
169
|
+
if (run.time === null) return now();
|
|
170
|
+
return this.run(live.subscriber, live.reference, live.args, {
|
|
171
|
+
journal: run.journal,
|
|
172
|
+
time: run.time
|
|
173
|
+
}).then((atRunTime) => conflict(this.world.state, run.snapshot, atRunTime.reads) !== void 0 || atRunTime.observedIdentity && atRunTime.identityVersion !== run.identityVersion ? now() : atRunTime);
|
|
174
|
+
}
|
|
175
|
+
run(subscriber, reference, args, pinned) {
|
|
176
|
+
const identityVersion = subscriber.identityVersion;
|
|
177
|
+
return this.world.subscribed(subscriber.caller, reference, args, pinned).then((run) => ({
|
|
178
|
+
...run,
|
|
179
|
+
identityVersion
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
//#endregion
|
|
184
|
+
export { Registry, Subscriber, invalidation, sameOutcome };
|
|
185
|
+
|
|
186
|
+
//# sourceMappingURL=Subscriptions.js.map
|
|
@@ -0,0 +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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"}
|
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.92",
|
|
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.92",
|
|
6
|
+
"@confect/server": "npm:@gaganref/confect-server@10.3.1-fork.92",
|
|
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/internal/Attempt.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Attempt as AttemptNumber } from "./Invocation.js";
|
|
|
4
4
|
import * as Kernel from "./Kernel.js";
|
|
5
5
|
import type { DocumentId, StoredDocument } from "./KernelValues.js";
|
|
6
6
|
import { tableNameFromId } from "./KernelValues.js";
|
|
7
|
+
import * as Metering from "./Metering.js";
|
|
7
8
|
import type * as ReadSet from "./ReadSet.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -44,19 +45,55 @@ export class KernelAttempt {
|
|
|
44
45
|
private readonly overlay: Overlay = new Map();
|
|
45
46
|
private readonly life: Life = { running: 0, started: 0, open: true };
|
|
46
47
|
private readonly allReads: Array<ReadSet.Read>;
|
|
48
|
+
/**
|
|
49
|
+
* The attempt's transaction-limit accounting. A nested mutation's frame
|
|
50
|
+
* meters into its own child, so its writes merge or drop with it while its
|
|
51
|
+
* reads stay.
|
|
52
|
+
*/
|
|
53
|
+
readonly meter: Metering.Meter;
|
|
54
|
+
/**
|
|
55
|
+
* Whether the attempt asked for its caller's identity, and the time it read.
|
|
56
|
+
* Every frame of the attempt shares them, as it shares the reads.
|
|
57
|
+
*/
|
|
58
|
+
private readonly observed: { identity: boolean; time: number | null };
|
|
47
59
|
|
|
48
60
|
constructor(
|
|
49
61
|
readonly snapshot: Kernel.State,
|
|
50
62
|
readonly number: AttemptNumber = 1,
|
|
51
63
|
private readonly parent?: KernelAttempt,
|
|
64
|
+
meter?: Metering.Meter,
|
|
52
65
|
) {
|
|
53
66
|
this.allReads = parent?.allReads ?? [];
|
|
67
|
+
this.meter = meter ?? parent?.meter.child() ?? new Metering.Meter();
|
|
68
|
+
this.observed = parent?.observed ?? { identity: false, time: null };
|
|
54
69
|
}
|
|
55
70
|
|
|
56
71
|
get reads(): ReadonlyArray<ReadSet.Read> {
|
|
57
72
|
return this.allReads;
|
|
58
73
|
}
|
|
59
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Whether the function asked for its caller's identity.
|
|
77
|
+
*/
|
|
78
|
+
get observedIdentity(): boolean {
|
|
79
|
+
return this.observed.identity;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The pinned time the function read, or `null` when it never read the time.
|
|
84
|
+
*/
|
|
85
|
+
get observedTime(): number | null {
|
|
86
|
+
return this.observed.time;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
observeIdentity() {
|
|
90
|
+
this.observed.identity = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
observeTime(at: number) {
|
|
94
|
+
this.observed.time = at;
|
|
95
|
+
}
|
|
96
|
+
|
|
60
97
|
get closed(): boolean {
|
|
61
98
|
return !this.life.open;
|
|
62
99
|
}
|
|
@@ -151,6 +188,20 @@ export class KernelAttempt {
|
|
|
151
188
|
writes.set(id, doc);
|
|
152
189
|
}
|
|
153
190
|
|
|
191
|
+
/**
|
|
192
|
+
* A query run on this attempt's snapshot rather than its pending writes. It
|
|
193
|
+
* reads and meters on its own, and its meter starts with this attempt's
|
|
194
|
+
* scheduling counters, as the binary's snapshot clone does.
|
|
195
|
+
*/
|
|
196
|
+
staleSnapshot(): KernelAttempt {
|
|
197
|
+
return new KernelAttempt(
|
|
198
|
+
this.snapshot,
|
|
199
|
+
this.number,
|
|
200
|
+
undefined,
|
|
201
|
+
this.meter.forStaleSnapshot(),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
154
205
|
/**
|
|
155
206
|
* A nested mutation begins, in a frame of its own. Dropping a failed one is
|
|
156
207
|
* forgetting it.
|
|
@@ -172,6 +223,7 @@ export class KernelAttempt {
|
|
|
172
223
|
}
|
|
173
224
|
for (const [componentPath, writes] of this.overlay)
|
|
174
225
|
for (const [id, doc] of writes) this.parent.write(componentPath, id, doc);
|
|
226
|
+
this.meter.merge();
|
|
175
227
|
}
|
|
176
228
|
|
|
177
229
|
/**
|
|
@@ -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 * as Subscriptions from "./Subscriptions.js";
|
|
10
11
|
import type * as Scheduling from "./Scheduling.js";
|
|
11
12
|
|
|
12
13
|
type Modules = Record<string, () => Promise<any>>;
|
|
@@ -25,6 +26,24 @@ export interface KernelControls {
|
|
|
25
26
|
) => Promise<Execution.HeldAttempt>;
|
|
26
27
|
readonly apply: (id: string) => Promise<Execution.KernelApply>;
|
|
27
28
|
readonly held: () => ReadonlyArray<Execution.HeldAttempt>;
|
|
29
|
+
readonly withIdentity: (identity: Partial<UserIdentity>) => KernelControls;
|
|
30
|
+
readonly withRequestMetadata: (metadata: {
|
|
31
|
+
readonly ip?: string | null;
|
|
32
|
+
readonly userAgent?: string | null;
|
|
33
|
+
}) => KernelControls;
|
|
34
|
+
/**
|
|
35
|
+
* A client's query set under this accessor's caller.
|
|
36
|
+
*/
|
|
37
|
+
readonly subscriber: () => Subscriptions.Subscriber;
|
|
38
|
+
/**
|
|
39
|
+
* Rerun every stale subscription. Lists each one whose value changed.
|
|
40
|
+
*/
|
|
41
|
+
readonly refresh: () => Promise<ReadonlyArray<Subscriptions.Transition>>;
|
|
42
|
+
/**
|
|
43
|
+
* Rerun every subscription from scratch. Lists each one whose value differs
|
|
44
|
+
* from the full rerun.
|
|
45
|
+
*/
|
|
46
|
+
readonly reference: () => Promise<ReadonlyArray<Subscriptions.Disagreement>>;
|
|
28
47
|
/**
|
|
29
48
|
* Move the kernel's clock forward, running each job due on the way at its
|
|
30
49
|
* deadline.
|
|
@@ -69,7 +88,11 @@ const isSchemaDefinition = (
|
|
|
69
88
|
"tables" in value &&
|
|
70
89
|
typeof (value as { tables: unknown }).tables === "object";
|
|
71
90
|
|
|
72
|
-
const accessor = (
|
|
91
|
+
const accessor = (
|
|
92
|
+
world: Execution.World,
|
|
93
|
+
subscriptions: Subscriptions.Registry,
|
|
94
|
+
caller: Execution.Caller,
|
|
95
|
+
): any => ({
|
|
73
96
|
query: (reference: any, args?: any) => world.query(caller, reference, args),
|
|
74
97
|
mutation: (reference: any, args?: any) =>
|
|
75
98
|
world.mutation(caller, reference, args),
|
|
@@ -78,16 +101,19 @@ const accessor = (world: Execution.World, caller: Execution.Caller): any => ({
|
|
|
78
101
|
world.prepare(caller, reference, args),
|
|
79
102
|
apply: (id: string) => world.apply(id),
|
|
80
103
|
held: () => world.held(),
|
|
104
|
+
subscriber: () => subscriptions.subscriber(caller),
|
|
105
|
+
refresh: () => subscriptions.refresh(),
|
|
106
|
+
reference: () => subscriptions.reference(),
|
|
81
107
|
run: (handler: (ctx: any) => any) => world.run(caller, "", handler),
|
|
82
108
|
runInComponent: (componentPath: string, handler: (ctx: any) => any) =>
|
|
83
109
|
world.run(caller, componentPath, handler),
|
|
84
110
|
withIdentity: (identity: Partial<UserIdentity>) =>
|
|
85
|
-
accessor(world, Execution.withIdentity(caller, identity)),
|
|
111
|
+
accessor(world, subscriptions, Execution.withIdentity(caller, identity)),
|
|
86
112
|
withRequestMetadata: (metadata: {
|
|
87
113
|
readonly ip?: string | null;
|
|
88
114
|
readonly userAgent?: string | null;
|
|
89
115
|
}) =>
|
|
90
|
-
accessor(world, {
|
|
116
|
+
accessor(world, subscriptions, {
|
|
91
117
|
...caller,
|
|
92
118
|
ip: metadata.ip ?? null,
|
|
93
119
|
userAgent: metadata.userAgent ?? null,
|
|
@@ -112,7 +138,9 @@ const accessor = (world: Execution.World, caller: Execution.Caller): any => ({
|
|
|
112
138
|
* it, and `apply` validates and publishes a held attempt. `mutation` applies
|
|
113
139
|
* each attempt it prepares at once and prepares the next on a conflict, up to
|
|
114
140
|
* attempt 5. `runtime` supplies the clock and commit timestamps. Scheduled
|
|
115
|
-
* functions run on the kernel's clock, never on a host timer.
|
|
141
|
+
* functions run on the kernel's clock, never on a host timer. A subscriber's
|
|
142
|
+
* subscriptions go stale when a commit changes what they read or the identity
|
|
143
|
+
* they read changes, and `refresh` reruns them.
|
|
116
144
|
*/
|
|
117
145
|
export function convexTest(
|
|
118
146
|
schemaOrOptions?:
|
|
@@ -144,7 +172,7 @@ export function convexTest(
|
|
|
144
172
|
}
|
|
145
173
|
const world = new Execution.World(schema, modules, runtime);
|
|
146
174
|
return {
|
|
147
|
-
...accessor(world, Execution.anonymous),
|
|
175
|
+
...accessor(world, new Subscriptions.Registry(world), Execution.anonymous),
|
|
148
176
|
registerComponent: (
|
|
149
177
|
componentPath: string,
|
|
150
178
|
componentSchema: SchemaDefinition<GenericSchema, boolean>,
|