@arki/journal 0.0.0-stage → 0.1.0

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 (121) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +541 -2
  3. package/dist/api.d.ts +336 -0
  4. package/dist/api.d.ts.map +1 -0
  5. package/dist/api.js +2 -0
  6. package/dist/api.js.map +1 -0
  7. package/dist/command.d.ts +27 -0
  8. package/dist/command.d.ts.map +1 -0
  9. package/dist/command.js +157 -0
  10. package/dist/command.js.map +1 -0
  11. package/dist/debug.d.ts +7 -0
  12. package/dist/debug.d.ts.map +1 -0
  13. package/dist/debug.js +8 -0
  14. package/dist/debug.js.map +1 -0
  15. package/dist/dot.d.ts +102 -0
  16. package/dist/dot.d.ts.map +1 -0
  17. package/dist/dot.js +159 -0
  18. package/dist/dot.js.map +1 -0
  19. package/dist/entity.d.ts +56 -0
  20. package/dist/entity.d.ts.map +1 -0
  21. package/dist/entity.js +174 -0
  22. package/dist/entity.js.map +1 -0
  23. package/dist/errors.d.ts +47 -0
  24. package/dist/errors.d.ts.map +1 -0
  25. package/dist/errors.js +112 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/event.d.ts +9 -0
  28. package/dist/event.d.ts.map +1 -0
  29. package/dist/event.js +19 -0
  30. package/dist/event.js.map +1 -0
  31. package/dist/fold.d.ts +29 -0
  32. package/dist/fold.d.ts.map +1 -0
  33. package/dist/fold.js +49 -0
  34. package/dist/fold.js.map +1 -0
  35. package/dist/handlers.d.ts +26 -0
  36. package/dist/handlers.d.ts.map +1 -0
  37. package/dist/handlers.js +130 -0
  38. package/dist/handlers.js.map +1 -0
  39. package/dist/history.d.ts +49 -0
  40. package/dist/history.d.ts.map +1 -0
  41. package/dist/history.js +259 -0
  42. package/dist/history.js.map +1 -0
  43. package/dist/hooks.d.ts +14 -0
  44. package/dist/hooks.d.ts.map +1 -0
  45. package/dist/hooks.js +67 -0
  46. package/dist/hooks.js.map +1 -0
  47. package/dist/id.d.ts +18 -0
  48. package/dist/id.d.ts.map +1 -0
  49. package/dist/id.js +41 -0
  50. package/dist/id.js.map +1 -0
  51. package/dist/index.d.ts +12 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +5 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/journal.d.ts +51 -0
  56. package/dist/journal.d.ts.map +1 -0
  57. package/dist/journal.js +181 -0
  58. package/dist/journal.js.map +1 -0
  59. package/dist/listen.d.ts +37 -0
  60. package/dist/listen.d.ts.map +1 -0
  61. package/dist/listen.js +90 -0
  62. package/dist/listen.js.map +1 -0
  63. package/dist/orpc.d.ts +21 -0
  64. package/dist/orpc.d.ts.map +1 -0
  65. package/dist/orpc.js +57 -0
  66. package/dist/orpc.js.map +1 -0
  67. package/dist/pg.d.ts +537 -0
  68. package/dist/pg.d.ts.map +1 -0
  69. package/dist/pg.js +80 -0
  70. package/dist/pg.js.map +1 -0
  71. package/dist/storage.d.ts +52 -0
  72. package/dist/storage.d.ts.map +1 -0
  73. package/dist/storage.js +221 -0
  74. package/dist/storage.js.map +1 -0
  75. package/dist/testing.d.ts +19 -0
  76. package/dist/testing.d.ts.map +1 -0
  77. package/dist/testing.js +23 -0
  78. package/dist/testing.js.map +1 -0
  79. package/dist/transaction-guard.d.ts +2 -0
  80. package/dist/transaction-guard.d.ts.map +1 -0
  81. package/dist/transaction-guard.js +39 -0
  82. package/dist/transaction-guard.js.map +1 -0
  83. package/dist/types.d.ts +414 -0
  84. package/dist/types.d.ts.map +1 -0
  85. package/dist/types.js +2 -0
  86. package/dist/types.js.map +1 -0
  87. package/dist/worker.d.ts +109 -0
  88. package/dist/worker.d.ts.map +1 -0
  89. package/dist/worker.js +459 -0
  90. package/dist/worker.js.map +1 -0
  91. package/dist/write.d.ts +39 -0
  92. package/dist/write.d.ts.map +1 -0
  93. package/dist/write.js +287 -0
  94. package/dist/write.js.map +1 -0
  95. package/docs/agent-guide.md +324 -0
  96. package/llms.txt +40 -0
  97. package/package.json +99 -4
  98. package/src/api.ts +465 -0
  99. package/src/command.ts +242 -0
  100. package/src/debug.ts +8 -0
  101. package/src/dot.ts +296 -0
  102. package/src/entity.ts +377 -0
  103. package/src/errors.ts +134 -0
  104. package/src/event.ts +26 -0
  105. package/src/fold.ts +97 -0
  106. package/src/handlers.ts +262 -0
  107. package/src/history.ts +390 -0
  108. package/src/hooks.ts +85 -0
  109. package/src/id.ts +54 -0
  110. package/src/index.ts +76 -0
  111. package/src/journal.ts +256 -0
  112. package/src/listen.ts +109 -0
  113. package/src/orpc.ts +59 -0
  114. package/src/pg.ts +107 -0
  115. package/src/storage.ts +277 -0
  116. package/src/test-support.ts +82 -0
  117. package/src/testing.ts +23 -0
  118. package/src/transaction-guard.ts +41 -0
  119. package/src/types.ts +439 -0
  120. package/src/worker.ts +610 -0
  121. package/src/write.ts +434 -0
package/src/types.ts ADDED
@@ -0,0 +1,439 @@
1
+ import type { AnyRelations } from 'drizzle-orm';
2
+ import type { AnyPgColumn, PgAsyncDatabase, PgQueryResultHKT, PgTableWithColumns } from 'drizzle-orm/pg-core';
3
+ import type { z } from 'zod';
4
+
5
+ /**
6
+ * A function type whose parameters are checked bivariantly.
7
+ *
8
+ * TypeScript only relaxes parameter variance for method signatures. Reading
9
+ * the method back out of an object type keeps that relaxation on a plain
10
+ * function type. This is how a registry can hold handlers written for one
11
+ * event type under a common `Recorded` parameter without a cast.
12
+ */
13
+ export type BivariantFn<Args extends unknown[], R> = { bivarianceHack(...args: Args): R }['bivarianceHack'];
14
+
15
+ type AnyDb = PgAsyncDatabase<PgQueryResultHKT, AnyRelations>;
16
+
17
+ /** A key's longest id, and whether the key column or the journal's own `entity_id` column sets it. */
18
+ export type KeyLimit = { readonly max: number; readonly source: 'column' | 'journal' };
19
+
20
+ /**
21
+ * The Drizzle surface the journal needs. Structural: a Drizzle database and a
22
+ * Drizzle transaction both fit. The journal never touches `db.query`, so the
23
+ * app's relations do not matter here.
24
+ */
25
+ export type DbHandle = {
26
+ readonly select: AnyDb['select'];
27
+ readonly insert: AnyDb['insert'];
28
+ readonly update: AnyDb['update'];
29
+ readonly delete: AnyDb['delete'];
30
+ readonly execute: AnyDb['execute'];
31
+ transaction<T>(run: (tx: DbHandle) => Promise<T>): Promise<T>;
32
+ };
33
+
34
+ /**
35
+ * The root database only. A Drizzle transaction has `rollback()`, so it does
36
+ * not fit. `drain` and `worker` need this: inside a transaction their claim
37
+ * would be a savepoint and handlers would run before the outer commit.
38
+ */
39
+ export type RootDb = DbHandle & { readonly rollback?: undefined };
40
+
41
+ /**
42
+ * A database or transaction whose nested transactions have type `Tx`. The
43
+ * app's Drizzle transaction type fits `TransactionSource<itself>`: a savepoint has
44
+ * the same type as its parent. The root database fits `TransactionSource<AppTx>`.
45
+ * `DbHandle` fits `TransactionSource<DbHandle>`, which is the default everywhere.
46
+ */
47
+ export type TransactionSource<Tx> = { transaction<T>(run: (tx: Tx) => Promise<T>): Promise<T> } & DbHandle;
48
+
49
+ /** A column that can hold an entity's key: a non-null string column. */
50
+ export type KeyColumn = AnyPgColumn<{ data: string; notNull: true }>;
51
+ export type VersionColumn = AnyPgColumn<{ data: number; notNull: true }>;
52
+
53
+ /**
54
+ * A Drizzle table qualifies as an entity table when it has an integer
55
+ * `version` and a string key column. The key column is `id` unless the entity
56
+ * names another one. Every other column is the app's business.
57
+ */
58
+ export type EntityTable = PgTableWithColumns<{
59
+ name: string;
60
+ schema: string | undefined;
61
+ dialect: 'pg';
62
+ columns: {
63
+ version: VersionColumn;
64
+ };
65
+ }>;
66
+
67
+ /** An entity table whose key column is called `id`. The default for `j.entity(kind, table)`. */
68
+ export type IdTable = PgTableWithColumns<{
69
+ name: string;
70
+ schema: string | undefined;
71
+ dialect: 'pg';
72
+ columns: {
73
+ id: KeyColumn;
74
+ version: VersionColumn;
75
+ };
76
+ }>;
77
+
78
+ /** The property names of the columns of `Tbl` that can hold the key: non-null string columns. */
79
+ export type KeyName<Tbl extends EntityTable> = string &
80
+ {
81
+ [K in keyof Tbl['_']['columns']]: Tbl['_']['columns'][K] extends KeyColumn ? K : never;
82
+ }[keyof Tbl['_']['columns']];
83
+
84
+ /** `true` when `T` is a union of two or more members. */
85
+ type IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;
86
+ /**
87
+ * `Key` when it is one property name, `never` for a union. A union key would make
88
+ * `ReducerValues` omit every member, so a reducer could leave required columns unset.
89
+ */
90
+ export type SingleKey<Key extends string> = IsUnion<Key> extends true ? never : Key;
91
+
92
+ /** The current state of one entity. A plain Drizzle select row. */
93
+ export type Row<Tbl extends EntityTable> = Tbl['$inferSelect'];
94
+ /** What reducers return. The library sets the key column and `version`. */
95
+ export type ReducerValues<Tbl extends EntityTable, Key extends string = 'id'> = Omit<
96
+ Tbl['$inferInsert'],
97
+ Key | 'version'
98
+ >;
99
+ /** The key type of an entity table, as the key column declares it. */
100
+ export type EntityId<Tbl extends EntityTable, Key extends string = 'id'> = Extract<
101
+ Row<Tbl>[Key & keyof Row<Tbl>],
102
+ string
103
+ >;
104
+ /**
105
+ * A row rebuilt in memory from events: a reducer result plus the key column
106
+ * and `version`. Columns a reducer leaves out are absent here even when the
107
+ * database would fill them with a default.
108
+ */
109
+ export type ReducerRow<Tbl extends EntityTable, Key extends string = 'id'> = ReducerValues<Tbl, Key> &
110
+ Readonly<Record<Key, EntityId<Tbl, Key>>> & { readonly version: number };
111
+
112
+ /** A validated event that has not been written yet. */
113
+ export type Pending<T extends string = string, Data = unknown> = {
114
+ readonly type: T;
115
+ readonly data: Data;
116
+ };
117
+
118
+ /** What the log adds to a pending event when it is written. `Id` is the entity's key type. */
119
+ export type RecordedStamp<Meta = unknown, Id extends string = string> = {
120
+ readonly seq: number;
121
+ readonly id: string;
122
+ readonly kind: string;
123
+ readonly entityId: Id;
124
+ readonly version: number;
125
+ readonly meta: Meta;
126
+ readonly recordedAt: Date;
127
+ /** The `cmd_` id of the command run that wrote it. Null for a bare `journal.write`, and on rows older than the column. */
128
+ readonly commandId: string | null;
129
+ /** The seq of the event whose effect ran the command that wrote it. Null outside an effect, and on rows older than the column. */
130
+ readonly causeSeq: number | null;
131
+ /** Shared by every event of one chain. Null only on rows older than the column. */
132
+ readonly correlationId: string | null;
133
+ /** When `forget` rewrote `data` to the event's erased form. Null if it never did. */
134
+ readonly erasedAt: Date | null;
135
+ };
136
+
137
+ /**
138
+ * Where a write comes from. The library fills it: a command run stamps its own id, an
139
+ * effect's `ctx.run` passes the event it is for, a root `run` may be seeded with a
140
+ * correlation id from the request. A bare `journal.write` has no command and no cause.
141
+ */
142
+ export type Trace = {
143
+ readonly commandId: string | null;
144
+ readonly causeSeq: number | null;
145
+ readonly correlationId: string;
146
+ };
147
+
148
+ /** What reducers and handlers receive: the event as stored in the log. */
149
+ export type Recorded<T extends string = string, Data = unknown, Meta = unknown, Id extends string = string> = Pending<
150
+ T,
151
+ Data
152
+ > &
153
+ RecordedStamp<Meta, Id>;
154
+
155
+ /**
156
+ * An event definition. Calling it validates the data and returns a pending
157
+ * event. `type` and `schema` are read by the entity builder and by replay.
158
+ */
159
+ export type EventDef<T extends string, S extends z.ZodType> = BivariantFn<
160
+ [data: z.input<S>],
161
+ Pending<T, z.output<S>>
162
+ > & {
163
+ readonly type: T;
164
+ readonly schema: S;
165
+ /**
166
+ * The event's erased form, for `forget`: the same data with the personal parts
167
+ * removed. It must still fit `schema`. Absent on an event that holds no personal data.
168
+ * A method signature, so every `EventDef` is an `AnyEventDef`; see `BivariantFn`.
169
+ */
170
+ erased?(data: z.output<S>): z.input<S>;
171
+ /**
172
+ * The schema version new events are stored at: one more than the number of upcasters.
173
+ * Rows record it in `schema_version`; a row at an older version is upcast on read.
174
+ */
175
+ readonly version: number;
176
+ /** Lifts stored data one version up. `upcasters[0]` takes version 1 to 2. Empty for most events. */
177
+ readonly upcasters: readonly Upcaster[];
178
+ };
179
+
180
+ /**
181
+ * One step of an event's history: the schema its data had at one version, and how to
182
+ * lift that data to the next. Replay parses with `from` first, so `up` sees valid old data.
183
+ * `up` is a method signature, so a typed step is an `Upcaster`; see `BivariantFn`.
184
+ */
185
+ export type Upcaster = {
186
+ readonly from: z.ZodType;
187
+ up(data: unknown): unknown;
188
+ };
189
+
190
+ /** Typed upcaster steps: each `up` receives the output of its own `from`. */
191
+ export type UpcasterSteps<U extends readonly z.ZodType[]> = {
192
+ readonly [K in keyof U]: { readonly from: U[K]; readonly up: (data: z.output<U[K]>) => unknown };
193
+ };
194
+
195
+ /** The third argument of `j.event`. */
196
+ export type EventOptions<S extends z.ZodType, U extends readonly z.ZodType[] = readonly []> = {
197
+ /** See `EventDef.erased`. */
198
+ readonly erased?: (data: z.output<S>) => z.input<S>;
199
+ /**
200
+ * Older shapes of this event, oldest first: `{ from: schemaV1, up: v1 => v2 }`, then
201
+ * `{ from: schemaV2, up: v2 => v3 }`. The last step's result must fit the current schema.
202
+ * Append a step when the event's data changes incompatibly; never edit or remove one.
203
+ */
204
+ readonly upcasters?: UpcasterSteps<U>;
205
+ };
206
+ export type AnyEventDef = EventDef<string, z.ZodType>;
207
+
208
+ /** The pending form of one or more event definitions. Distributes over unions. */
209
+ export type PendingOf<D> = D extends AnyEventDef ? Pending<D['type'], z.output<D['schema']>> : never;
210
+ /** The recorded form of one or more event definitions. */
211
+ export type RecordedOf<D, Meta, Id extends string = string> = PendingOf<D> & RecordedStamp<Meta, Id>;
212
+
213
+ /**
214
+ * What an `.on` reducer returns: the columns that changed. `null` deletes the
215
+ * row. A key set to `undefined` means "unchanged".
216
+ */
217
+ export type Patch<Tbl extends EntityTable, Key extends string = 'id'> = Partial<ReducerValues<Tbl, Key>>;
218
+
219
+ /** A create reducer receives no row. It builds the first row from the event. */
220
+ export type CreateReducer<Tbl extends EntityTable, Meta, Key extends string = 'id'> = BivariantFn<
221
+ [event: Recorded<string, unknown, Meta>],
222
+ ReducerValues<Tbl, Key>
223
+ >;
224
+ /**
225
+ * An `.on` reducer may receive a row loaded from the table or a row rebuilt in
226
+ * memory by an earlier reducer in the same write or replay. Both are
227
+ * `ReducerRow` at runtime; the union is here because TypeScript cannot relate a
228
+ * generic select row to the insert shape.
229
+ */
230
+ export type OnReducer<Tbl extends EntityTable, Meta, Key extends string = 'id'> = BivariantFn<
231
+ [row: Row<Tbl> | ReducerRow<Tbl, Key>, event: Recorded<string, unknown, Meta>],
232
+ Patch<Tbl, Key> | null
233
+ >;
234
+ /** The reducers declared for one event type. Both may be set: the fold picks by state. */
235
+ export type Reducers<Tbl extends EntityTable, Meta, Key extends string = 'id'> = {
236
+ readonly create?: CreateReducer<Tbl, Meta, Key>;
237
+ readonly on?: OnReducer<Tbl, Meta, Key>;
238
+ };
239
+
240
+ /** An entity: a kind, its table and key column, and the events and reducers declared so far. */
241
+ export type EntityDef<
242
+ K extends string,
243
+ Tbl extends EntityTable,
244
+ E extends AnyEventDef,
245
+ Meta,
246
+ Key extends string = 'id',
247
+ > = {
248
+ readonly kind: K;
249
+ readonly table: Tbl;
250
+ /** The property name of the key column on the table and its rows. */
251
+ readonly key: Key;
252
+ /** The key column. Lock-and-load, update and delete filter on it. */
253
+ readonly keyColumn: KeyColumn;
254
+ /** Whether the key column is a `uuid`, decided once when the entity is defined. Such ids must be canonical. */
255
+ readonly uuidKey: boolean;
256
+ /** The longest id the key holds, in code points, decided once when the entity is defined. */
257
+ readonly keyLimit: KeyLimit;
258
+ readonly events: ReadonlyMap<string, AnyEventDef>;
259
+ readonly reducers: ReadonlyMap<string, Reducers<Tbl, Meta, Key>>;
260
+ /** Phantom: the union of event definitions this entity accepts. Never set at runtime. */
261
+ readonly $events?: E;
262
+ };
263
+ export type AnyEntity<Meta = unknown> = EntityDef<string, EntityTable, AnyEventDef, Meta, string>;
264
+
265
+ /** The event definitions an entity accepts, read from its phantom `$events`. */
266
+ export type EventsOf<E> = E extends { readonly $events?: infer D } ? Extract<NonNullable<D>, AnyEventDef> : never;
267
+ /** The key type of one or more entities, read from their table and key. */
268
+ export type EntityIdOf<E> = E extends {
269
+ readonly table: infer Tbl extends EntityTable;
270
+ readonly key: infer Key extends string;
271
+ }
272
+ ? EntityId<Tbl, Key>
273
+ : never;
274
+
275
+ /** Keys are event types; each handler sees the event typed for that key. */
276
+ export type ProjectionHandlers<E, Meta, Tx = DbHandle> = {
277
+ readonly [D in EventsOf<E> as D['type']]?: BivariantFn<[tx: Tx, event: RecordedOf<D, Meta, EntityIdOf<E>>], unknown>;
278
+ };
279
+ /** An effect handler also gets one value for each of the effect's needs `N`, under the need's name. */
280
+ export type EffectHandlers<E, Meta, AppDb = RootDb, Run = unknown, N extends Needs = NoNeeds> = {
281
+ readonly [D in EventsOf<E> as D['type']]?: BivariantFn<
282
+ [event: RecordedOf<D, Meta, EntityIdOf<E>>, ctx: EffectContext<AppDb, Run> & Needed<N>],
283
+ unknown
284
+ >;
285
+ };
286
+
287
+ /** Inline handler. Runs inside the write's transaction. */
288
+ export type Projection<Meta = unknown, Tx = DbHandle> = {
289
+ readonly name: string;
290
+ readonly entities: readonly AnyEntity<Meta>[];
291
+ readonly types: ReadonlySet<string>;
292
+ /**
293
+ * `inline`: runs inside the write's transaction, so the read model is exact. `async`: the
294
+ * worker applies it later, in batches, with a cursor in `journal_cursors` committed in the
295
+ * same transaction as the handlers; exactly once for database writes, eventually consistent.
296
+ */
297
+ readonly lane: 'inline' | 'async';
298
+ readonly run: BivariantFn<[tx: Tx, event: Recorded<string, unknown, Meta>], Promise<void>>;
299
+ };
300
+
301
+ /** The fourth argument of `j.projection`. */
302
+ export type ProjectionOptions = { readonly lane?: 'inline' | 'async' };
303
+
304
+ /**
305
+ * `db` is the root database that was given to `drain` or `worker`: the same object.
306
+ * `run` is `journal.run` on that database. A journal types it as `RootCommandRunner`
307
+ * (in `api.ts`). This file cannot name a command, so the type comes in as `Run`.
308
+ */
309
+ export type EffectContext<AppDb = RootDb, Run = unknown> = {
310
+ readonly jobId: string;
311
+ readonly signal: AbortSignal;
312
+ readonly db: AppDb;
313
+ readonly run: Run;
314
+ };
315
+
316
+ declare const needType: unique symbol;
317
+ /**
318
+ * A marker for one thing an effect needs, such as a mailer. `need<Mailer>()` makes one.
319
+ * It holds the type only. The real value comes later: from `services` in `j.compose`,
320
+ * or from the services of the Dot feature that lists the effect.
321
+ */
322
+ export type Need<T> = { readonly [needType]?: T };
323
+ /** The needs of one effect, by the name its handlers read them under. */
324
+ export type Needs = Readonly<Record<string, Need<unknown>>>;
325
+ export type NoNeeds = Readonly<Record<never, Need<unknown>>>;
326
+ /** What the handlers get for the needs `N`: one value per name. */
327
+ export type Needed<N extends Needs> = { readonly [K in keyof N]: N[K] extends Need<infer T> ? T : never };
328
+ /** Real services by name, as an app or a Dot feature hands them over. */
329
+ export type Services = Readonly<Record<string, unknown>>;
330
+
331
+ type UnionToIntersection<U> = (U extends unknown ? (member: U) => void : never) extends (all: infer I) => void
332
+ ? I
333
+ : never;
334
+ /** The needs of each effect in a union. An effect that needs nothing adds nothing. */
335
+ type DeclaredNeeds<E> = E extends { readonly needs: infer N extends Needs }
336
+ ? keyof N extends never
337
+ ? never
338
+ : N
339
+ : never;
340
+ /**
341
+ * Every need of every effect in a list, as one object of services. Two effects that
342
+ * use the same name get the same service. `unknown` when no effect needs anything.
343
+ */
344
+ export type ServicesFor<E extends readonly { readonly needs: Needs }[]> = UnionToIntersection<
345
+ Needed<DeclaredNeeds<E[number]>>
346
+ >;
347
+
348
+ /** Async handler. Runs after commit, at least once. */
349
+ /**
350
+ * When an effect's jobs run. `priority`: higher first among due jobs; strict, so a
351
+ * steady stream of high-priority jobs can starve lower ones. `delaySeconds`: due that
352
+ * long after the write's transaction started (`now()` in Postgres). `runAt`: due at the
353
+ * time the event names, from the app's clock. Retries keep their backoff and priority.
354
+ */
355
+ export type EffectSchedule<Meta = unknown> = {
356
+ readonly priority?: number;
357
+ readonly delaySeconds?: number;
358
+ /** A method signature, so an effect of one meta type is still an `Effect<unknown>`; see `BivariantFn`. */
359
+ runAt?(event: Recorded<string, unknown, Meta>): Date;
360
+ };
361
+
362
+ export type Effect<Meta = unknown, AppDb = RootDb, Run = unknown, N extends Needs = NoNeeds> = {
363
+ readonly name: string;
364
+ readonly entities: readonly AnyEntity<Meta>[];
365
+ readonly types: ReadonlySet<string>;
366
+ /** What the handlers need on top of `EffectContext`. Empty for an effect that already has its services. */
367
+ readonly needs: N;
368
+ readonly schedule: EffectSchedule<Meta>;
369
+ /** The same effect with this schedule. `j.effect('mail', order, handlers).withSchedule({ priority: 5 })`. */
370
+ withSchedule(schedule: EffectSchedule<Meta>): Effect<Meta, AppDb, Run, N>;
371
+ readonly run: BivariantFn<
372
+ [event: Recorded<string, unknown, Meta>, ctx: EffectContext<AppDb, Run> & Needed<N>],
373
+ Promise<void>
374
+ >;
375
+ };
376
+
377
+ export type EntityInfo = { readonly kind: string; readonly table: string; readonly events: readonly string[] };
378
+
379
+ /** What a tap handler gets: the root database, plus its needs. No `run`: a tap may not issue commands. */
380
+ export type TapContext<AppDb = RootDb> = { readonly db: AppDb };
381
+
382
+ export type TapHandlers<E, Meta, AppDb = RootDb, N extends Needs = NoNeeds> = {
383
+ readonly [D in EventsOf<E> as D['type']]?: BivariantFn<
384
+ [event: RecordedOf<D, Meta, EntityIdOf<E>>, ctx: TapContext<AppDb> & Needed<N>],
385
+ unknown
386
+ >;
387
+ };
388
+
389
+ /**
390
+ * A tap: in-process, after commit, lossy. No job row, no retry, no ordering promise across
391
+ * entities. It fires in every process whose worker listens on the wake channel, from the
392
+ * seq range the commit announced. For a live page, a cache, a metric. Never for work that
393
+ * must happen: that is an effect.
394
+ */
395
+ export type Tap<Meta = unknown, AppDb = RootDb, N extends Needs = NoNeeds> = {
396
+ readonly name: string;
397
+ readonly entities: readonly AnyEntity<Meta>[];
398
+ readonly types: ReadonlySet<string>;
399
+ readonly needs: N;
400
+ readonly run: BivariantFn<
401
+ [event: Recorded<string, unknown, Meta>, ctx: TapContext<AppDb> & Needed<N>],
402
+ Promise<void>
403
+ >;
404
+ };
405
+
406
+ /** Stops listening. May be async. */
407
+ export type Unlisten = () => void | Promise<void>;
408
+ /**
409
+ * How a worker hears the wake channel. The app wires its own client: for `pg`, a dedicated
410
+ * client that runs `LISTEN <channel>` and calls `onMessage(payload)` on each notification; for
411
+ * PGlite, `client.listen(channel, onMessage)`. Start it before `worker.start()` resolves work
412
+ * you care about: `LISTEN` only hears what is sent after it ran, and the worker still polls.
413
+ */
414
+ export type WakeListener = (onMessage: (payload: string) => void) => Unlisten | Promise<Unlisten>;
415
+
416
+ /** `wake()` runs a drain pass now instead of at the next poll. It does nothing unless the worker is running. */
417
+ export type JournalWorker = { start(): Promise<void>; stop(): Promise<void>; wake(): void };
418
+
419
+ /** What a command hook sees. Frozen. `input` is the parsed input; a hook may read it, not change it. */
420
+ export type CommandHookContext<MetaOut = unknown> = {
421
+ readonly name: string;
422
+ readonly input: unknown;
423
+ readonly meta: MetaOut;
424
+ readonly commandId: string;
425
+ readonly correlationId: string;
426
+ /** The idempotency key of this call, or null. A hook sees a replayed call too: its handler does not run. */
427
+ readonly idempotencyKey: string | null;
428
+ };
429
+
430
+ /** What an effect hook sees. Frozen. `attempt` counts from 1. */
431
+ export type EffectHookContext<Meta = unknown> = {
432
+ readonly name: string;
433
+ readonly event: Recorded<string, unknown, Meta>;
434
+ readonly jobId: string;
435
+ readonly attempt: number;
436
+ };
437
+
438
+ /** Pending jobs for one effect name that the journal has no effect for. */
439
+ export type StrandedJobs = { readonly effect: string; readonly jobs: number };