@gaganref/confect-test 10.3.1-fork.87 → 10.3.1-fork.89

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