@kronos-ts/messaging 0.1.0 → 0.2.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/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-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/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 +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/intercepting-query-bus.d.ts.map +1 -1
- package/dist/intercepting-query-bus.js.map +1 -1
- package/dist/query-bus.d.ts +8 -3
- package/dist/query-bus.d.ts.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/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/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 +3 -3
- package/src/emit-update.ts +3 -2
- package/src/event-scheduler.ts +96 -0
- package/src/in-memory-event-scheduler.ts +150 -0
- package/src/index.ts +22 -0
- package/src/intercepting-query-bus.ts +4 -3
- package/src/query-bus.ts +8 -3
- package/src/simple-query-bus.ts +8 -6
- package/src/subscription-filter.ts +85 -0
- package/src/transaction.ts +84 -0
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)
|
|
@@ -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
|
+
}
|
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
|
+
}
|