@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/api.ts ADDED
@@ -0,0 +1,465 @@
1
+ /**
2
+ * The public declarations: the `Journal` object, commands and their context, the
3
+ * definition helpers `initJournal` returns, and the input, option and result types.
4
+ * Types only, no runtime code. May import `entity`, `types` and `pg`; nothing else.
5
+ */
6
+ import type { z } from 'zod';
7
+
8
+ import type { EntityBuilder } from './entity.js';
9
+ import type { HookPoint } from './hooks.js';
10
+ import type { JournalTables } from './pg.js';
11
+ import type {
12
+ AnyEntity,
13
+ AnyEventDef,
14
+ CommandHookContext,
15
+ DbHandle,
16
+ Effect,
17
+ EffectHandlers,
18
+ EffectHookContext,
19
+ EntityDef,
20
+ EntityId,
21
+ EntityInfo,
22
+ EntityTable,
23
+ EventDef,
24
+ EventOptions,
25
+ IdTable,
26
+ JournalWorker,
27
+ KeyName,
28
+ Needs,
29
+ PendingOf,
30
+ Projection,
31
+ ProjectionHandlers,
32
+ ProjectionOptions,
33
+ RecordedOf,
34
+ ReducerRow,
35
+ RootDb,
36
+ Row,
37
+ Services,
38
+ ServicesFor,
39
+ SingleKey,
40
+ StrandedJobs,
41
+ Tap,
42
+ TapHandlers,
43
+ TransactionSource,
44
+ WakeListener,
45
+ } from './types.js';
46
+
47
+ /**
48
+ * Looks at the locked row, checks the rules, and returns the events to append.
49
+ * Runs after the row is locked, inside the write's savepoint. `tx` is that
50
+ * savepoint: reads through it see the locked row's transaction.
51
+ */
52
+ export type NextEvents<Tbl extends EntityTable, E extends AnyEventDef, Tx = DbHandle> = (
53
+ row: Row<Tbl> | null,
54
+ ctx: { readonly tx: Tx },
55
+ ) => readonly PendingOf<E>[] | Promise<readonly PendingOf<E>[]>;
56
+ /** Options of a write, from `journal.write` or a command's `write`. */
57
+ export type WriteOptions = {
58
+ /**
59
+ * The row version the caller saw. If the stored version differs (0 for no row), the
60
+ * write is refused with `CONFLICT` before `events` runs, so a client that edited a
61
+ * stale copy learns it instead of overwriting a newer one.
62
+ */
63
+ readonly expectedVersion?: number;
64
+ };
65
+ export type WriteInput<
66
+ Tbl extends EntityTable,
67
+ E extends AnyEventDef,
68
+ Meta extends z.ZodObject,
69
+ Tx = DbHandle,
70
+ > = WriteOptions & {
71
+ readonly meta: z.input<Meta>;
72
+ readonly events: NextEvents<Tbl, E, Tx>;
73
+ };
74
+
75
+ /**
76
+ * A command: a name, an input schema and a handler. `j.command` makes one;
77
+ * `journal.run` runs it. `name` and `input` are for the app (an oRPC procedure
78
+ * can reuse or `.pick` the schema). The handler is for `journal.run` only.
79
+ */
80
+ export type Command<S extends z.ZodType, R, in MetaOut, in Tx extends TransactionSource<Tx> = DbHandle> = {
81
+ readonly name: string;
82
+ readonly input: S;
83
+ readonly handler: (input: z.output<S>, context: CommandContext<MetaOut, Tx>) => R;
84
+ };
85
+ /**
86
+ * What a command handler receives. `write` and `run` are bound to the command's
87
+ * transaction and metadata. The variance annotations here and on `Command` are
88
+ * needed: the two types refer to each other, and TypeScript cannot measure the
89
+ * variance of such a pair on its own.
90
+ */
91
+ /** Options of one call: `journal.run`, a bound runner, a command's `run`, an effect's `ctx.run`. */
92
+ export type CallOptions = {
93
+ /**
94
+ * Makes the call idempotent. The first run with this key records its result in
95
+ * `journal_receipts`, in its own transaction. A later run with the same key and the same
96
+ * input returns that result without running the handler; the same key with another input
97
+ * or another command is `CONFLICT`. The result must survive JSON storage, or the first run
98
+ * fails with `UNSTABLE_SCHEMA`. Scope a key from outside to its caller: `${tenant}:${id}`.
99
+ */
100
+ readonly idempotencyKey?: string;
101
+ };
102
+ /** Options of `journal.run` and `journal.bind`. */
103
+ export type RunOptions = CallOptions & {
104
+ /**
105
+ * Seeds the chain's correlation id, for example with the request id. Without it, a run
106
+ * from an effect continues the cause event's chain, and any other run starts a new one.
107
+ */
108
+ readonly correlationId?: string;
109
+ };
110
+ export type CommandContext<out MetaOut, out Tx extends TransactionSource<Tx> = DbHandle> = {
111
+ /** The command's transaction. Use it for reads and for writes outside any entity. */
112
+ readonly tx: Tx;
113
+ /** The parsed metadata every `write` in this command records. */
114
+ readonly meta: MetaOut;
115
+ /** This run's `cmd_` id. Every event it writes carries it. */
116
+ readonly commandId: string;
117
+ /** The chain this run belongs to. Every event it writes carries it; a child run inherits it. */
118
+ readonly correlationId: string;
119
+ /** `journal.write` on this command's transaction with this command's metadata. Safe to destructure. */
120
+ readonly write: <K extends string, Tbl extends EntityTable, E extends AnyEventDef, Key extends string>(
121
+ entity: EntityDef<K, Tbl, E, MetaOut, Key>,
122
+ id: EntityId<Tbl, Key>,
123
+ events: NextEvents<Tbl, E, Tx>,
124
+ opts?: WriteOptions,
125
+ ) => Promise<WriteResult<Tbl, E, MetaOut, Key>>;
126
+ /** Run another command in a savepoint of this transaction, with the same metadata. Safe to destructure. */
127
+ readonly run: <S extends z.ZodType, R>(
128
+ command: Command<S, R, MetaOut, Tx>,
129
+ input: NoInfer<z.input<S>>,
130
+ opts?: CallOptions,
131
+ ) => Promise<Awaited<R>>;
132
+ };
133
+ /**
134
+ * The type of `context.run`. Use it in app code that passes `run` on, so the
135
+ * generic signature is not repeated there.
136
+ */
137
+ export type CommandRunner<MetaOut, Tx extends TransactionSource<Tx> = DbHandle> = CommandContext<MetaOut, Tx>['run'];
138
+ /**
139
+ * The type of an effect's `ctx.run`: `journal.run` without `source`. It runs on the
140
+ * root database the worker or `drain` was given, in its own transaction. `meta`
141
+ * is required, as in `journal.run`: the library never picks an actor.
142
+ */
143
+ export type RootCommandRunner<Meta extends z.ZodObject, Tx extends TransactionSource<Tx> = DbHandle> = <
144
+ S extends z.ZodType,
145
+ R,
146
+ >(
147
+ command: Command<S, R, z.output<Meta>, Tx>,
148
+ input: NoInfer<z.input<S>>,
149
+ meta: z.input<Meta>,
150
+ opts?: CallOptions,
151
+ ) => Promise<Awaited<R>>;
152
+ export type WriteResult<Tbl extends EntityTable, E extends AnyEventDef, MetaOut, Key extends string = 'id'> = {
153
+ readonly row: Row<Tbl> | null;
154
+ readonly events: RecordedOf<E, MetaOut, EntityId<Tbl, Key>>[];
155
+ };
156
+ export type ForgetResult<Tbl extends EntityTable> = {
157
+ /** How many events were rewritten in this call. Already-erased events count again: the rewrite is the same. */
158
+ readonly erased: number;
159
+ /** The row after repair from the erased log, or `null` if the log says the entity is gone. */
160
+ readonly row: Row<Tbl> | null;
161
+ };
162
+ export type ReplayResult<Tbl extends EntityTable, E extends AnyEventDef, MetaOut, Key extends string = 'id'> = {
163
+ readonly row: ReducerRow<Tbl, Key> | null;
164
+ readonly events: RecordedOf<E, MetaOut, EntityId<Tbl, Key>>[];
165
+ };
166
+ export type VerifyResult<Tbl extends EntityTable, Key extends string = 'id'> =
167
+ | { readonly ok: true }
168
+ | { readonly ok: false; readonly expected: ReducerRow<Tbl, Key> | null; readonly actual: Row<Tbl> | null };
169
+
170
+ export type Journal<
171
+ Meta extends z.ZodObject,
172
+ Tx extends TransactionSource<Tx> = DbHandle,
173
+ AppDb extends RootDb = RootDb,
174
+ > = {
175
+ /** Append events, run reducers, save the row, run projections, queue jobs. All inside `tx`. */
176
+ write<K extends string, Tbl extends EntityTable, E extends AnyEventDef, Key extends string>(
177
+ tx: Tx,
178
+ entity: EntityDef<K, Tbl, E, z.output<Meta>, Key>,
179
+ id: EntityId<Tbl, Key>,
180
+ input: WriteInput<Tbl, E, Meta, Tx>,
181
+ ): Promise<WriteResult<Tbl, E, z.output<Meta>, Key>>;
182
+ /**
183
+ * Parse `input` and `meta`, then call the command's handler inside one
184
+ * transaction of `source`. A root database opens a transaction; a transaction
185
+ * opens a savepoint. Returns what the handler returns. Errors pass through.
186
+ * On a root database, a run that queued a job calls `wake()` after the commit.
187
+ */
188
+ run<S extends z.ZodType, R>(
189
+ source: TransactionSource<Tx>,
190
+ command: Command<S, R, z.output<Meta>, Tx>,
191
+ input: NoInfer<z.input<S>>,
192
+ meta: z.input<Meta>,
193
+ opts?: RunOptions,
194
+ ): Promise<Awaited<R>>;
195
+ /**
196
+ * The command door: `run` with `source` and `meta` fixed. It returns
197
+ * `(command, input) => journal.run(source, command, input, meta)`. A request
198
+ * context binds it once with the signed-in actor; a job binds it with a
199
+ * system actor. The caller still gives `meta`: the library never picks an actor.
200
+ * `source` follows the rules of `run`: a root database commits and wakes, a
201
+ * transaction joins and leaves the commit to the caller.
202
+ * `meta` is parsed on every call, not once here, so a bound runner is exactly
203
+ * `run`: a default in the meta schema gives each command its own value, and
204
+ * a bad `meta` rejects the call, inside the transaction guard, as `run` does.
205
+ */
206
+ bind(source: TransactionSource<Tx>, meta: z.input<Meta>, opts?: RunOptions): CommandRunner<z.output<Meta>, Tx>;
207
+ /**
208
+ * Bring a row that existed before the journal into it. The row must be at version 0
209
+ * with no history, or `CONFLICT`. `events` builds exactly one create event from the
210
+ * stored row. Its `.create` reducer must rebuild that row exactly, column by column,
211
+ * or `VALUE_MISMATCH` and nothing changes: replay folds from nothing and must land on
212
+ * the row as it is. The event is recorded as version 1 and the row moves to version 1,
213
+ * so `replay`, `verify` and `repair` work from then on. Projections run and effects
214
+ * are queued as for any write, because the event is the app's own.
215
+ */
216
+ adopt<K extends string, Tbl extends EntityTable, E extends AnyEventDef, Key extends string>(
217
+ tx: Tx,
218
+ entity: EntityDef<K, Tbl, E, z.output<Meta>, Key>,
219
+ id: EntityId<Tbl, Key>,
220
+ input: WriteInput<Tbl, E, Meta, Tx>,
221
+ ): Promise<WriteResult<Tbl, E, z.output<Meta>, Key>>;
222
+ /** Rebuild the row from the log in memory. `throughVersion` is the last version applied. */
223
+ replay<K extends string, Tbl extends EntityTable, E extends AnyEventDef, Key extends string>(
224
+ db: DbHandle,
225
+ entity: EntityDef<K, Tbl, E, z.output<Meta>, Key>,
226
+ id: EntityId<Tbl, Key>,
227
+ opts?: { readonly throughVersion?: number },
228
+ ): Promise<ReplayResult<Tbl, E, z.output<Meta>, Key>>;
229
+ /** Replay and compare with the stored row on the columns the reducers produce. */
230
+ verify<K extends string, Tbl extends EntityTable, E extends AnyEventDef, Key extends string>(
231
+ db: DbHandle,
232
+ entity: EntityDef<K, Tbl, E, z.output<Meta>, Key>,
233
+ id: EntityId<Tbl, Key>,
234
+ ): Promise<VerifyResult<Tbl, Key>>;
235
+ /**
236
+ * Remove personal data from one entity's history. Every stored event whose definition
237
+ * has an `erased` form gets its `data` rewritten to that form and `erased_at` set; events
238
+ * without one are left alone. Then the row is repaired from the rewritten log, so the row
239
+ * and its history agree. All in one transaction: an erased form the schema rejects is
240
+ * `BAD_DEFINITION` and nothing changes; a history replay cannot rebuild (a legacy row that
241
+ * was never adopted) is `NO_HISTORY` and nothing changes. Projections are not rerun, and no
242
+ * job is queued: the app owns its read models. Running it twice is safe.
243
+ */
244
+ forget<K extends string, Tbl extends EntityTable, E extends AnyEventDef, Key extends string>(
245
+ db: DbHandle,
246
+ entity: EntityDef<K, Tbl, E, z.output<Meta>, Key>,
247
+ id: EntityId<Tbl, Key>,
248
+ ): Promise<ForgetResult<Tbl>>;
249
+ /**
250
+ * Delete receipts recorded before `before`. Receipts hold inputs and results, which may
251
+ * carry personal data; `forget` does not touch them, so an app that keys personal flows
252
+ * prunes on a schedule. Returns how many were deleted.
253
+ */
254
+ pruneReceipts(db: DbHandle, opts: { readonly before: Date }): Promise<number>;
255
+ /** Lock the row, replay, save the result. Writes no event, runs no projection, creates no job. */
256
+ repair<K extends string, Tbl extends EntityTable, E extends AnyEventDef, Key extends string>(
257
+ db: DbHandle,
258
+ entity: EntityDef<K, Tbl, E, z.output<Meta>, Key>,
259
+ id: EntityId<Tbl, Key>,
260
+ ): Promise<Row<Tbl> | null>;
261
+ /**
262
+ * The async lane of projections: one batch per async projection. Each batch locks the
263
+ * projection's cursor, scans the next `batchSize` events in commit order, applies the
264
+ * matching ones on that transaction, and moves the cursor to the last event scanned.
265
+ * The worker calls it after every drain; call it yourself in tests or one-off scripts.
266
+ * A projection whose batch throws is skipped this pass and tried again next pass.
267
+ */
268
+ catchUp(
269
+ db: RootDb & TransactionSource<Tx>,
270
+ opts?: { readonly batchSize?: number },
271
+ ): Promise<{ readonly scanned: number }>;
272
+ /**
273
+ * Rewind an async projection to the start. `clear` runs on the same transaction, after the
274
+ * cursor is locked and before it is rewound, so the read model is emptied and refilled with
275
+ * no batch landing in between. To rebuild without downtime, deploy the projection under a
276
+ * new name into a new table instead, wait for `cursors` to show lag 0, then switch reads.
277
+ */
278
+ reset(
279
+ db: RootDb & TransactionSource<Tx>,
280
+ projection: Projection<z.output<Meta>, Tx>,
281
+ clear?: (tx: Tx) => Promise<void>,
282
+ ): Promise<void>;
283
+ /** Per async projection: its cursor and how far behind the newest event it is, in seqs. */
284
+ cursors(db: DbHandle): Promise<readonly { readonly name: string; readonly seq: number; readonly lag: number }[]>;
285
+ /** Walk the log after sequence number `afterSeq` and call one inline projection's handlers, a batch per transaction. Async projections use `reset` and `catchUp`. */
286
+ rerun(
287
+ db: TransactionSource<Tx>,
288
+ projection: Projection<z.output<Meta>, Tx>,
289
+ opts?: { readonly afterSeq?: number; readonly batchSize?: number },
290
+ ): Promise<{ readonly events: number }>;
291
+ /**
292
+ * Run up to `batchSize` due jobs. Returns how many jobs were claimed. Root database only.
293
+ * It must open the journal's transaction type: an effect runs commands on it.
294
+ * Each job is claimed right before it runs, so its lease (`leaseSeconds`, default 60)
295
+ * covers only its own work, and the lease is renewed every half lease while the effect
296
+ * runs. A lost lease aborts the effect's `ctx.signal`.
297
+ * Once `signal` is aborted no further job is claimed; the running one sees it as `ctx.signal`.
298
+ */
299
+ drain(
300
+ db: AppDb & RootDb & TransactionSource<Tx>,
301
+ opts?: { readonly batchSize?: number; readonly signal?: AbortSignal; readonly leaseSeconds?: number },
302
+ ): Promise<number>;
303
+ /**
304
+ * A loop around `drain`. The same database as `drain`. `stop()` aborts the running
305
+ * effect's `ctx.signal`, claims nothing more, and resolves when that effect returns.
306
+ */
307
+ worker(
308
+ db: AppDb & RootDb & TransactionSource<Tx>,
309
+ opts?: {
310
+ readonly pollMs?: number;
311
+ readonly batchSize?: number;
312
+ readonly leaseSeconds?: number;
313
+ /**
314
+ * Hear the wake channel (`journal.wakeChannel`): a commit that recorded events wakes this
315
+ * worker at once, from any process, and the journal's taps fire here for those events.
316
+ * Required when the journal has taps, unless `taps: false` says this worker runs jobs only.
317
+ */
318
+ readonly listen?: WakeListener;
319
+ /** `false`: a jobs-only worker in a journal that has taps. Another process must listen. */
320
+ readonly taps?: boolean;
321
+ },
322
+ ): JournalWorker;
323
+ /**
324
+ * The Postgres channel every commit that recorded events notifies, with the seq range as
325
+ * JSON `{ "from": n, "to": m }`. `journal_wake`, or `<schema>_journal_wake` for tables in a
326
+ * schema. Payloads are small and bounded; a listener loads what it needs by seq.
327
+ */
328
+ readonly wakeChannel: string;
329
+ /**
330
+ * Wake every running worker this journal started in this process. Call it
331
+ * after committing a transaction of your own that ran commands. No worker: nothing happens.
332
+ */
333
+ wake(): void;
334
+ /**
335
+ * Pending jobs whose effect this journal does not have, counted per effect name.
336
+ * `drain` leaves them alone, because during a rolling deploy an older worker may
337
+ * still have that effect. After the deploy, a non-empty list means an effect was
338
+ * renamed or removed while it had jobs: bring it back under its old name until
339
+ * the count is zero, or delete those job rows on purpose.
340
+ */
341
+ strandedJobs(db: DbHandle): Promise<readonly StrandedJobs[]>;
342
+ /**
343
+ * List job rows. `pending` (the default) is everything not dead: due, delayed and leased
344
+ * alike. `dead` is what the worker gave up on. Oldest due first, `limit` 100.
345
+ */
346
+ jobs(
347
+ db: DbHandle,
348
+ opts?: { readonly status?: 'pending' | 'dead' | 'all'; readonly effect?: string; readonly limit?: number },
349
+ ): Promise<JournalTables['jobs']['$inferSelect'][]>;
350
+ /** A dead job back to pending: attempts and lease cleared, due now, workers woken. A job that is not dead throws. */
351
+ retry(db: DbHandle, jobId: string): Promise<void>;
352
+ /** Delete a dead job. A job that is not dead throws: deleting a live one would not stop its work. */
353
+ discard(db: DbHandle, jobId: string): Promise<void>;
354
+ /**
355
+ * The one way to extend a run from outside: a wrapper that receives the context and
356
+ * `next`. Command hooks run inside the transaction, around the handler; effect hooks run
357
+ * inside the lease, around the handler. Rules: call `next` exactly once, or throw before
358
+ * it to stop; a rejection of `next` is the hook's rejection too; the context is frozen
359
+ * and the result cannot be changed; hooks run in registration order, the first outermost.
360
+ * Register at boot: once the journal has run a command, a drain or a worker, `around`
361
+ * throws `BAD_DEFINITION`.
362
+ */
363
+ readonly hooks: {
364
+ readonly command: HookPoint<CommandHookContext<z.output<Meta>>>;
365
+ readonly effect: HookPoint<EffectHookContext<z.output<Meta>>>;
366
+ };
367
+ readonly catalog: {
368
+ /**
369
+ * Only the entities that a projection or an effect of this journal names. An
370
+ * entity that no handler names is not listed, even when the app writes to it.
371
+ */
372
+ readonly entities: readonly EntityInfo[];
373
+ readonly projections: readonly string[];
374
+ readonly effects: readonly string[];
375
+ readonly taps: readonly string[];
376
+ };
377
+ };
378
+
379
+ export type JournalDefinitions<
380
+ Meta extends z.ZodObject,
381
+ Tx extends TransactionSource<Tx> = DbHandle,
382
+ AppDb extends RootDb = RootDb,
383
+ > = {
384
+ event<T extends string, S extends z.ZodType, const U extends readonly z.ZodType[] = readonly []>(
385
+ type: T,
386
+ schema: S,
387
+ opts?: EventOptions<S, U>,
388
+ ): EventDef<T, S>;
389
+ /** The key column is `id`. */
390
+ entity<K extends string, Tbl extends IdTable>(
391
+ kind: K,
392
+ table: Tbl,
393
+ ): EntityBuilder<K, Tbl, never, z.output<Meta>, 'id'>;
394
+ /**
395
+ * The key column is the one `table` holds under the property name `options.key`:
396
+ * a non-null string column that is the primary key or unique.
397
+ */
398
+ entity<K extends string, Tbl extends EntityTable, Key extends KeyName<Tbl>>(
399
+ kind: K,
400
+ table: Tbl,
401
+ options: { readonly key: Key & SingleKey<Key> },
402
+ ): EntityBuilder<K, Tbl, never, z.output<Meta>, Key>;
403
+ /**
404
+ * A command: `input` is parsed before the handler runs; the handler's
405
+ * `write` and `run` are bound to one transaction and one parsed `meta`.
406
+ * Needs only `j`, entities and events, never the finished journal.
407
+ */
408
+ command<S extends z.ZodType, R>(
409
+ name: string,
410
+ input: S,
411
+ handler: (input: z.output<S>, context: CommandContext<z.output<Meta>, Tx>) => R,
412
+ ): Command<S, R, z.output<Meta>, Tx>;
413
+ /** `on` is `NoInfer` so `E` comes from `entity`; handlers are then contextually typed. */
414
+ projection<E extends AnyEntity<z.output<Meta>>>(
415
+ name: string,
416
+ entity: E | readonly E[],
417
+ on: NoInfer<ProjectionHandlers<E, z.output<Meta>, Tx>>,
418
+ opts?: ProjectionOptions,
419
+ ): Projection<z.output<Meta>, Tx>;
420
+ effect<E extends AnyEntity<z.output<Meta>>>(
421
+ name: string,
422
+ entity: E | readonly E[],
423
+ on: NoInfer<EffectHandlers<E, z.output<Meta>, AppDb, RootCommandRunner<Meta, Tx>>>,
424
+ ): Effect<z.output<Meta>, AppDb, RootCommandRunner<Meta, Tx>>;
425
+ /**
426
+ * An effect that needs a service: `needs` is `{ mailer: need<Mailer>() }`, and each
427
+ * handler then gets `mailer` in its context, next to `jobId`, `signal`, `db` and `run`.
428
+ */
429
+ effect<E extends AnyEntity<z.output<Meta>>, N extends Needs>(
430
+ name: string,
431
+ entity: E | readonly E[],
432
+ needs: N,
433
+ on: NoInfer<EffectHandlers<E, z.output<Meta>, AppDb, RootCommandRunner<Meta, Tx>, N>>,
434
+ ): Effect<z.output<Meta>, AppDb, RootCommandRunner<Meta, Tx>, N>;
435
+ /**
436
+ * Build the runtime journal from the tables and the handlers. `services` holds one
437
+ * value for every need of every listed effect, by name. A need without a value throws here.
438
+ * `const`: the list is read as a tuple, so an effect with needs keeps them next to one without.
439
+ */
440
+ /** A tap: in-process, after commit, lossy. See `Tap`. Same shapes as `effect`. */
441
+ tap<E extends AnyEntity<z.output<Meta>>>(
442
+ name: string,
443
+ entity: E | readonly E[],
444
+ on: NoInfer<TapHandlers<E, z.output<Meta>, AppDb>>,
445
+ ): Tap<z.output<Meta>, AppDb>;
446
+ tap<E extends AnyEntity<z.output<Meta>>, N extends Needs>(
447
+ name: string,
448
+ entity: E | readonly E[],
449
+ needs: N,
450
+ on: NoInfer<TapHandlers<E, z.output<Meta>, AppDb, N>>,
451
+ ): Tap<z.output<Meta>, AppDb, N>;
452
+ compose<
453
+ const E extends readonly Effect<z.output<Meta>, AppDb, RootCommandRunner<Meta, Tx>, Needs>[] = readonly [],
454
+ const T extends readonly Tap<z.output<Meta>, AppDb, Needs>[] = readonly [],
455
+ >(
456
+ config: {
457
+ readonly tables: JournalTables;
458
+ readonly projections?: readonly Projection<z.output<Meta>, Tx>[];
459
+ readonly effects?: E;
460
+ readonly taps?: T;
461
+ } & ServicesOption<ServicesFor<E> & ServicesFor<T>>,
462
+ ): Journal<Meta, Tx, AppDb>;
463
+ };
464
+ /** `services` is required as soon as one listed effect has a need. */
465
+ type ServicesOption<S> = unknown extends S ? { readonly services?: Services } : { readonly services: S & Services };