@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.
Files changed (34) hide show
  1. package/dist/internal/Attempt.d.ts +29 -1
  2. package/dist/internal/Attempt.d.ts.map +1 -1
  3. package/dist/internal/Attempt.js +45 -1
  4. package/dist/internal/Attempt.js.map +1 -1
  5. package/dist/internal/ConvexTest.d.ts +23 -2
  6. package/dist/internal/ConvexTest.d.ts.map +1 -1
  7. package/dist/internal/ConvexTest.js +11 -5
  8. package/dist/internal/ConvexTest.js.map +1 -1
  9. package/dist/internal/Execution.d.ts +43 -0
  10. package/dist/internal/Execution.d.ts.map +1 -1
  11. package/dist/internal/Execution.js +200 -41
  12. package/dist/internal/Execution.js.map +1 -1
  13. package/dist/internal/Invocation.d.ts.map +1 -1
  14. package/dist/internal/Invocation.js.map +1 -1
  15. package/dist/internal/KernelQuery.d.ts +8 -3
  16. package/dist/internal/KernelQuery.d.ts.map +1 -1
  17. package/dist/internal/KernelQuery.js +10 -3
  18. package/dist/internal/KernelQuery.js.map +1 -1
  19. package/dist/internal/Metering.d.ts +186 -0
  20. package/dist/internal/Metering.d.ts.map +1 -0
  21. package/dist/internal/Metering.js +401 -0
  22. package/dist/internal/Metering.js.map +1 -0
  23. package/dist/internal/Subscriptions.d.ts +116 -0
  24. package/dist/internal/Subscriptions.d.ts.map +1 -0
  25. package/dist/internal/Subscriptions.js +186 -0
  26. package/dist/internal/Subscriptions.js.map +1 -0
  27. package/package.json +3 -3
  28. package/src/internal/Attempt.ts +52 -0
  29. package/src/internal/ConvexTest.ts +33 -5
  30. package/src/internal/Execution.ts +375 -54
  31. package/src/internal/Invocation.ts +2 -1
  32. package/src/internal/KernelQuery.ts +17 -6
  33. package/src/internal/Metering.ts +520 -0
  34. 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
+ }