@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,302 @@
|
|
|
1
|
+
import type { UserIdentity } from "convex/server";
|
|
2
|
+
import { compareValues, convexToJson, type Value } from "convex/values";
|
|
3
|
+
import * as Execution from "./Execution.js";
|
|
4
|
+
import * as Kernel from "./Kernel.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One run of a subscribed query, and the identity version its subscriber had
|
|
8
|
+
* when it ran.
|
|
9
|
+
*/
|
|
10
|
+
export interface Run extends Execution.QueryRun {
|
|
11
|
+
readonly identityVersion: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Why a run no longer shows what its query answers now.
|
|
16
|
+
*/
|
|
17
|
+
export type Invalidation =
|
|
18
|
+
| { readonly kind: "commit"; readonly change: Kernel.Change }
|
|
19
|
+
| {
|
|
20
|
+
readonly kind: "identity";
|
|
21
|
+
readonly from: number;
|
|
22
|
+
readonly to: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Why `run` is stale, if it is: a commit since its snapshot changed something
|
|
27
|
+
* it read, as an attempt's read set conflicts, or it read its subscriber's
|
|
28
|
+
* identity and that identity has changed. The clock never makes a run stale.
|
|
29
|
+
* The binary does not rerun a query because time passed, so a run keeps the
|
|
30
|
+
* time it read until something else invalidates it.
|
|
31
|
+
*/
|
|
32
|
+
export const invalidation = (
|
|
33
|
+
run: Run,
|
|
34
|
+
state: Kernel.State,
|
|
35
|
+
identityVersion: number,
|
|
36
|
+
): Invalidation | undefined => {
|
|
37
|
+
const change = Kernel.conflict(state, run.snapshot, run.reads);
|
|
38
|
+
if (change !== undefined) return { kind: "commit", change };
|
|
39
|
+
if (run.observedIdentity && run.identityVersion !== identityVersion) {
|
|
40
|
+
return { kind: "identity", from: run.identityVersion, to: identityVersion };
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A thrown value as JSON: a Convex value as Convex writes it, and anything else
|
|
47
|
+
* as its string, beside its type so `"1"` and `1` differ.
|
|
48
|
+
*/
|
|
49
|
+
const thrownKey = (thrown: unknown): unknown => {
|
|
50
|
+
try {
|
|
51
|
+
return [typeof thrown, convexToJson(thrown as Value)];
|
|
52
|
+
} catch {
|
|
53
|
+
return [typeof thrown, String(thrown)];
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const errorKey = (error: unknown): string =>
|
|
58
|
+
JSON.stringify(
|
|
59
|
+
error instanceof Error
|
|
60
|
+
? [
|
|
61
|
+
"error",
|
|
62
|
+
error.name,
|
|
63
|
+
error.message,
|
|
64
|
+
thrownKey((error as { readonly data?: unknown }).data),
|
|
65
|
+
]
|
|
66
|
+
: ["thrown", thrownKey(error)],
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether two outcomes show a client the same thing: equal values, failures
|
|
71
|
+
* with errors of the same name, message and data, or failures that threw equal
|
|
72
|
+
* bare values.
|
|
73
|
+
*/
|
|
74
|
+
export const sameOutcome = (
|
|
75
|
+
a: Execution.Outcome,
|
|
76
|
+
b: Execution.Outcome,
|
|
77
|
+
): boolean =>
|
|
78
|
+
a._tag === "returned"
|
|
79
|
+
? b._tag === "returned" && compareValues(a.value, b.value) === 0
|
|
80
|
+
: b._tag === "failed" && errorKey(a.error) === errorKey(b.error);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A subscription's value changed when a refresh reran it.
|
|
84
|
+
*/
|
|
85
|
+
export interface Transition {
|
|
86
|
+
readonly subscription: string;
|
|
87
|
+
readonly from: Execution.Outcome;
|
|
88
|
+
readonly to: Execution.Outcome;
|
|
89
|
+
readonly because: Invalidation;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A subscription whose value differs from what a full rerun of its query
|
|
94
|
+
* answers.
|
|
95
|
+
*/
|
|
96
|
+
export interface Disagreement {
|
|
97
|
+
readonly subscription: string;
|
|
98
|
+
readonly incremental: Execution.Outcome;
|
|
99
|
+
readonly reference: Execution.Outcome;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Run `f` over `items` one at a time, in order, and keep what it answers.
|
|
104
|
+
*/
|
|
105
|
+
const inTurn = <A, B>(
|
|
106
|
+
items: ReadonlyArray<A>,
|
|
107
|
+
f: (item: A) => Promise<B | undefined>,
|
|
108
|
+
): Promise<Array<B>> =>
|
|
109
|
+
items.reduce<Promise<Array<B>>>(
|
|
110
|
+
(previous, item) =>
|
|
111
|
+
previous.then((kept) =>
|
|
112
|
+
f(item).then((b) => (b === undefined ? kept : [...kept, b])),
|
|
113
|
+
),
|
|
114
|
+
Promise.resolve([]),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
export interface Subscription {
|
|
118
|
+
readonly id: string;
|
|
119
|
+
/**
|
|
120
|
+
* The latest run: its value and the dependencies it took.
|
|
121
|
+
*/
|
|
122
|
+
readonly current: () => Run;
|
|
123
|
+
readonly unsubscribe: () => void;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface Live {
|
|
127
|
+
readonly id: string;
|
|
128
|
+
readonly subscriber: Subscriber;
|
|
129
|
+
readonly reference: unknown;
|
|
130
|
+
readonly args: unknown;
|
|
131
|
+
run: Run;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* One client's query set. Its subscriptions share one caller, whose identity
|
|
136
|
+
* `authenticate` replaces, bumping the identity version.
|
|
137
|
+
*/
|
|
138
|
+
export class Subscriber {
|
|
139
|
+
private version = 0;
|
|
140
|
+
|
|
141
|
+
constructor(
|
|
142
|
+
private readonly registry: Registry,
|
|
143
|
+
private who: Execution.Caller,
|
|
144
|
+
) {}
|
|
145
|
+
|
|
146
|
+
get caller(): Execution.Caller {
|
|
147
|
+
return this.who;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get identityVersion(): number {
|
|
151
|
+
return this.version;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
subscribe(reference: unknown, args?: unknown): Promise<Subscription> {
|
|
155
|
+
return this.registry.subscribe(this, reference, args ?? {});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Replace the caller's identity, or sign it out with `null`. Each
|
|
160
|
+
* subscription that read the identity is stale until the next refresh.
|
|
161
|
+
*/
|
|
162
|
+
authenticate(identity: Partial<UserIdentity> | null) {
|
|
163
|
+
this.who =
|
|
164
|
+
identity === null
|
|
165
|
+
? { ...this.who, identity: null, token: null }
|
|
166
|
+
: Execution.withIdentity(this.who, identity);
|
|
167
|
+
this.version++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Every live subscription on one world. A commit or an identity change makes a
|
|
173
|
+
* subscription stale. `refresh` reruns the stale ones, and `reference` reruns
|
|
174
|
+
* every one from scratch to check that none was missed.
|
|
175
|
+
*/
|
|
176
|
+
export class Registry {
|
|
177
|
+
private readonly live = new Map<string, Live>();
|
|
178
|
+
private nextId = 1;
|
|
179
|
+
|
|
180
|
+
constructor(private readonly world: Execution.World) {}
|
|
181
|
+
|
|
182
|
+
subscriber(caller: Execution.Caller): Subscriber {
|
|
183
|
+
return new Subscriber(this, caller);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
subscribe(
|
|
187
|
+
subscriber: Subscriber,
|
|
188
|
+
reference: unknown,
|
|
189
|
+
args: unknown,
|
|
190
|
+
): Promise<Subscription> {
|
|
191
|
+
return this.run(subscriber, reference, args, { journal: null }).then(
|
|
192
|
+
(run) => {
|
|
193
|
+
const live: Live = {
|
|
194
|
+
id: `subscription-${this.nextId++}`,
|
|
195
|
+
subscriber,
|
|
196
|
+
reference,
|
|
197
|
+
args,
|
|
198
|
+
run,
|
|
199
|
+
};
|
|
200
|
+
this.live.set(live.id, live);
|
|
201
|
+
return {
|
|
202
|
+
id: live.id,
|
|
203
|
+
current: () => live.run,
|
|
204
|
+
unsubscribe: () => {
|
|
205
|
+
this.live.delete(live.id);
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Rerun each stale subscription at the latest commit and the kernel's time
|
|
214
|
+
* now, oldest first, until none is stale. Lists each one whose value
|
|
215
|
+
* changed.
|
|
216
|
+
*/
|
|
217
|
+
refresh(): Promise<ReadonlyArray<Transition>> {
|
|
218
|
+
const stale = Array.from(this.live.values()).flatMap((live) => {
|
|
219
|
+
const because = invalidation(
|
|
220
|
+
live.run,
|
|
221
|
+
this.world.state,
|
|
222
|
+
live.subscriber.identityVersion,
|
|
223
|
+
);
|
|
224
|
+
return because === undefined ? [] : [{ live, because }];
|
|
225
|
+
});
|
|
226
|
+
if (stale.length === 0) return Promise.resolve([]);
|
|
227
|
+
return inTurn(stale, ({ live, because }) =>
|
|
228
|
+
this.run(live.subscriber, live.reference, live.args, {
|
|
229
|
+
journal: live.run.journal,
|
|
230
|
+
}).then((run): Transition | undefined => {
|
|
231
|
+
if (this.live.get(live.id) !== live) return undefined;
|
|
232
|
+
const from = live.run.outcome;
|
|
233
|
+
live.run = run;
|
|
234
|
+
return sameOutcome(from, run.outcome)
|
|
235
|
+
? undefined
|
|
236
|
+
: { subscription: live.id, from, to: run.outcome, because };
|
|
237
|
+
}),
|
|
238
|
+
).then((transitions) =>
|
|
239
|
+
this.refresh().then((later) => [...transitions, ...later]),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Rerun every subscription's query from scratch, at the latest commit and
|
|
245
|
+
* under its subscriber's identity now, and list each whose value differs. A
|
|
246
|
+
* paginated run keeps its page end.
|
|
247
|
+
*/
|
|
248
|
+
reference(): Promise<ReadonlyArray<Disagreement>> {
|
|
249
|
+
return inTurn(Array.from(this.live.values()), (live) =>
|
|
250
|
+
this.rerun(live).then(({ outcome }): Disagreement | undefined =>
|
|
251
|
+
sameOutcome(live.run.outcome, outcome)
|
|
252
|
+
? undefined
|
|
253
|
+
: {
|
|
254
|
+
subscription: live.id,
|
|
255
|
+
incremental: live.run.outcome,
|
|
256
|
+
reference: outcome,
|
|
257
|
+
},
|
|
258
|
+
),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* A full rerun of `live`'s query. A run that read the time first reruns at
|
|
264
|
+
* that time, since the clock alone never invalidates it. When that rerun's
|
|
265
|
+
* own reads show the run was owed a rerun, it reruns again at the kernel's
|
|
266
|
+
* time now, as `refresh` would. The rerun was owed when a commit since the
|
|
267
|
+
* run changed something the rerun read, or the rerun read an identity that
|
|
268
|
+
* has changed since. The check reads neither the run's recorded reads nor
|
|
269
|
+
* `invalidation`, so a dependency `refresh` missed still disagrees.
|
|
270
|
+
*/
|
|
271
|
+
private rerun(live: Live): Promise<Run> {
|
|
272
|
+
const { run } = live;
|
|
273
|
+
const now = () =>
|
|
274
|
+
this.run(live.subscriber, live.reference, live.args, {
|
|
275
|
+
journal: run.journal,
|
|
276
|
+
});
|
|
277
|
+
if (run.time === null) return now();
|
|
278
|
+
return this.run(live.subscriber, live.reference, live.args, {
|
|
279
|
+
journal: run.journal,
|
|
280
|
+
time: run.time,
|
|
281
|
+
}).then((atRunTime) =>
|
|
282
|
+
Kernel.conflict(this.world.state, run.snapshot, atRunTime.reads) !==
|
|
283
|
+
undefined ||
|
|
284
|
+
(atRunTime.observedIdentity &&
|
|
285
|
+
atRunTime.identityVersion !== run.identityVersion)
|
|
286
|
+
? now()
|
|
287
|
+
: atRunTime,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private run(
|
|
292
|
+
subscriber: Subscriber,
|
|
293
|
+
reference: unknown,
|
|
294
|
+
args: unknown,
|
|
295
|
+
pinned: { readonly time?: number; readonly journal: string | null },
|
|
296
|
+
): Promise<Run> {
|
|
297
|
+
const identityVersion = subscriber.identityVersion;
|
|
298
|
+
return this.world
|
|
299
|
+
.subscribed(subscriber.caller, reference, args, pinned)
|
|
300
|
+
.then((run) => ({ ...run, identityVersion }));
|
|
301
|
+
}
|
|
302
|
+
}
|