@arki/journal 0.1.0 → 0.2.1
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.
- package/README.md +18 -10
- package/dist/api.d.ts +40 -6
- package/dist/api.d.ts.map +1 -1
- package/dist/command.d.ts.map +1 -1
- package/dist/command.js +4 -0
- package/dist/command.js.map +1 -1
- package/dist/dot.d.ts +83 -41
- package/dist/dot.d.ts.map +1 -1
- package/dist/dot.js +111 -82
- package/dist/dot.js.map +1 -1
- package/dist/handlers.d.ts +22 -2
- package/dist/handlers.d.ts.map +1 -1
- package/dist/handlers.js +26 -12
- package/dist/handlers.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +12 -5
- package/dist/journal.js.map +1 -1
- package/dist/transaction-guard.d.ts.map +1 -1
- package/dist/transaction-guard.js +0 -1
- package/dist/transaction-guard.js.map +1 -1
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.d.ts +12 -5
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +13 -8
- package/dist/worker.js.map +1 -1
- package/dist/write.js +1 -1
- package/dist/write.js.map +1 -1
- package/docs/agent-guide.md +23 -20
- package/llms.txt +1 -1
- package/package.json +3 -3
- package/src/api.ts +46 -4
- package/src/command.ts +3 -0
- package/src/dot.ts +203 -134
- package/src/handlers.ts +41 -19
- package/src/index.ts +4 -0
- package/src/journal.ts +16 -6
- package/src/transaction-guard.ts +0 -1
- package/src/types.ts +9 -0
- package/src/worker.ts +45 -14
- package/src/write.ts +1 -1
package/src/handlers.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
EffectContext,
|
|
7
7
|
EffectHandlers,
|
|
8
8
|
EffectSchedule,
|
|
9
|
+
HandlerServices,
|
|
9
10
|
Need,
|
|
10
11
|
Needed,
|
|
11
12
|
Needs,
|
|
@@ -217,19 +218,32 @@ export function tapFor<Meta, AppDb>() {
|
|
|
217
218
|
return tap;
|
|
218
219
|
}
|
|
219
220
|
|
|
220
|
-
/**
|
|
221
|
-
|
|
221
|
+
/**
|
|
222
|
+
* The value of each need of one handler, read from `services` by name. `services` may hold more.
|
|
223
|
+
* Throws when one is missing: without this check the handler would start, and fail on `undefined`.
|
|
224
|
+
*/
|
|
225
|
+
export function givenServices(
|
|
226
|
+
kind: 'effect' | 'tap',
|
|
227
|
+
handler: { readonly name: string; readonly needs: Needs },
|
|
228
|
+
services: Services,
|
|
229
|
+
): Services {
|
|
222
230
|
const given: Record<string, unknown> = {};
|
|
223
|
-
for (const key of Object.keys(
|
|
231
|
+
for (const key of Object.keys(handler.needs)) {
|
|
224
232
|
if (!Object.hasOwn(services, key) || services[key] === undefined)
|
|
225
233
|
throw new JournalError(
|
|
226
234
|
'BAD_DEFINITION',
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
`journal:
|
|
235
|
+
kind,
|
|
236
|
+
handler.name,
|
|
237
|
+
`journal: ${kind} "${handler.name}" needs "${key}", but no service with that name was given; pass it as j.compose({ services: { ${key} } }), or in a Dot app add it to the \`needs\` of the feature that lists this ${kind}`,
|
|
230
238
|
);
|
|
231
239
|
given[key] = services[key];
|
|
232
240
|
}
|
|
241
|
+
return given;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Give one tap its services, as `bindEffect` does for an effect. */
|
|
245
|
+
export function bindTap<Meta, AppDb>(tap: Tap<Meta, AppDb, Needs>, services: Services): Tap<Meta, AppDb> {
|
|
246
|
+
const given = givenServices('tap', tap, services);
|
|
233
247
|
return { ...tap, needs: {}, run: (event, ctx) => tap.run(event, { ...given, ...ctx }) };
|
|
234
248
|
}
|
|
235
249
|
|
|
@@ -237,26 +251,34 @@ export function bindTap<Meta, AppDb>(tap: Tap<Meta, AppDb, Needs>, services: Ser
|
|
|
237
251
|
* Give one effect its services. The result needs nothing more, so the worker can run it.
|
|
238
252
|
* `services` may hold more than this effect needs: each need is read by its name.
|
|
239
253
|
* This is the only place a need meets its value. `j.compose` calls it with the app's
|
|
240
|
-
* `services`;
|
|
254
|
+
* `services`; a worker calls it with the services it was given (`HandlerServices`).
|
|
241
255
|
*/
|
|
242
256
|
export function bindEffect<Meta, AppDb, Run>(
|
|
243
257
|
effect: Effect<Meta, AppDb, Run, Needs>,
|
|
244
258
|
services: Services,
|
|
245
259
|
): Effect<Meta, AppDb, Run> {
|
|
246
|
-
const given
|
|
247
|
-
for (const key of Object.keys(effect.needs)) {
|
|
248
|
-
// Without this check the effect would start, and every job would fail on `undefined`.
|
|
249
|
-
if (!Object.hasOwn(services, key) || services[key] === undefined)
|
|
250
|
-
throw new JournalError(
|
|
251
|
-
'BAD_DEFINITION',
|
|
252
|
-
'effect',
|
|
253
|
-
effect.name,
|
|
254
|
-
`journal: effect "${effect.name}" needs "${key}", but no service with that name was given; pass it as j.compose({ services: { ${key} } }), or in a Dot app add it to the \`needs\` of the feature that lists this effect`,
|
|
255
|
-
);
|
|
256
|
-
given[key] = services[key];
|
|
257
|
-
}
|
|
260
|
+
const given = givenServices('effect', effect, services);
|
|
258
261
|
return withSchedule(
|
|
259
262
|
{ ...effect, needs: {}, run: (event, ctx) => effect.run(event, { ...given, ...ctx }) },
|
|
260
263
|
effect.schedule,
|
|
261
264
|
);
|
|
262
265
|
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The effects and taps as they run: each bound to its services from `services`.
|
|
269
|
+
* A handler `j.compose` already bound needs nothing, so it gets nothing. Throws on the first
|
|
270
|
+
* missing service, so a worker refuses to start instead of failing job after job.
|
|
271
|
+
*/
|
|
272
|
+
export function bindHandlers<Meta, AppDb, Run>(
|
|
273
|
+
handlers: {
|
|
274
|
+
readonly effects: readonly Effect<Meta, AppDb, Run, Needs>[];
|
|
275
|
+
readonly taps: readonly Tap<Meta, AppDb, Needs>[];
|
|
276
|
+
},
|
|
277
|
+
services: HandlerServices | undefined,
|
|
278
|
+
): { readonly effects: readonly Effect<Meta, AppDb, Run>[]; readonly taps: readonly Tap<Meta, AppDb>[] } {
|
|
279
|
+
const of = (kind: 'effect' | 'tap', name: string): Services => services?.({ kind, name }) ?? {};
|
|
280
|
+
return {
|
|
281
|
+
effects: handlers.effects.map(effect => bindEffect(effect, of('effect', effect.name))),
|
|
282
|
+
taps: handlers.taps.map(tap => bindTap(tap, of('tap', tap.name))),
|
|
283
|
+
};
|
|
284
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,9 @@ export { initJournal } from './journal.js';
|
|
|
2
2
|
export { need } from './handlers.js';
|
|
3
3
|
export type {
|
|
4
4
|
Command,
|
|
5
|
+
CommandAuthorize,
|
|
5
6
|
CommandContext,
|
|
7
|
+
CommandOptions,
|
|
6
8
|
CommandRunner,
|
|
7
9
|
Journal,
|
|
8
10
|
JournalDefinitions,
|
|
@@ -66,6 +68,8 @@ export type {
|
|
|
66
68
|
Row,
|
|
67
69
|
Services,
|
|
68
70
|
ServicesFor,
|
|
71
|
+
HandlerRef,
|
|
72
|
+
HandlerServices,
|
|
69
73
|
TransactionSource,
|
|
70
74
|
JournalWorker,
|
|
71
75
|
StrandedJobs,
|
package/src/journal.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
EntityTable,
|
|
21
21
|
IdTable,
|
|
22
22
|
KeyName,
|
|
23
|
+
Needs,
|
|
23
24
|
Projection,
|
|
24
25
|
RootDb,
|
|
25
26
|
SingleKey,
|
|
@@ -30,7 +31,7 @@ import { createCommandRunner } from './command.js';
|
|
|
30
31
|
import { defineEntity } from './entity.js';
|
|
31
32
|
import { JournalError } from './errors.js';
|
|
32
33
|
import { defineEvent } from './event.js';
|
|
33
|
-
import { bindEffect, bindTap, defineProjection, effectFor, tapFor } from './handlers.js';
|
|
34
|
+
import { bindEffect, bindHandlers, bindTap, defineProjection, effectFor, tapFor } from './handlers.js';
|
|
34
35
|
import { createHistory } from './history.js';
|
|
35
36
|
import { createHookPoint } from './hooks.js';
|
|
36
37
|
import {
|
|
@@ -114,13 +115,20 @@ function createInit<Meta extends z.ZodObject, Tx extends TransactionSource<Tx>,
|
|
|
114
115
|
return {
|
|
115
116
|
event: defineEvent,
|
|
116
117
|
entity,
|
|
117
|
-
command: (name, input, handler) =>
|
|
118
|
+
command: (name, input, handler, options) =>
|
|
119
|
+
options?.authorize === undefined
|
|
120
|
+
? { name, input, handler }
|
|
121
|
+
: { name, input, handler, authorize: options.authorize },
|
|
118
122
|
projection: (name, entity, on, opts) => defineProjection(name, entity, on, opts),
|
|
119
123
|
effect: effectFor<MetaOut, AppDb, RootCommandRunner<Meta, Tx>>(),
|
|
120
124
|
tap: tapFor<MetaOut, AppDb>(),
|
|
121
125
|
compose: config => {
|
|
122
|
-
|
|
123
|
-
const
|
|
126
|
+
// `'worker'`: the handlers keep their needs, and each worker or drain gives the services.
|
|
127
|
+
const services = config.services === 'worker' ? undefined : (config.services ?? {});
|
|
128
|
+
const effects = (config.effects ?? []).map(effect =>
|
|
129
|
+
services === undefined ? effect : bindEffect(effect, services),
|
|
130
|
+
);
|
|
131
|
+
const taps = (config.taps ?? []).map(tap => (services === undefined ? tap : bindTap(tap, services)));
|
|
124
132
|
return createJournal<Meta, Tx, AppDb>(metaSchema, config.tables, config.projections ?? [], effects, taps);
|
|
125
133
|
},
|
|
126
134
|
};
|
|
@@ -143,8 +151,9 @@ function createJournal<Meta extends z.ZodObject, Tx extends TransactionSource<Tx
|
|
|
143
151
|
metaSchema: Meta,
|
|
144
152
|
tables: JournalTables,
|
|
145
153
|
projections: readonly Projection<z.output<Meta>, Tx>[],
|
|
146
|
-
|
|
147
|
-
|
|
154
|
+
/** An effect or a tap may still have needs (`services: 'worker'`): only the worker runs them, with its `services`. */
|
|
155
|
+
effects: readonly Effect<z.output<Meta>, AppDb, RootCommandRunner<Meta, Tx>, Needs>[],
|
|
156
|
+
taps: readonly Tap<z.output<Meta>, AppDb, Needs>[] = [],
|
|
148
157
|
): Journal<Meta, Tx, AppDb> {
|
|
149
158
|
assertUniqueNames('tap', taps);
|
|
150
159
|
const eventsSchema = getTableConfig(tables.events).schema;
|
|
@@ -196,6 +205,7 @@ function createJournal<Meta extends z.ZodObject, Tx extends TransactionSource<Tx
|
|
|
196
205
|
drain: (db, opts) => journal.drain(db, opts),
|
|
197
206
|
catchUp: (db, opts) => journal.catchUp(db, opts),
|
|
198
207
|
dispatch,
|
|
208
|
+
checkServices: services => void bindHandlers({ effects, taps }, services),
|
|
199
209
|
tapNames: taps.map(tap => tap.name),
|
|
200
210
|
});
|
|
201
211
|
const strandedJobs = createStrandedJobs({ tables, effectNames: effects.map(effect => effect.name) });
|
package/src/transaction-guard.ts
CHANGED
|
@@ -34,7 +34,6 @@ export function oneAtATime<T>(handle: object, what: string, work: () => Promise<
|
|
|
34
34
|
);
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
-
// eslint-disable-next-line drizzle/enforce-delete-with-where -- WeakMap#delete, not a Drizzle query
|
|
38
37
|
const running = work().finally(() => inFlight.delete(handle));
|
|
39
38
|
inFlight.set(handle, running);
|
|
40
39
|
return running;
|
package/src/types.ts
CHANGED
|
@@ -327,6 +327,15 @@ export type NoNeeds = Readonly<Record<never, Need<unknown>>>;
|
|
|
327
327
|
export type Needed<N extends Needs> = { readonly [K in keyof N]: N[K] extends Need<infer T> ? T : never };
|
|
328
328
|
/** Real services by name, as an app or a Dot feature hands them over. */
|
|
329
329
|
export type Services = Readonly<Record<string, unknown>>;
|
|
330
|
+
/** Names one effect or one tap. Effect names and tap names are two separate sets. */
|
|
331
|
+
export type HandlerRef = { readonly kind: 'effect' | 'tap'; readonly name: string };
|
|
332
|
+
/**
|
|
333
|
+
* The services of each effect and tap, given where they run instead of where the journal
|
|
334
|
+
* is composed: `j.compose({ ..., services: 'worker' })`, then `journal.worker(db, { services })`.
|
|
335
|
+
* One function, so each handler can get its own services: two Dot features may use one need
|
|
336
|
+
* name for two different services.
|
|
337
|
+
*/
|
|
338
|
+
export type HandlerServices = (handler: HandlerRef) => Services;
|
|
330
339
|
|
|
331
340
|
type UnionToIntersection<U> = (U extends unknown ? (member: U) => void : never) extends (all: infer I) => void
|
|
332
341
|
? I
|
package/src/worker.ts
CHANGED
|
@@ -15,7 +15,9 @@ import type {
|
|
|
15
15
|
DbHandle,
|
|
16
16
|
Effect,
|
|
17
17
|
EffectHookContext,
|
|
18
|
+
HandlerServices,
|
|
18
19
|
JournalWorker,
|
|
20
|
+
Needs,
|
|
19
21
|
Projection,
|
|
20
22
|
RootDb,
|
|
21
23
|
StrandedJobs,
|
|
@@ -27,6 +29,7 @@ import type {
|
|
|
27
29
|
import { debugDrain, debugWorker } from './debug.js';
|
|
28
30
|
import { JournalError } from './errors.js';
|
|
29
31
|
import { findEventDefinition } from './fold.js';
|
|
32
|
+
import { bindHandlers } from './handlers.js';
|
|
30
33
|
import { leaseToken } from './id.js';
|
|
31
34
|
import { assertRootDb } from './storage.js';
|
|
32
35
|
|
|
@@ -57,7 +60,8 @@ export type DrainDeps<Meta extends z.ZodObject, AppDb extends RootDb, Run> = {
|
|
|
57
60
|
readonly tables: JournalTables;
|
|
58
61
|
/** The effect hooks. Frozen by the first drain. */
|
|
59
62
|
readonly hooks: HookPointInternal<EffectHookContext<z.output<Meta>>>;
|
|
60
|
-
|
|
63
|
+
/** As composed: an effect may still have needs. Each drain binds them from its `services`. */
|
|
64
|
+
readonly effects: readonly Effect<z.output<Meta>, AppDb, Run, Needs>[];
|
|
61
65
|
readonly history: Pick<History<Meta>, 'decodeStoredEvent'>;
|
|
62
66
|
/** Makes an effect's `ctx.run`: the journal's `run`, bound to the root database this drain was given and to the event the job is for. */
|
|
63
67
|
readonly runOn: (db: AppDb & RootDb, cause: { readonly seq: number; readonly correlationId: string | null }) => Run;
|
|
@@ -66,9 +70,7 @@ export type DrainDeps<Meta extends z.ZodObject, AppDb extends RootDb, Run> = {
|
|
|
66
70
|
export function createDrain<Meta extends z.ZodObject, AppDb extends RootDb, Run>(deps: DrainDeps<Meta, AppDb, Run>) {
|
|
67
71
|
const { events: eventsTable, jobs: jobsTable } = deps.tables;
|
|
68
72
|
const { decodeStoredEvent } = deps.history;
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
const known = [...effectsByName.keys()];
|
|
73
|
+
const known = deps.effects.map(effect => effect.name);
|
|
72
74
|
|
|
73
75
|
// Every time here is database time, so a worker with a skewed clock
|
|
74
76
|
// cannot take a live lease or push a retry into the far future.
|
|
@@ -149,7 +151,13 @@ export function createDrain<Meta extends z.ZodObject, AppDb extends RootDb, Run>
|
|
|
149
151
|
};
|
|
150
152
|
};
|
|
151
153
|
|
|
152
|
-
const runJob = async (
|
|
154
|
+
const runJob = async (
|
|
155
|
+
db: AppDb & RootDb,
|
|
156
|
+
job: JobRow,
|
|
157
|
+
signal: AbortSignal,
|
|
158
|
+
leaseSeconds: number,
|
|
159
|
+
effectsByName: ReadonlyMap<string, Effect<z.output<Meta>, AppDb, Run>>,
|
|
160
|
+
): Promise<void> => {
|
|
153
161
|
const effect = effectsByName.get(job.effect);
|
|
154
162
|
const lost = new AbortController();
|
|
155
163
|
const stopRenewing = renewWhile(db, job, leaseSeconds, lost);
|
|
@@ -194,9 +202,17 @@ export function createDrain<Meta extends z.ZodObject, AppDb extends RootDb, Run>
|
|
|
194
202
|
|
|
195
203
|
return async (
|
|
196
204
|
db: AppDb & RootDb,
|
|
197
|
-
opts?: {
|
|
205
|
+
opts?: {
|
|
206
|
+
readonly batchSize?: number;
|
|
207
|
+
readonly signal?: AbortSignal;
|
|
208
|
+
readonly leaseSeconds?: number;
|
|
209
|
+
readonly services?: HandlerServices;
|
|
210
|
+
},
|
|
198
211
|
): Promise<number> => {
|
|
199
212
|
assertRootDb(db, 'drain');
|
|
213
|
+
// Before any claim: a missing service refuses the drain, not one job after another.
|
|
214
|
+
const { effects } = bindHandlers({ effects: deps.effects, taps: [] }, opts?.services);
|
|
215
|
+
const effectsByName = new Map(effects.map(effect => [effect.name, effect]));
|
|
200
216
|
deps.hooks.freeze();
|
|
201
217
|
const batchSize = opts?.batchSize ?? 20;
|
|
202
218
|
const leaseSeconds = opts?.leaseSeconds ?? DEFAULT_LEASE_SECONDS;
|
|
@@ -213,7 +229,7 @@ export function createDrain<Meta extends z.ZodObject, AppDb extends RootDb, Run>
|
|
|
213
229
|
const job = await claimOne(db, leaseSeconds);
|
|
214
230
|
if (job === undefined) break;
|
|
215
231
|
claimed += 1;
|
|
216
|
-
await runJob(db, job, signal, leaseSeconds);
|
|
232
|
+
await runJob(db, job, signal, leaseSeconds, effectsByName);
|
|
217
233
|
}
|
|
218
234
|
return claimed;
|
|
219
235
|
};
|
|
@@ -293,7 +309,12 @@ export type WorkerDeps<AppDb extends RootDb> = {
|
|
|
293
309
|
/** Runs one batch. The journal passes its own `drain` method, looked up on each call. */
|
|
294
310
|
readonly drain: (
|
|
295
311
|
db: AppDb & RootDb,
|
|
296
|
-
opts?: {
|
|
312
|
+
opts?: {
|
|
313
|
+
readonly batchSize?: number;
|
|
314
|
+
readonly signal?: AbortSignal;
|
|
315
|
+
readonly leaseSeconds?: number;
|
|
316
|
+
readonly services?: HandlerServices;
|
|
317
|
+
},
|
|
297
318
|
) => Promise<number>;
|
|
298
319
|
/** One batch per async projection. Returns how many events were scanned. */
|
|
299
320
|
readonly catchUp: (
|
|
@@ -301,7 +322,9 @@ export type WorkerDeps<AppDb extends RootDb> = {
|
|
|
301
322
|
opts?: { readonly batchSize?: number },
|
|
302
323
|
) => Promise<{ readonly scanned: number }>;
|
|
303
324
|
/** Fire the taps for one wake payload. */
|
|
304
|
-
readonly dispatch: (db: AppDb & RootDb, payload: string) => Promise<void>;
|
|
325
|
+
readonly dispatch: (db: AppDb & RootDb, payload: string, services: HandlerServices | undefined) => Promise<void>;
|
|
326
|
+
/** Throws when an effect or a tap is missing a service in `services`. */
|
|
327
|
+
readonly checkServices: (services: HandlerServices | undefined) => void;
|
|
305
328
|
readonly tapNames: readonly string[];
|
|
306
329
|
};
|
|
307
330
|
|
|
@@ -427,7 +450,8 @@ export function createCatchUp<Meta extends z.ZodObject, Tx extends TransactionSo
|
|
|
427
450
|
/** What tap dispatch needs from the journal. */
|
|
428
451
|
export type TapDispatchDeps<Meta extends z.ZodObject, AppDb extends RootDb> = {
|
|
429
452
|
readonly tables: Pick<JournalTables, 'events'>;
|
|
430
|
-
|
|
453
|
+
/** As composed: a tap may still have needs. Each dispatch binds them from its `services`. */
|
|
454
|
+
readonly taps: readonly Tap<z.output<Meta>, AppDb, Needs>[];
|
|
431
455
|
readonly history: Pick<History<Meta>, 'decodeStoredEvent'>;
|
|
432
456
|
};
|
|
433
457
|
|
|
@@ -451,10 +475,15 @@ export function createTapDispatch<Meta extends z.ZodObject, AppDb extends RootDb
|
|
|
451
475
|
const types = [...new Set(deps.taps.flatMap(tap => [...tap.types]))];
|
|
452
476
|
|
|
453
477
|
/** Load the committed events in the range that some tap wants, and run each matching tap. Errors are logged, never thrown. */
|
|
454
|
-
const dispatch = async (
|
|
478
|
+
const dispatch = async (
|
|
479
|
+
db: AppDb & RootDb,
|
|
480
|
+
payload: string,
|
|
481
|
+
services: HandlerServices | undefined,
|
|
482
|
+
): Promise<void> => {
|
|
455
483
|
if (deps.taps.length === 0) return;
|
|
456
484
|
const range = parseRange(payload);
|
|
457
485
|
if (range === undefined) return;
|
|
486
|
+
const { taps } = bindHandlers({ effects: [], taps: deps.taps }, services);
|
|
458
487
|
const rows = await db
|
|
459
488
|
.select()
|
|
460
489
|
.from(eventsTable)
|
|
@@ -468,7 +497,7 @@ export function createTapDispatch<Meta extends z.ZodObject, AppDb extends RootDb
|
|
|
468
497
|
)
|
|
469
498
|
.orderBy(asc(eventsTable.seq));
|
|
470
499
|
for (const row of rows) {
|
|
471
|
-
for (const tap of
|
|
500
|
+
for (const tap of taps) {
|
|
472
501
|
if (!tap.types.has(row.type) || !tap.entities.some(entity => entity.kind === row.kind)) continue;
|
|
473
502
|
try {
|
|
474
503
|
const event = deps.history.decodeStoredEvent(
|
|
@@ -507,9 +536,11 @@ export function createWorkers<AppDb extends RootDb>(deps: WorkerDeps<AppDb>) {
|
|
|
507
536
|
readonly leaseSeconds?: number;
|
|
508
537
|
readonly listen?: WakeListener;
|
|
509
538
|
readonly taps?: boolean;
|
|
539
|
+
readonly services?: HandlerServices;
|
|
510
540
|
},
|
|
511
541
|
): JournalWorker => {
|
|
512
542
|
assertRootDb(db, 'worker');
|
|
543
|
+
deps.checkServices(opts?.services);
|
|
513
544
|
if (deps.tapNames.length > 0 && opts?.listen === undefined && opts?.taps !== false)
|
|
514
545
|
throw new Error(
|
|
515
546
|
`journal: this journal has taps (${deps.tapNames.join(', ')}) but the worker has no \`listen\`; taps fire only from the wake channel. Wire LISTEN on journal.wakeChannel, or pass { taps: false } for a worker that runs jobs only`,
|
|
@@ -551,6 +582,7 @@ export function createWorkers<AppDb extends RootDb>(deps: WorkerDeps<AppDb>) {
|
|
|
551
582
|
batchSize: opts?.batchSize,
|
|
552
583
|
signal: stopping.signal,
|
|
553
584
|
leaseSeconds: opts?.leaseSeconds,
|
|
585
|
+
services: opts?.services,
|
|
554
586
|
});
|
|
555
587
|
if (isRunning()) {
|
|
556
588
|
const caught = await deps.catchUp(db);
|
|
@@ -577,7 +609,7 @@ export function createWorkers<AppDb extends RootDb>(deps: WorkerDeps<AppDb>) {
|
|
|
577
609
|
unlisten = await opts.listen(payload => {
|
|
578
610
|
wake();
|
|
579
611
|
if (opts.taps === false) return;
|
|
580
|
-
dispatching = dispatching.then(() => deps.dispatch(db, payload)).catch(quiet);
|
|
612
|
+
dispatching = dispatching.then(() => deps.dispatch(db, payload, opts.services)).catch(quiet);
|
|
581
613
|
});
|
|
582
614
|
}
|
|
583
615
|
debugWorker.debug('start (poll {pollMs}ms{value2})', {
|
|
@@ -589,7 +621,6 @@ export function createWorkers<AppDb extends RootDb>(deps: WorkerDeps<AppDb>) {
|
|
|
589
621
|
async stop() {
|
|
590
622
|
running = false;
|
|
591
623
|
stopping.abort();
|
|
592
|
-
// eslint-disable-next-line drizzle/enforce-delete-with-where -- Set#delete, not a Drizzle query
|
|
593
624
|
wakers.delete(wake);
|
|
594
625
|
endSleep?.();
|
|
595
626
|
await unlisten?.();
|
package/src/write.ts
CHANGED
|
@@ -87,7 +87,7 @@ function dueAt(
|
|
|
87
87
|
|
|
88
88
|
/** Insert order is the version order. `RETURNING` does not promise it, so restore it. */
|
|
89
89
|
export function sortedByVersion<Row extends { readonly version: number }>(rows: readonly Row[]): Row[] {
|
|
90
|
-
return rows.
|
|
90
|
+
return [...rows].sort((a, b) => a.version - b.version);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/** What a write needs from the journal. */
|