@gaganref/confect-test 10.3.1-fork.9 → 10.3.1-fork.90

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 (71) hide show
  1. package/dist/TestConfect.d.ts +37 -11
  2. package/dist/TestConfect.d.ts.map +1 -1
  3. package/dist/TestConfect.js +22 -7
  4. package/dist/TestConfect.js.map +1 -1
  5. package/dist/internal/Attempt.d.ts +77 -0
  6. package/dist/internal/Attempt.d.ts.map +1 -0
  7. package/dist/internal/Attempt.js +154 -0
  8. package/dist/internal/Attempt.js.map +1 -0
  9. package/dist/internal/Clock.d.ts +29 -0
  10. package/dist/internal/Clock.d.ts.map +1 -0
  11. package/dist/internal/Clock.js +66 -0
  12. package/dist/internal/Clock.js.map +1 -0
  13. package/dist/internal/ConvexTest.d.ts +66 -0
  14. package/dist/internal/ConvexTest.d.ts.map +1 -0
  15. package/dist/internal/ConvexTest.js +62 -0
  16. package/dist/internal/ConvexTest.js.map +1 -0
  17. package/dist/internal/Execution.d.ts +247 -0
  18. package/dist/internal/Execution.d.ts.map +1 -0
  19. package/dist/internal/Execution.js +1024 -0
  20. package/dist/internal/Execution.js.map +1 -0
  21. package/dist/internal/Globals.d.ts +11 -0
  22. package/dist/internal/Globals.d.ts.map +1 -0
  23. package/dist/internal/Globals.js +88 -0
  24. package/dist/internal/Globals.js.map +1 -0
  25. package/dist/internal/Invocation.d.ts +68 -0
  26. package/dist/internal/Invocation.d.ts.map +1 -0
  27. package/dist/internal/Invocation.js +158 -0
  28. package/dist/internal/Invocation.js.map +1 -0
  29. package/dist/internal/Kernel.d.ts +95 -0
  30. package/dist/internal/Kernel.d.ts.map +1 -0
  31. package/dist/internal/Kernel.js +91 -0
  32. package/dist/internal/Kernel.js.map +1 -0
  33. package/dist/internal/KernelQuery.d.ts +55 -0
  34. package/dist/internal/KernelQuery.d.ts.map +1 -0
  35. package/dist/internal/KernelQuery.js +205 -0
  36. package/dist/internal/KernelQuery.js.map +1 -0
  37. package/dist/internal/KernelRefusal.d.ts +102 -0
  38. package/dist/internal/KernelRefusal.d.ts.map +1 -0
  39. package/dist/internal/KernelRefusal.js +94 -0
  40. package/dist/internal/KernelRefusal.js.map +1 -0
  41. package/dist/internal/KernelValues.d.ts +138 -0
  42. package/dist/internal/KernelValues.d.ts.map +1 -0
  43. package/dist/internal/KernelValues.js +281 -0
  44. package/dist/internal/KernelValues.js.map +1 -0
  45. package/dist/internal/MountedHttp.d.ts +1 -1
  46. package/dist/internal/MountedHttp.d.ts.map +1 -1
  47. package/dist/internal/MountedHttp.js +3 -3
  48. package/dist/internal/MountedHttp.js.map +1 -1
  49. package/dist/internal/ReadSet.d.ts +55 -0
  50. package/dist/internal/ReadSet.d.ts.map +1 -0
  51. package/dist/internal/ReadSet.js +29 -0
  52. package/dist/internal/ReadSet.js.map +1 -0
  53. package/dist/internal/Scheduling.d.ts +71 -0
  54. package/dist/internal/Scheduling.d.ts.map +1 -0
  55. package/dist/internal/Scheduling.js +60 -0
  56. package/dist/internal/Scheduling.js.map +1 -0
  57. package/package.json +5 -5
  58. package/src/TestConfect.ts +84 -19
  59. package/src/internal/Attempt.ts +185 -0
  60. package/src/internal/Clock.ts +95 -0
  61. package/src/internal/ConvexTest.ts +163 -0
  62. package/src/internal/Execution.ts +1795 -0
  63. package/src/internal/Globals.ts +113 -0
  64. package/src/internal/Invocation.ts +316 -0
  65. package/src/internal/Kernel.ts +190 -0
  66. package/src/internal/KernelQuery.ts +405 -0
  67. package/src/internal/KernelRefusal.ts +112 -0
  68. package/src/internal/KernelValues.ts +765 -0
  69. package/src/internal/MountedHttp.ts +13 -6
  70. package/src/internal/ReadSet.ts +108 -0
  71. package/src/internal/Scheduling.ts +126 -0
@@ -0,0 +1,113 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ /**
4
+ * Convex gives every function execution its own globals. The kernel gives each
5
+ * function call it starts its own copy of the globals a test invocation pins,
6
+ * as convex-test 0.0.65 does for every handler (its `dist/index.js:1744`,
7
+ * `:1883-1905` and `:1927-1930`). An assignment inside the call lands in its
8
+ * copy only, so the invocation bridge's pins stay with the handler that made
9
+ * them, and overlapping or nested calls keep their own.
10
+ */
11
+
12
+ const names = ["Date", "Math", "crypto"] as const;
13
+
14
+ type Name = (typeof names)[number];
15
+
16
+ type Copies = { [name in Name]?: unknown };
17
+
18
+ interface State {
19
+ readonly copies: AsyncLocalStorage<Copies>;
20
+ /**
21
+ * The getters this module installed, so a later install skips them.
22
+ */
23
+ readonly getters: WeakSet<() => unknown>;
24
+ }
25
+
26
+ const key = Symbol.for("@confect/test/Globals");
27
+
28
+ // One store per test runtime, so every copy of this module reads the same
29
+ // copies.
30
+ const state = (): State =>
31
+ ((globalThis as { [key]?: State })[key] ??= {
32
+ copies: new AsyncLocalStorage(),
33
+ getters: new WeakSet(),
34
+ });
35
+
36
+ /**
37
+ * Whether `accessor`, another library's, already reads through one of this
38
+ * module's, as convex-test's does once it wraps the kernel's: it answers a
39
+ * value only a fresh copy holds. convex-test reinstalls its accessor whenever
40
+ * another is on top (its `dist/index.js:1854-1866`), so wrapping it again would
41
+ * add a layer at every call while both kinds of world are live.
42
+ */
43
+ const readsThrough = (
44
+ copies: State["copies"],
45
+ accessor: PropertyDescriptor,
46
+ name: Name,
47
+ ): boolean => {
48
+ const probe = {};
49
+ return (
50
+ copies.run({ [name]: probe }, () => accessor.get?.call(globalThis)) ===
51
+ probe
52
+ );
53
+ };
54
+
55
+ /**
56
+ * Point each global at the calling function's copy, falling back to the
57
+ * global's own value outside one. A test that redefines a global, as
58
+ * `vi.stubGlobal` does, removes the accessor, so every call installs it again
59
+ * over what the test left.
60
+ */
61
+ const install = ({ copies, getters }: State) => {
62
+ const global = globalThis as unknown as Record<Name, unknown>;
63
+ for (const name of names) {
64
+ const own = Object.getOwnPropertyDescriptor(global, name);
65
+ // oxlint-disable-next-line typescript/unbound-method -- Compared by identity, never called.
66
+ if (own?.get !== undefined && getters.has(own.get)) continue;
67
+ if (own?.get !== undefined && readsThrough(copies, own, name)) continue;
68
+ const base: PropertyDescriptor = own ?? {
69
+ value: undefined,
70
+ writable: true,
71
+ };
72
+ const get = () => {
73
+ const copy = copies.getStore();
74
+ if (copy !== undefined && name in copy) return copy[name];
75
+ return "value" in base ? base.value : base.get?.call(global);
76
+ };
77
+ const set = (value: unknown) => {
78
+ const copy = copies.getStore();
79
+ if (copy !== undefined) copy[name] = value;
80
+ else if ("value" in base && base.writable === true) base.value = value;
81
+ else if (base.set !== undefined) base.set.call(global, value);
82
+ else throw new TypeError(`Cannot assign to read only global '${name}'`);
83
+ };
84
+ try {
85
+ Object.defineProperty(global, name, {
86
+ get,
87
+ set,
88
+ configurable: true,
89
+ enumerable: own?.enumerable ?? false,
90
+ });
91
+ getters.add(get);
92
+ } catch {
93
+ // A global that cannot be redefined keeps no copy, and the bridge
94
+ // refuses to pin it.
95
+ }
96
+ }
97
+ };
98
+
99
+ /**
100
+ * Run `call` with a fresh copy of the globals. It starts from the test
101
+ * environment's, whatever the caller assigned in its own copy.
102
+ */
103
+ export const own = <A>(call: () => A): A => {
104
+ const current = state();
105
+ install(current);
106
+ return current.copies.run({}, call);
107
+ };
108
+
109
+ /**
110
+ * Read `read` against the test environment's globals, never a function call's
111
+ * copy, as the kernel's own clock must.
112
+ */
113
+ export const host = <A>(read: () => A): A => state().copies.exit(read);
@@ -0,0 +1,316 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import type { FunctionMetadata, RequestMetadata } from "convex/server";
3
+ import type * as Context from "effect/Context";
4
+ import * as Clock from "./Clock.js";
5
+
6
+ /**
7
+ * Four re-executions after the first try. A conflict on attempt 5 exhausts.
8
+ */
9
+ export type Attempt = 1 | 2 | 3 | 4 | 5;
10
+
11
+ /**
12
+ * One test backend. Its `services`, `clock` and `random` reach every invocation
13
+ * it starts on its own, such as a scheduled job.
14
+ */
15
+ export interface World {
16
+ readonly name: string;
17
+ readonly services: Context.Context<never>;
18
+ /**
19
+ * The world's controlled clock in epoch milliseconds. An invocation the world
20
+ * starts on its own pins its start time from one reading. A bound action's
21
+ * `Date` reads it live.
22
+ */
23
+ readonly clock: () => number;
24
+ /**
25
+ * The random stream, uniform on [0, 1), that an invocation the world starts
26
+ * on its own draws from.
27
+ */
28
+ readonly random: () => number;
29
+ }
30
+
31
+ export interface Invocation {
32
+ readonly world: World;
33
+ readonly componentPath: string;
34
+ readonly attempt: Attempt;
35
+ readonly request: Omit<RequestMetadata, "authToken">;
36
+ readonly services: Context.Context<never>;
37
+ /**
38
+ * The start time in epoch milliseconds. Every query or mutation of the
39
+ * invocation reads it from `Date.now()` and `new Date()`, so Effect's default
40
+ * `Clock` reads it too. An action reads its world's clock instead, as it
41
+ * runs, and a query or mutation an action calls starts at the action's time
42
+ * then.
43
+ */
44
+ readonly startedAt: number;
45
+ /**
46
+ * The random stream, uniform on [0, 1). Every handler of the invocation draws
47
+ * from it through `Math.random` and `crypto.getRandomValues`, so Effect's
48
+ * default `Random` draws from it too.
49
+ */
50
+ readonly random: () => number;
51
+ }
52
+
53
+ type FunctionType = "query" | "mutation" | "action";
54
+
55
+ /**
56
+ * The invocation a handler runs under, and the type of the function it runs.
57
+ * Test code that binds an invocation runs no function, so it has no type.
58
+ */
59
+ interface Running {
60
+ readonly invocation: Invocation;
61
+ readonly functionType: FunctionType | undefined;
62
+ }
63
+
64
+ /**
65
+ * What `runHandlerPromise` in `@confect/server` reads from `globalThis` under
66
+ * the same key. `start` answers `undefined` when no invocation is bound.
67
+ */
68
+ interface Bridge {
69
+ readonly storage: AsyncLocalStorage<Running>;
70
+ readonly start: <A>(
71
+ meta: {
72
+ readonly getFunctionMetadata: () => Promise<FunctionMetadata>;
73
+ readonly getRequestMetadata?: () => Promise<RequestMetadata>;
74
+ },
75
+ run: (services: Context.Context<never>) => Promise<A>,
76
+ functionType: FunctionType,
77
+ ) => Promise<A> | undefined;
78
+ }
79
+
80
+ const key = Symbol.for("@confect/test/Invocation");
81
+
82
+ /**
83
+ * A fresh invocation of `world` for a request of its own, as an action or a
84
+ * scheduled job starts: attempt 1, the world's services and random stream, and
85
+ * a start time read from the world's clock.
86
+ */
87
+ export const fresh = (
88
+ world: World,
89
+ componentPath: string,
90
+ { ip, userAgent, requestId, scheduledFunctionId }: Invocation["request"],
91
+ ): Invocation => ({
92
+ world,
93
+ componentPath,
94
+ attempt: 1,
95
+ request: { ip, userAgent, requestId, scheduledFunctionId },
96
+ services: world.services,
97
+ startedAt: world.clock(),
98
+ random: world.random,
99
+ });
100
+
101
+ /**
102
+ * The invocation a function of `functionType` called from `running` joins. A
103
+ * query or mutation an action calls is a transaction of its own, so it starts
104
+ * at the world's time when it begins. Any other call shares its caller's
105
+ * start.
106
+ */
107
+ const caller = (running: Running, functionType: FunctionType): Invocation =>
108
+ running.functionType === "action" && functionType !== "action"
109
+ ? { ...running.invocation, startedAt: running.invocation.world.clock() }
110
+ : running.invocation;
111
+
112
+ /**
113
+ * The invocation a handler runs under. A call nested in the bound request joins
114
+ * it at the callee's component path. A job Convex started from a timer set
115
+ * inside the bound request is a request of its own: its scheduled function ID
116
+ * differs, so it gets a fresh record with the world's services, clock and
117
+ * random stream.
118
+ */
119
+ const resolve = (
120
+ bound: Invocation,
121
+ componentPath: string,
122
+ request: RequestMetadata | undefined,
123
+ ): Invocation => {
124
+ if (
125
+ request !== undefined &&
126
+ request.scheduledFunctionId !== bound.request.scheduledFunctionId
127
+ ) {
128
+ return fresh(bound.world, componentPath, request);
129
+ }
130
+ return componentPath === bound.componentPath
131
+ ? bound
132
+ : { ...bound, componentPath };
133
+ };
134
+
135
+ /**
136
+ * What `Date` answers for a function of `functionType` under `invocation`: a
137
+ * query's or mutation's start, or the world's clock as it runs in an action.
138
+ */
139
+ const clockOf = (
140
+ invocation: Invocation,
141
+ functionType: FunctionType,
142
+ ): (() => number) =>
143
+ functionType === "action"
144
+ ? invocation.world.clock
145
+ : () => invocation.startedAt;
146
+
147
+ const pinnedDate = (
148
+ base: DateConstructor,
149
+ now: () => number,
150
+ ): DateConstructor =>
151
+ new Proxy(base, {
152
+ get: (target, property, receiver) =>
153
+ property === "now" ? now : Reflect.get(target, property, receiver),
154
+ construct: (target, args, newTarget) =>
155
+ Reflect.construct(target, args.length === 0 ? [now()] : args, newTarget),
156
+ apply: () => new base(now()).toString(),
157
+ });
158
+
159
+ const drawingMath = (base: Math, random: () => number): Math =>
160
+ new Proxy(base, {
161
+ get: (target, property, receiver) =>
162
+ property === "random" ? random : Reflect.get(target, property, receiver),
163
+ });
164
+
165
+ const drawingCrypto = (base: Crypto, random: () => number): Crypto => {
166
+ function getRandomValues<T extends Parameters<Crypto["getRandomValues"]>[0]>(
167
+ this: unknown,
168
+ array: T,
169
+ ): T {
170
+ // The host call checks the receiver and the argument and throws the host's
171
+ // errors, so a detached call fails as it does in production.
172
+ // oxlint-disable-next-line typescript/unbound-method -- Applied with the caller's receiver.
173
+ Reflect.apply(base.getRandomValues, this === drawing ? base : this, [
174
+ array,
175
+ ]);
176
+ if (array === null) return array;
177
+ const bytes = new Uint8Array(
178
+ array.buffer,
179
+ array.byteOffset,
180
+ array.byteLength,
181
+ );
182
+ for (let index = 0; index < bytes.length; index++) {
183
+ bytes[index] = Math.floor(random() * 256);
184
+ }
185
+ return array;
186
+ }
187
+ const drawing: Crypto = new Proxy(base, {
188
+ get: (target, property) => {
189
+ if (property === "getRandomValues") return getRandomValues;
190
+ // Crypto's methods and getters need the host object as `this`.
191
+ const value: unknown = Reflect.get(target, property, target);
192
+ return typeof value === "function" ? value.bind(target) : value;
193
+ },
194
+ });
195
+ return drawing;
196
+ };
197
+
198
+ /**
199
+ * Point the calling handler's `Date`, `Math` and `crypto` at `invocation`, its
200
+ * `Date` through {@link clockOf}. convex-test gives each handler call its own
201
+ * copy of these globals, and an assignment inside a handler lands in that copy
202
+ * only, so overlapping handlers and the handler's own nested calls keep
203
+ * theirs.
204
+ *
205
+ * `outside` reads in the test's own async context. Where the handler has no
206
+ * copy, an assignment either throws or reaches test code, so `pin` puts back
207
+ * each global that reached it and throws.
208
+ */
209
+ const pin = (
210
+ invocation: Invocation,
211
+ functionType: FunctionType,
212
+ outside: <R>(read: () => R) => R,
213
+ ): void => {
214
+ const host = globalThis;
215
+ const previous = { Date: host.Date, Math: host.Math, crypto: host.crypto };
216
+ // On the kernel, `Date` already answers the running function's time on the
217
+ // kernel's clock, so `runAfter` schedules on it. The bridge leaves it there.
218
+ const kernelDate = Clock.answersInvocation(previous.Date);
219
+ const pinned = {
220
+ Date: kernelDate
221
+ ? previous.Date
222
+ : pinnedDate(previous.Date, clockOf(invocation, functionType)),
223
+ Math: drawingMath(previous.Math, invocation.random),
224
+ crypto: drawingCrypto(previous.crypto, invocation.random),
225
+ };
226
+ try {
227
+ if (!kernelDate) host.Date = pinned.Date;
228
+ host.Math = pinned.Math;
229
+ host.crypto = pinned.crypto;
230
+ } catch {
231
+ // A global without a per-handler copy can be read-only. The check below
232
+ // refuses the handler.
233
+ }
234
+ const reached = outside(() => ({
235
+ Date: !kernelDate && host.Date === pinned.Date,
236
+ Math: host.Math === pinned.Math,
237
+ crypto: host.crypto === pinned.crypto,
238
+ }));
239
+ if (
240
+ host.Date === pinned.Date &&
241
+ host.Math === pinned.Math &&
242
+ host.crypto === pinned.crypto &&
243
+ !reached.Date &&
244
+ !reached.Math &&
245
+ !reached.crypto
246
+ ) {
247
+ return;
248
+ }
249
+ if (reached.Date) host.Date = previous.Date;
250
+ if (reached.Math) host.Math = previous.Math;
251
+ if (reached.crypto) host.crypto = previous.crypto;
252
+ throw new Error(
253
+ "@confect/test cannot pin this handler's Date, Math and crypto: it runs without convex-test's per-handler globals, so pinning would change them for every caller",
254
+ );
255
+ };
256
+
257
+ const make = (): Bridge => {
258
+ const storage = new AsyncLocalStorage<Running>();
259
+ // Taken at the first `bind`, which test code calls outside any handler, so
260
+ // it reads the test environment's globals.
261
+ const outside = AsyncLocalStorage.snapshot();
262
+ return {
263
+ storage,
264
+ start: (meta, run, functionType) => {
265
+ const running = storage.getStore();
266
+ if (running === undefined) return undefined;
267
+ return Promise.all([
268
+ meta.getFunctionMetadata(),
269
+ meta.getRequestMetadata?.(),
270
+ ]).then(([{ componentPath }, request]) => {
271
+ const invocation = resolve(
272
+ caller(running, functionType),
273
+ componentPath,
274
+ request,
275
+ );
276
+ pin(invocation, functionType, outside);
277
+ return storage.run({ invocation, functionType }, () =>
278
+ run(invocation.services),
279
+ );
280
+ });
281
+ },
282
+ };
283
+ };
284
+
285
+ // One bridge per test runtime, so every copy of this module that a test loads
286
+ // binds through the store `runHandlerPromise` reads.
287
+ const installed = globalThis as { [key]?: Bridge };
288
+
289
+ /**
290
+ * Run `start` with `invocation` bound. Every registered handler it reaches
291
+ * through convex-test runs with the invocation's services, clock and random
292
+ * stream.
293
+ */
294
+ export const bind = <A>(
295
+ invocation: Invocation,
296
+ start: () => Promise<A>,
297
+ ): Promise<A> =>
298
+ (installed[key] ??= make()).storage.run(
299
+ { invocation, functionType: undefined },
300
+ start,
301
+ );
302
+
303
+ /**
304
+ * Run `start` with no invocation bound, so every registered handler it reaches
305
+ * keeps its production runtime, whatever the caller has bound.
306
+ */
307
+ export const unbound = <A>(start: () => Promise<A>): Promise<A> => {
308
+ const bridge = installed[key];
309
+ return bridge === undefined ? start() : bridge.storage.exit(start);
310
+ };
311
+
312
+ /**
313
+ * The invocation the calling handler runs under, if one is bound.
314
+ */
315
+ export const current = (): Invocation | undefined =>
316
+ installed[key]?.storage.getStore()?.invocation;
@@ -0,0 +1,190 @@
1
+ import * as Clock from "./Clock.js";
2
+ import type { DocumentId, StoredDocument } from "./KernelValues.js";
3
+ import { resolveCommittedValues, tableNameFromId } from "./KernelValues.js";
4
+ import * as ReadSet from "./ReadSet.js";
5
+
6
+ /**
7
+ * The number of commits the kernel has published. The empty world is 0.
8
+ */
9
+ export type Version = number;
10
+
11
+ /**
12
+ * A committed document and the version that wrote it.
13
+ */
14
+ export interface Row {
15
+ readonly doc: StoredDocument;
16
+ readonly version: Version;
17
+ }
18
+
19
+ /**
20
+ * Committed rows, keyed by component path, then table name, then document ID. A
21
+ * published state is never changed. A commit makes a new one.
22
+ */
23
+ export type Tables = ReadonlyMap<
24
+ string,
25
+ ReadonlyMap<string, ReadonlyMap<DocumentId, Row>>
26
+ >;
27
+
28
+ /**
29
+ * One document's final value in a commit. `null` deletes it.
30
+ */
31
+ export interface Write {
32
+ readonly componentPath: string;
33
+ readonly id: DocumentId;
34
+ readonly doc: StoredDocument | null;
35
+ }
36
+
37
+ /**
38
+ * One document a commit changed: its value before and after. `null` is absent.
39
+ */
40
+ export interface Change {
41
+ readonly componentPath: string;
42
+ readonly id: DocumentId;
43
+ readonly before: StoredDocument | null;
44
+ readonly after: StoredDocument | null;
45
+ }
46
+
47
+ export interface Commit {
48
+ readonly version: Version;
49
+ readonly commitTs: bigint;
50
+ readonly changes: ReadonlyArray<Change>;
51
+ }
52
+
53
+ /**
54
+ * The kernel's committed state: rows, the latest version and commit timestamp,
55
+ * and the write log, oldest first.
56
+ */
57
+ export interface State {
58
+ readonly tables: Tables;
59
+ readonly version: Version;
60
+ readonly commitTs: bigint;
61
+ readonly log: ReadonlyArray<Commit>;
62
+ }
63
+
64
+ export const empty: State = {
65
+ tables: new Map(),
66
+ version: 0,
67
+ commitTs: 0n,
68
+ log: [],
69
+ };
70
+
71
+ /**
72
+ * The committed row for `id` in `componentPath`, if any.
73
+ */
74
+ export const get = (
75
+ state: State,
76
+ componentPath: string,
77
+ id: DocumentId,
78
+ ): Row | undefined => {
79
+ const tableName = tableNameFromId(id);
80
+ return tableName === null
81
+ ? undefined
82
+ : state.tables.get(componentPath)?.get(tableName)?.get(id);
83
+ };
84
+
85
+ /**
86
+ * Every committed row of one table in one component.
87
+ */
88
+ export const rows = (
89
+ state: State,
90
+ componentPath: string,
91
+ tableName: string,
92
+ ): Iterable<Row> =>
93
+ state.tables.get(componentPath)?.get(tableName)?.values() ?? [];
94
+
95
+ /**
96
+ * Publish one attempt's writes as a single commit at `commitTs`. Every
97
+ * component's writes land in the same version, so a reader sees all of them or
98
+ * none. A commit with no writes publishes nothing and leaves the state as it
99
+ * was.
100
+ */
101
+ export const apply = (
102
+ state: State,
103
+ writes: ReadonlyArray<Write>,
104
+ commitTs: bigint,
105
+ ): State => {
106
+ if (writes.length === 0) return state;
107
+ const version = state.version + 1;
108
+ const tables = new Map(
109
+ Array.from(state.tables, ([path, byTable]) => [path, new Map(byTable)]),
110
+ );
111
+ const changes = writes.map(({ componentPath, id, doc }): Change => ({
112
+ componentPath,
113
+ id,
114
+ before: get(state, componentPath, id)?.doc ?? null,
115
+ after: doc === null ? null : resolveCommittedValues(doc, commitTs),
116
+ }));
117
+ const touched = new Set<string>();
118
+ for (const { componentPath, id, after } of changes) {
119
+ const tableName = tableNameFromId(id)!;
120
+ const byTable = tables.get(componentPath) ?? new Map();
121
+ tables.set(componentPath, byTable);
122
+ const key = `${componentPath}\u0000${tableName}`;
123
+ const table = touched.has(key)
124
+ ? byTable.get(tableName)!
125
+ : new Map(byTable.get(tableName) ?? []);
126
+ touched.add(key);
127
+ byTable.set(tableName, table);
128
+ if (after === null) table.delete(id);
129
+ else table.set(id, { doc: after, version });
130
+ }
131
+ return {
132
+ tables,
133
+ version,
134
+ commitTs,
135
+ log: [...state.log, { version, commitTs, changes }],
136
+ };
137
+ };
138
+
139
+ /**
140
+ * The first change published after version `snapshot` that invalidates one of
141
+ * `reads`, if any.
142
+ */
143
+ export const conflict = (
144
+ state: State,
145
+ snapshot: Version,
146
+ reads: ReadonlyArray<ReadSet.Read>,
147
+ ): Change | undefined => {
148
+ for (const commit of state.log.slice(snapshot)) {
149
+ for (const change of commit.changes) {
150
+ if (reads.some((read) => ReadSet.covers(read, change))) return change;
151
+ }
152
+ }
153
+ return undefined;
154
+ };
155
+
156
+ /**
157
+ * What the driver supplies. The kernel holds no clock of its own.
158
+ */
159
+ export interface KernelRuntime {
160
+ /**
161
+ * The time in milliseconds. The kernel's clock never reads earlier than the
162
+ * latest deadline `advance` reached, so a runtime that stands still still
163
+ * lets jobs run on time.
164
+ */
165
+ readonly now: () => number;
166
+ /**
167
+ * The commit timestamp of `version`, asked once as it is published. The
168
+ * answers strictly increase. A mutation that wrote nothing publishes no
169
+ * version and asks nothing.
170
+ */
171
+ readonly commitTs: (version: Version) => bigint;
172
+ }
173
+
174
+ /**
175
+ * The wall clock, as convex-test reads it. Each answer is the time in
176
+ * nanoseconds when it is asked for, bumped past the previous answer when the
177
+ * clock stands still.
178
+ */
179
+ export const wallClock = (): KernelRuntime => {
180
+ let latest = 0n;
181
+ const now = Clock.hostNow;
182
+ return {
183
+ now,
184
+ commitTs: () => {
185
+ const nanos = BigInt(now()) * 1_000_000n;
186
+ latest = nanos > latest ? nanos : latest + 1n;
187
+ return latest;
188
+ },
189
+ };
190
+ };