@kronos-ts/messaging 0.1.1 → 0.3.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.
- package/dist/command-handler.d.ts +15 -15
- package/dist/command-handler.d.ts.map +1 -1
- package/dist/command-handler.js.map +1 -1
- package/dist/command-handling-module.js +2 -2
- package/dist/command-handling-module.js.map +1 -1
- package/dist/dead-lettering-handler.js +1 -1
- package/dist/dead-lettering-handler.js.map +1 -1
- package/dist/emit-update.d.ts +2 -1
- package/dist/emit-update.d.ts.map +1 -1
- package/dist/emit-update.js.map +1 -1
- package/dist/event-handler.d.ts +5 -5
- package/dist/event-handler.d.ts.map +1 -1
- package/dist/event-handler.js +2 -2
- package/dist/event-handler.js.map +1 -1
- package/dist/event-processor-builder.d.ts +3 -3
- package/dist/event-processor-builder.js +3 -3
- package/dist/event-scheduler.d.ts +95 -0
- package/dist/event-scheduler.d.ts.map +1 -0
- package/dist/event-scheduler.js +47 -0
- package/dist/event-scheduler.js.map +1 -0
- package/dist/gateway.d.ts +7 -5
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +9 -12
- package/dist/gateway.js.map +1 -1
- package/dist/handler.d.ts +13 -13
- package/dist/handler.d.ts.map +1 -1
- package/dist/handler.js.map +1 -1
- package/dist/in-memory-event-scheduler.d.ts +45 -0
- package/dist/in-memory-event-scheduler.d.ts.map +1 -0
- package/dist/in-memory-event-scheduler.js +112 -0
- package/dist/in-memory-event-scheduler.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/intercepting-command-bus.d.ts +1 -1
- package/dist/intercepting-command-bus.d.ts.map +1 -1
- package/dist/intercepting-command-bus.js +3 -4
- package/dist/intercepting-command-bus.js.map +1 -1
- package/dist/intercepting-query-bus.d.ts +1 -1
- package/dist/intercepting-query-bus.d.ts.map +1 -1
- package/dist/intercepting-query-bus.js +3 -4
- package/dist/intercepting-query-bus.js.map +1 -1
- package/dist/interceptor.d.ts +18 -3
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/message.d.ts +4 -0
- package/dist/message.d.ts.map +1 -1
- package/dist/query-bus.d.ts +8 -3
- package/dist/query-bus.d.ts.map +1 -1
- package/dist/query-handler.d.ts +5 -5
- package/dist/query-handler.d.ts.map +1 -1
- package/dist/query-handler.js +2 -2
- package/dist/query-handler.js.map +1 -1
- package/dist/query-handling-module.js +1 -1
- package/dist/query-handling-module.js.map +1 -1
- package/dist/simple-command-bus.d.ts +11 -4
- package/dist/simple-command-bus.d.ts.map +1 -1
- package/dist/simple-command-bus.js +16 -10
- package/dist/simple-command-bus.js.map +1 -1
- package/dist/simple-query-bus.d.ts.map +1 -1
- package/dist/simple-query-bus.js +4 -3
- package/dist/simple-query-bus.js.map +1 -1
- package/dist/streaming-event-processor.js +1 -1
- package/dist/streaming-event-processor.js.map +1 -1
- package/dist/subscribing-event-processor.js +1 -1
- package/dist/subscribing-event-processor.js.map +1 -1
- package/dist/subscription-filter.d.ts +43 -0
- package/dist/subscription-filter.d.ts.map +1 -0
- package/dist/subscription-filter.js +71 -0
- package/dist/subscription-filter.js.map +1 -0
- package/dist/tracking-event-processor.js +1 -1
- package/dist/tracking-event-processor.js.map +1 -1
- package/dist/transaction.d.ts +31 -0
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +80 -0
- package/dist/transaction.js.map +1 -1
- package/package.json +1 -1
- package/src/command-handler.ts +15 -28
- package/src/command-handling-module.ts +2 -2
- package/src/dead-lettering-handler.ts +1 -1
- package/src/emit-update.ts +3 -2
- package/src/event-handler.ts +5 -8
- package/src/event-processor-builder.ts +3 -3
- package/src/event-scheduler.ts +96 -0
- package/src/gateway.ts +14 -22
- package/src/handler.ts +13 -22
- package/src/in-memory-event-scheduler.ts +150 -0
- package/src/index.ts +23 -1
- package/src/intercepting-command-bus.ts +7 -6
- package/src/intercepting-query-bus.ts +11 -9
- package/src/interceptor.ts +21 -4
- package/src/message.ts +5 -0
- package/src/query-bus.ts +8 -3
- package/src/query-handler.ts +5 -5
- package/src/query-handling-module.ts +1 -1
- package/src/simple-command-bus.ts +17 -11
- package/src/simple-query-bus.ts +8 -6
- package/src/streaming-event-processor.ts +1 -1
- package/src/subscribing-event-processor.ts +1 -1
- package/src/subscription-filter.ts +85 -0
- package/src/tracking-event-processor.ts +1 -1
- package/src/transaction.ts +84 -0
package/src/interceptor.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Metadata } from "@kronos-ts/common"
|
|
2
1
|
import type { Message } from "./message.js"
|
|
3
2
|
|
|
4
3
|
/**
|
|
@@ -38,11 +37,29 @@ export interface DispatchInterceptor<M extends Message = Message> {
|
|
|
38
37
|
* accessors (`getResource` / `setResource`) — no `ProcessingContext`
|
|
39
38
|
* parameter is threaded.
|
|
40
39
|
*
|
|
40
|
+
* The first argument is the full message object. Prefer keeping it as
|
|
41
|
+
* `message` when transforming or inspecting broad message details:
|
|
42
|
+
*
|
|
43
|
+
* ```
|
|
44
|
+
* app.handlerInterceptor(async (message, next) => {
|
|
45
|
+
* const { payload, metadata, timestamp } = message
|
|
46
|
+
* return next({
|
|
47
|
+
* ...message,
|
|
48
|
+
* metadata: { ...metadata, tenantId: "tenant-1" },
|
|
49
|
+
* })
|
|
50
|
+
* })
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
41
53
|
* The `next` function calls the next interceptor in the chain, or the
|
|
42
|
-
* actual handler if this is the last interceptor.
|
|
54
|
+
* actual handler if this is the last interceptor. Call `next()` to proceed
|
|
55
|
+
* with the current message, or `next(replacementMessage)` to proceed with a
|
|
56
|
+
* transformed message.
|
|
43
57
|
*
|
|
44
58
|
* To skip handling entirely, don't call `next()` and return a result directly.
|
|
45
59
|
*/
|
|
46
|
-
export interface HandlerInterceptor<
|
|
47
|
-
|
|
60
|
+
export interface HandlerInterceptor<
|
|
61
|
+
M extends Message = Message,
|
|
62
|
+
R = unknown,
|
|
63
|
+
> {
|
|
64
|
+
(message: M, next: (message?: M) => Promise<R>): Promise<R>
|
|
48
65
|
}
|
package/src/message.ts
CHANGED
|
@@ -35,6 +35,11 @@ export interface EventMessage<P = unknown> extends Message<P> {
|
|
|
35
35
|
readonly tags: ReadonlyArray<{ readonly key: string; readonly value: string }>
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export interface SequencedEventMessage<P = unknown> extends EventMessage<P> {
|
|
39
|
+
/** Stream position when the source has one; absent for push-only delivery. */
|
|
40
|
+
readonly sequence?: bigint
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
/**
|
|
39
44
|
* A query message — dispatched to handler(s) that can answer it.
|
|
40
45
|
*/
|
package/src/query-bus.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { QueryMessage } from "./message.js"
|
|
2
2
|
import type { SubscriptionQueryResult } from "./subscription-query.js"
|
|
3
|
+
import type { SubscriptionFilter } from "./subscription-filter.js"
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* The query bus — low-level infrastructure for dispatching query messages.
|
|
@@ -43,10 +44,14 @@ export interface QueryBus {
|
|
|
43
44
|
* Emit an update to all active subscription queries matching the filter.
|
|
44
45
|
* When called within an active UnitOfWork (detected via ALS), the update is
|
|
45
46
|
* deferred to AFTER_COMMIT.
|
|
47
|
+
*
|
|
48
|
+
* The filter can be either a function (local-only when a distributed bus is
|
|
49
|
+
* in use) or a structured `payloadEquals` predicate (crosses transports).
|
|
50
|
+
* See {@link SubscriptionFilter}.
|
|
46
51
|
*/
|
|
47
52
|
emitUpdate(
|
|
48
53
|
queryName: string,
|
|
49
|
-
filter:
|
|
54
|
+
filter: SubscriptionFilter,
|
|
50
55
|
update: unknown,
|
|
51
56
|
): Promise<void>
|
|
52
57
|
|
|
@@ -55,7 +60,7 @@ export interface QueryBus {
|
|
|
55
60
|
*/
|
|
56
61
|
completeSubscription(
|
|
57
62
|
queryName: string,
|
|
58
|
-
filter?:
|
|
63
|
+
filter?: SubscriptionFilter,
|
|
59
64
|
): Promise<void>
|
|
60
65
|
|
|
61
66
|
/**
|
|
@@ -64,6 +69,6 @@ export interface QueryBus {
|
|
|
64
69
|
completeSubscriptionExceptionally(
|
|
65
70
|
queryName: string,
|
|
66
71
|
error: Error,
|
|
67
|
-
filter?:
|
|
72
|
+
filter?: SubscriptionFilter,
|
|
68
73
|
): Promise<void>
|
|
69
74
|
}
|
package/src/query-handler.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { z } from "zod"
|
|
2
|
-
import type { Metadata } from "@kronos-ts/common"
|
|
3
2
|
import type { QueryDescriptor } from "./descriptor.js"
|
|
3
|
+
import type { QueryMessage } from "./message.js"
|
|
4
4
|
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
// Singular factory — mirrors commandHandler / eventHandler.
|
|
@@ -23,15 +23,15 @@ export interface QueryHandlerDefinition<
|
|
|
23
23
|
> {
|
|
24
24
|
readonly kind: "query-handler"
|
|
25
25
|
readonly descriptor: QueryDescriptor<Q, z.ZodType | undefined>
|
|
26
|
-
readonly handler: (
|
|
26
|
+
readonly handler: (message: QueryMessage<z.infer<Q>>) => Promise<R> | R
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Defines a singular query handler.
|
|
31
31
|
*
|
|
32
32
|
* ```
|
|
33
|
-
* const getCourseView = queryHandler(GetCourseView, async (
|
|
34
|
-
* const view = courseViews.get(
|
|
33
|
+
* const getCourseView = queryHandler(GetCourseView, async ({ payload, metadata }) => {
|
|
34
|
+
* const view = courseViews.get(payload.courseId)
|
|
35
35
|
* if (!view) throw new Error("not found")
|
|
36
36
|
* return view
|
|
37
37
|
* })
|
|
@@ -43,7 +43,7 @@ export interface QueryHandlerDefinition<
|
|
|
43
43
|
*/
|
|
44
44
|
export function queryHandler<Q extends z.ZodType, R>(
|
|
45
45
|
descriptor: QueryDescriptor<Q, z.ZodType | undefined>,
|
|
46
|
-
handler: (
|
|
46
|
+
handler: (message: QueryMessage<z.infer<Q>>) => Promise<R> | R,
|
|
47
47
|
): QueryHandlerDefinition<Q, R> {
|
|
48
48
|
return { kind: "query-handler", descriptor, handler }
|
|
49
49
|
}
|
|
@@ -31,7 +31,7 @@ export function registerQueryHandlersNatively(
|
|
|
31
31
|
for (const reg of handlers) {
|
|
32
32
|
const queryName = qualifiedNameToString(reg.descriptor.name)
|
|
33
33
|
let invocation = async (message: QueryMessage) =>
|
|
34
|
-
reg.handler(message
|
|
34
|
+
reg.handler(message)
|
|
35
35
|
if (deps.handlerEnhancer) {
|
|
36
36
|
invocation = deps.handlerEnhancer.wrapHandler(invocation, {
|
|
37
37
|
messageType: "query",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CommandBus } from "./command-bus.js"
|
|
2
2
|
import type { CommandMessage } from "./message.js"
|
|
3
|
-
import { runInNewUoW } from "./unit-of-work.js"
|
|
3
|
+
import { runInNewUoW, type UoWRunner } from "./unit-of-work.js"
|
|
4
4
|
import { qualifiedNameToString } from "@kronos-ts/common"
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -16,14 +16,20 @@ import { qualifiedNameToString } from "@kronos-ts/common"
|
|
|
16
16
|
* by sharing a transaction. DCB read-set / append-condition merging
|
|
17
17
|
* happens only WITHIN a single handler's UnitOfWork.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* The handler runs through `unitOfWorkRunner` — by default `runInNewUoW`, but
|
|
20
|
+
* the configurer injects the resolved `unitOfWorkFactory` slot (e.g. a
|
|
21
|
+
* transactional runner from a storage extension) so the per-command UoW
|
|
22
|
+
* carries whatever transaction that backend provides. This mirrors the
|
|
23
|
+
* distributed command buses (kronosdb / axon-server), which already run
|
|
24
|
+
* handlers through the configured runner. The runner is always built on
|
|
25
|
+
* `runInNewUoW`, so a command — primary OR nested via `send()` — still gets
|
|
26
|
+
* its own fresh UoW (and its own independent transaction); composition does
|
|
27
|
+
* not change AF5 isolation, only whether that fresh UoW has a transaction.
|
|
22
28
|
*
|
|
23
29
|
* Interceptor support is provided by wrapping with
|
|
24
30
|
* {@link createInterceptingCommandBus}.
|
|
25
31
|
*/
|
|
26
|
-
export function createSimpleCommandBus(): CommandBus {
|
|
32
|
+
export function createSimpleCommandBus(unitOfWorkRunner: UoWRunner = runInNewUoW): CommandBus {
|
|
27
33
|
const handlers = new Map<string, (message: CommandMessage) => Promise<unknown>>()
|
|
28
34
|
|
|
29
35
|
return {
|
|
@@ -34,12 +40,12 @@ export function createSimpleCommandBus(): CommandBus {
|
|
|
34
40
|
throw new Error(`No handler registered for command "${key}"`)
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
// AF5 parity: every command gets its own fresh UnitOfWork
|
|
38
|
-
// dispatched from inside another
|
|
39
|
-
// already run in the caller's context
|
|
40
|
-
// this one), so correlation data is carried
|
|
41
|
-
// before we cross into the new UoW.
|
|
42
|
-
return
|
|
43
|
+
// AF5 parity: every command gets its own fresh UnitOfWork (the runner is
|
|
44
|
+
// built on runInNewUoW), even when dispatched from inside another
|
|
45
|
+
// handler. Dispatch interceptors have already run in the caller's context
|
|
46
|
+
// (the intercepting bus wraps this one), so correlation data is carried
|
|
47
|
+
// on `message.metadata` before we cross into the new UoW.
|
|
48
|
+
return unitOfWorkRunner(message.metadata, () => handler(message))
|
|
43
49
|
},
|
|
44
50
|
|
|
45
51
|
subscribe(
|
package/src/simple-query-bus.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { QueryBus } from "./query-bus.js"
|
|
|
2
2
|
import type { QueryMessage } from "./message.js"
|
|
3
3
|
import type { SubscriptionQueryResult, UpdateHandler } from "./subscription-query.js"
|
|
4
4
|
import { createUpdateHandler, runAfterCommitOrImmediately } from "./subscription-query.js"
|
|
5
|
+
import type { SubscriptionFilter } from "./subscription-filter.js"
|
|
6
|
+
import { applySubscriptionFilter } from "./subscription-filter.js"
|
|
5
7
|
import { runInUoW } from "./unit-of-work.js"
|
|
6
8
|
import { qualifiedNameToString } from "@kronos-ts/common"
|
|
7
9
|
|
|
@@ -95,7 +97,7 @@ export function createSimpleQueryBus(): QueryBus {
|
|
|
95
97
|
|
|
96
98
|
async emitUpdate(
|
|
97
99
|
queryName: string,
|
|
98
|
-
filter:
|
|
100
|
+
filter: SubscriptionFilter,
|
|
99
101
|
update: unknown,
|
|
100
102
|
): Promise<void> {
|
|
101
103
|
runAfterCommitOrImmediately(() => {
|
|
@@ -107,7 +109,7 @@ export function createSimpleQueryBus(): QueryBus {
|
|
|
107
109
|
|
|
108
110
|
const handlerQueryName = qualifiedNameToString(handler.query.name)
|
|
109
111
|
if (handlerQueryName !== queryName) continue
|
|
110
|
-
if (!filter
|
|
112
|
+
if (!applySubscriptionFilter(filter, handler.query.payload)) continue
|
|
111
113
|
|
|
112
114
|
const accepted = handler.offer(update)
|
|
113
115
|
if (!accepted) {
|
|
@@ -122,13 +124,13 @@ export function createSimpleQueryBus(): QueryBus {
|
|
|
122
124
|
|
|
123
125
|
async completeSubscription(
|
|
124
126
|
queryName: string,
|
|
125
|
-
filter?:
|
|
127
|
+
filter?: SubscriptionFilter,
|
|
126
128
|
): Promise<void> {
|
|
127
129
|
runAfterCommitOrImmediately(() => {
|
|
128
130
|
for (const [id, handler] of subscriptions) {
|
|
129
131
|
const handlerQueryName = qualifiedNameToString(handler.query.name)
|
|
130
132
|
if (handlerQueryName !== queryName) continue
|
|
131
|
-
if (filter && !filter
|
|
133
|
+
if (filter && !applySubscriptionFilter(filter, handler.query.payload)) continue
|
|
132
134
|
|
|
133
135
|
handler.complete()
|
|
134
136
|
subscriptions.delete(id)
|
|
@@ -139,13 +141,13 @@ export function createSimpleQueryBus(): QueryBus {
|
|
|
139
141
|
async completeSubscriptionExceptionally(
|
|
140
142
|
queryName: string,
|
|
141
143
|
error: Error,
|
|
142
|
-
filter?:
|
|
144
|
+
filter?: SubscriptionFilter,
|
|
143
145
|
): Promise<void> {
|
|
144
146
|
runAfterCommitOrImmediately(() => {
|
|
145
147
|
for (const [id, handler] of subscriptions) {
|
|
146
148
|
const handlerQueryName = qualifiedNameToString(handler.query.name)
|
|
147
149
|
if (handlerQueryName !== queryName) continue
|
|
148
|
-
if (filter && !filter
|
|
150
|
+
if (filter && !applySubscriptionFilter(filter, handler.query.payload)) continue
|
|
149
151
|
|
|
150
152
|
handler.completeExceptionally(error)
|
|
151
153
|
subscriptions.delete(id)
|
|
@@ -249,7 +249,7 @@ export function createStreamingEventProcessor(
|
|
|
249
249
|
|
|
250
250
|
for (const reg of handlers) {
|
|
251
251
|
try {
|
|
252
|
-
await reg.handler(event
|
|
252
|
+
await reg.handler({ ...event, sequence: sequencedEvent.sequence })
|
|
253
253
|
} catch (err) {
|
|
254
254
|
await errorHandler.handleError(err, eventName, sequencedEvent.sequence)
|
|
255
255
|
}
|
|
@@ -135,7 +135,7 @@ export function createSubscribingEventProcessor(
|
|
|
135
135
|
|
|
136
136
|
for (const reg of handlers) {
|
|
137
137
|
try {
|
|
138
|
-
await reg.handler(event
|
|
138
|
+
await reg.handler(event)
|
|
139
139
|
} catch (err) {
|
|
140
140
|
// SubscribingEventProcessor doesn't have position tracking,
|
|
141
141
|
// so pass -1n as position indicator
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A predicate over a query payload, used by `emitUpdate` to decide which
|
|
3
|
+
* subscribers should receive an update.
|
|
4
|
+
*
|
|
5
|
+
* Two forms are supported:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Function** — `(payload) => boolean`. Easy to write but cannot cross a
|
|
8
|
+
* network boundary because functions are not serializable. Use for
|
|
9
|
+
* in-process / single-segment query buses.
|
|
10
|
+
*
|
|
11
|
+
* 2. **Structured `payloadEquals`** — `{ payloadEquals: Partial<P> }`. Every
|
|
12
|
+
* key in the partial must deep-equal the same key in the subscriber's
|
|
13
|
+
* query payload. Serializable, so it ships across distributed query bus
|
|
14
|
+
* transports (e.g. RabbitMQ broadcasts).
|
|
15
|
+
*
|
|
16
|
+
* Both forms work locally; only the structured form crosses processes when a
|
|
17
|
+
* distributed query bus is in use.
|
|
18
|
+
*/
|
|
19
|
+
export type SubscriptionFilter<P = unknown> =
|
|
20
|
+
| ((payload: P) => boolean)
|
|
21
|
+
| { readonly payloadEquals: Partial<P> }
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Helper that builds a structured filter from a partial payload.
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* emitUpdate(GetCourseView, payloadEquals({ courseId: e.courseId }), view)
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* Prefer this over a function filter when you want updates to fan out across
|
|
31
|
+
* a distributed query bus.
|
|
32
|
+
*/
|
|
33
|
+
export function payloadEquals<P>(partial: Partial<P>): { payloadEquals: Partial<P> } {
|
|
34
|
+
return { payloadEquals: partial }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Evaluate a {@link SubscriptionFilter} against a payload. */
|
|
38
|
+
export function applySubscriptionFilter<P>(filter: SubscriptionFilter<P>, payload: P): boolean {
|
|
39
|
+
if (typeof filter === "function") return filter(payload)
|
|
40
|
+
return matchesPayloadEquals(payload, filter.payloadEquals)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Extract the structured form, if any, for serialization across a transport. */
|
|
44
|
+
export function extractStructuredFilter<P>(
|
|
45
|
+
filter: SubscriptionFilter<P> | undefined,
|
|
46
|
+
): { payloadEquals: Partial<P> } | undefined {
|
|
47
|
+
if (!filter) return undefined
|
|
48
|
+
if (typeof filter === "function") return undefined
|
|
49
|
+
return filter
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Deep equality on the keys defined in `expected`. */
|
|
53
|
+
export function matchesPayloadEquals<P>(payload: P, expected: Partial<P>): boolean {
|
|
54
|
+
if (payload === null || typeof payload !== "object") {
|
|
55
|
+
return Object.keys(expected).length === 0
|
|
56
|
+
}
|
|
57
|
+
for (const key of Object.keys(expected) as Array<keyof P>) {
|
|
58
|
+
if (!deepEqual((payload as P)[key], expected[key])) return false
|
|
59
|
+
}
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function deepEqual(a: unknown, b: unknown): boolean {
|
|
64
|
+
if (a === b) return true
|
|
65
|
+
if (a === null || b === null) return false
|
|
66
|
+
if (typeof a !== typeof b) return false
|
|
67
|
+
if (typeof a !== "object") return false
|
|
68
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false
|
|
69
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
70
|
+
if (a.length !== b.length) return false
|
|
71
|
+
for (let i = 0; i < a.length; i++) {
|
|
72
|
+
if (!deepEqual(a[i], b[i])) return false
|
|
73
|
+
}
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
const ao = a as Record<string, unknown>
|
|
77
|
+
const bo = b as Record<string, unknown>
|
|
78
|
+
const aKeys = Object.keys(ao)
|
|
79
|
+
const bKeys = Object.keys(bo)
|
|
80
|
+
if (aKeys.length !== bKeys.length) return false
|
|
81
|
+
for (const key of aKeys) {
|
|
82
|
+
if (!deepEqual(ao[key], bo[key])) return false
|
|
83
|
+
}
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
@@ -268,7 +268,7 @@ export function createTrackingEventProcessor(
|
|
|
268
268
|
|
|
269
269
|
for (const reg of handlers) {
|
|
270
270
|
try {
|
|
271
|
-
await reg.handler(event
|
|
271
|
+
await reg.handler({ ...event, sequence: sequencedEvent.sequence })
|
|
272
272
|
} catch (err) {
|
|
273
273
|
await errorHandler.handleError(err, eventName, sequencedEvent.sequence)
|
|
274
274
|
}
|
package/src/transaction.ts
CHANGED
|
@@ -8,6 +8,18 @@ import {
|
|
|
8
8
|
} from "./processing-state.js"
|
|
9
9
|
import type { UoWRunner } from "./unit-of-work.js"
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Resource key holding a deferred-begin factory installed by
|
|
13
|
+
* {@link lazyTransactionalUnitOfWorkFactory}. The factory begins the tx on
|
|
14
|
+
* first call, registers commit/rollback hooks on the UoW, caches the
|
|
15
|
+
* resulting tx in {@link TRANSACTION_KEY}, and returns it. Subsequent calls
|
|
16
|
+
* return the cached tx without a re-begin.
|
|
17
|
+
*
|
|
18
|
+
* NOT exported from the package barrel — components reach the lazily-begun
|
|
19
|
+
* tx through {@link getOrBeginActiveTransaction}.
|
|
20
|
+
*/
|
|
21
|
+
const LAZY_TX_FACTORY_KEY: ResourceKey<() => Promise<unknown>> = resourceKey("lazyTxFactory")
|
|
22
|
+
|
|
11
23
|
/**
|
|
12
24
|
* Manages transaction lifecycle. Users provide an implementation
|
|
13
25
|
* for their specific database/ORM.
|
|
@@ -96,3 +108,75 @@ export function transactionalUnitOfWorkFactory<T>(
|
|
|
96
108
|
})
|
|
97
109
|
}
|
|
98
110
|
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Lazy variant of {@link transactionalUnitOfWorkFactory}.
|
|
114
|
+
*
|
|
115
|
+
* Unlike the eager factory, no transaction is begun on UoW entry. Instead,
|
|
116
|
+
* a factory is installed in the UoW that opens the tx on the first call to
|
|
117
|
+
* {@link getOrBeginActiveTransaction}. Pure-read UoWs that never request a
|
|
118
|
+
* tx pay zero begin/commit cost and never claim a connection from the pool.
|
|
119
|
+
*
|
|
120
|
+
* On first request: the tx is begun, stored in {@link TRANSACTION_KEY},
|
|
121
|
+
* and commit/rollback hooks are registered. Subsequent requests within
|
|
122
|
+
* the same UoW return the cached tx — there is exactly one tx per UoW.
|
|
123
|
+
*
|
|
124
|
+
* Components that may write to the underlying store (event stores,
|
|
125
|
+
* schedulers, ORM integrations) reach the tx via
|
|
126
|
+
* {@link getOrBeginActiveTransaction}; read-only paths use
|
|
127
|
+
* {@link getActiveTransaction} so they observe an existing tx but do not
|
|
128
|
+
* provoke one to open.
|
|
129
|
+
*/
|
|
130
|
+
export function lazyTransactionalUnitOfWorkFactory<T>(
|
|
131
|
+
delegate: UoWRunner,
|
|
132
|
+
txManager: TransactionManager<T>,
|
|
133
|
+
): UoWRunner {
|
|
134
|
+
return async (metadata, action) => {
|
|
135
|
+
return delegate(metadata, async () => {
|
|
136
|
+
let tx: T | undefined
|
|
137
|
+
let committed = false
|
|
138
|
+
|
|
139
|
+
const factory = async (): Promise<T> => {
|
|
140
|
+
if (tx !== undefined) return tx
|
|
141
|
+
tx = await txManager.begin()
|
|
142
|
+
setResource(TRANSACTION_KEY, tx)
|
|
143
|
+
on(Phase.COMMIT, async () => {
|
|
144
|
+
if (tx === undefined) return
|
|
145
|
+
await txManager.commit(tx)
|
|
146
|
+
committed = true
|
|
147
|
+
})
|
|
148
|
+
onError(async () => {
|
|
149
|
+
if (tx === undefined || committed) return
|
|
150
|
+
await txManager.rollback(tx)
|
|
151
|
+
})
|
|
152
|
+
return tx
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setResource(LAZY_TX_FACTORY_KEY, factory as () => Promise<unknown>)
|
|
156
|
+
return action()
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Return the active UoW transaction, opening it if a lazy factory is
|
|
163
|
+
* installed and no tx has been begun yet. Returns the cached tx on
|
|
164
|
+
* subsequent calls within the same UoW.
|
|
165
|
+
*
|
|
166
|
+
* Returns `undefined` when no UoW is active OR when an active UoW has
|
|
167
|
+
* neither an existing tx nor a lazy factory installed (e.g., the app
|
|
168
|
+
* doesn't compose a TransactionManager). Callers that need a tx must
|
|
169
|
+
* decide what to do with `undefined` — typically fall back to opening
|
|
170
|
+
* an ad-hoc tx on their own driver.
|
|
171
|
+
*/
|
|
172
|
+
export async function getOrBeginActiveTransaction<T = unknown>(): Promise<T | undefined> {
|
|
173
|
+
const state = processingStateStorage.getStore()
|
|
174
|
+
if (!state) return undefined
|
|
175
|
+
const existing = state.resources.get(TRANSACTION_KEY.symbol) as T | undefined
|
|
176
|
+
if (existing !== undefined) return existing
|
|
177
|
+
const factory = state.resources.get(LAZY_TX_FACTORY_KEY.symbol) as
|
|
178
|
+
| (() => Promise<T>)
|
|
179
|
+
| undefined
|
|
180
|
+
if (factory === undefined) return undefined
|
|
181
|
+
return await factory()
|
|
182
|
+
}
|