@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.
Files changed (40) hide show
  1. package/dist/emit-update.d.ts +2 -1
  2. package/dist/emit-update.d.ts.map +1 -1
  3. package/dist/emit-update.js.map +1 -1
  4. package/dist/event-scheduler.d.ts +95 -0
  5. package/dist/event-scheduler.d.ts.map +1 -0
  6. package/dist/event-scheduler.js +47 -0
  7. package/dist/event-scheduler.js.map +1 -0
  8. package/dist/in-memory-event-scheduler.d.ts +45 -0
  9. package/dist/in-memory-event-scheduler.d.ts.map +1 -0
  10. package/dist/in-memory-event-scheduler.js +112 -0
  11. package/dist/in-memory-event-scheduler.js.map +1 -0
  12. package/dist/index.d.ts +4 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +5 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/intercepting-query-bus.d.ts.map +1 -1
  17. package/dist/intercepting-query-bus.js.map +1 -1
  18. package/dist/query-bus.d.ts +8 -3
  19. package/dist/query-bus.d.ts.map +1 -1
  20. package/dist/simple-query-bus.d.ts.map +1 -1
  21. package/dist/simple-query-bus.js +4 -3
  22. package/dist/simple-query-bus.js.map +1 -1
  23. package/dist/subscription-filter.d.ts +43 -0
  24. package/dist/subscription-filter.d.ts.map +1 -0
  25. package/dist/subscription-filter.js +71 -0
  26. package/dist/subscription-filter.js.map +1 -0
  27. package/dist/transaction.d.ts +31 -0
  28. package/dist/transaction.d.ts.map +1 -1
  29. package/dist/transaction.js +80 -0
  30. package/dist/transaction.js.map +1 -1
  31. package/package.json +3 -3
  32. package/src/emit-update.ts +3 -2
  33. package/src/event-scheduler.ts +96 -0
  34. package/src/in-memory-event-scheduler.ts +150 -0
  35. package/src/index.ts +22 -0
  36. package/src/intercepting-query-bus.ts +4 -3
  37. package/src/query-bus.ts +8 -3
  38. package/src/simple-query-bus.ts +8 -6
  39. package/src/subscription-filter.ts +85 -0
  40. package/src/transaction.ts +84 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Helper that builds a structured filter from a partial payload.
3
+ *
4
+ * ```ts
5
+ * emitUpdate(GetCourseView, payloadEquals({ courseId: e.courseId }), view)
6
+ * ```
7
+ *
8
+ * Prefer this over a function filter when you want updates to fan out across
9
+ * a distributed query bus.
10
+ */
11
+ export function payloadEquals(partial) {
12
+ return { payloadEquals: partial };
13
+ }
14
+ /** Evaluate a {@link SubscriptionFilter} against a payload. */
15
+ export function applySubscriptionFilter(filter, payload) {
16
+ if (typeof filter === "function")
17
+ return filter(payload);
18
+ return matchesPayloadEquals(payload, filter.payloadEquals);
19
+ }
20
+ /** Extract the structured form, if any, for serialization across a transport. */
21
+ export function extractStructuredFilter(filter) {
22
+ if (!filter)
23
+ return undefined;
24
+ if (typeof filter === "function")
25
+ return undefined;
26
+ return filter;
27
+ }
28
+ /** Deep equality on the keys defined in `expected`. */
29
+ export function matchesPayloadEquals(payload, expected) {
30
+ if (payload === null || typeof payload !== "object") {
31
+ return Object.keys(expected).length === 0;
32
+ }
33
+ for (const key of Object.keys(expected)) {
34
+ if (!deepEqual(payload[key], expected[key]))
35
+ return false;
36
+ }
37
+ return true;
38
+ }
39
+ function deepEqual(a, b) {
40
+ if (a === b)
41
+ return true;
42
+ if (a === null || b === null)
43
+ return false;
44
+ if (typeof a !== typeof b)
45
+ return false;
46
+ if (typeof a !== "object")
47
+ return false;
48
+ if (Array.isArray(a) !== Array.isArray(b))
49
+ return false;
50
+ if (Array.isArray(a) && Array.isArray(b)) {
51
+ if (a.length !== b.length)
52
+ return false;
53
+ for (let i = 0; i < a.length; i++) {
54
+ if (!deepEqual(a[i], b[i]))
55
+ return false;
56
+ }
57
+ return true;
58
+ }
59
+ const ao = a;
60
+ const bo = b;
61
+ const aKeys = Object.keys(ao);
62
+ const bKeys = Object.keys(bo);
63
+ if (aKeys.length !== bKeys.length)
64
+ return false;
65
+ for (const key of aKeys) {
66
+ if (!deepEqual(ao[key], bo[key]))
67
+ return false;
68
+ }
69
+ return true;
70
+ }
71
+ //# sourceMappingURL=subscription-filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscription-filter.js","sourceRoot":"","sources":["../src/subscription-filter.ts"],"names":[],"mappings":"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"}
@@ -57,4 +57,35 @@ export declare function getActiveTransaction<T = unknown>(): T | undefined;
57
57
  * Migration) can rename if the kronos() app API warrants.
58
58
  */
59
59
  export declare function transactionalUnitOfWorkFactory<T>(delegate: UoWRunner, txManager: TransactionManager<T>): UoWRunner;
60
+ /**
61
+ * Lazy variant of {@link transactionalUnitOfWorkFactory}.
62
+ *
63
+ * Unlike the eager factory, no transaction is begun on UoW entry. Instead,
64
+ * a factory is installed in the UoW that opens the tx on the first call to
65
+ * {@link getOrBeginActiveTransaction}. Pure-read UoWs that never request a
66
+ * tx pay zero begin/commit cost and never claim a connection from the pool.
67
+ *
68
+ * On first request: the tx is begun, stored in {@link TRANSACTION_KEY},
69
+ * and commit/rollback hooks are registered. Subsequent requests within
70
+ * the same UoW return the cached tx — there is exactly one tx per UoW.
71
+ *
72
+ * Components that may write to the underlying store (event stores,
73
+ * schedulers, ORM integrations) reach the tx via
74
+ * {@link getOrBeginActiveTransaction}; read-only paths use
75
+ * {@link getActiveTransaction} so they observe an existing tx but do not
76
+ * provoke one to open.
77
+ */
78
+ export declare function lazyTransactionalUnitOfWorkFactory<T>(delegate: UoWRunner, txManager: TransactionManager<T>): UoWRunner;
79
+ /**
80
+ * Return the active UoW transaction, opening it if a lazy factory is
81
+ * installed and no tx has been begun yet. Returns the cached tx on
82
+ * subsequent calls within the same UoW.
83
+ *
84
+ * Returns `undefined` when no UoW is active OR when an active UoW has
85
+ * neither an existing tx nor a lazy factory installed (e.g., the app
86
+ * doesn't compose a TransactionManager). Callers that need a tx must
87
+ * decide what to do with `undefined` — typically fall back to opening
88
+ * an ad-hoc tx on their own driver.
89
+ */
90
+ export declare function getOrBeginActiveTransaction<T = unknown>(): Promise<T | undefined>;
60
91
  //# sourceMappingURL=transaction.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAQjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAElD;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,GAAG,OAAO;IAC7C,KAAK,IAAI,OAAO,CAAC,CAAC,CAAC,CAAA;IACnB,MAAM,CAAC,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,QAAQ,CAAC,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/B;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,kBAAkB,CAAC,IAAI,CAAC,CAM/D;AAED,uGAAuG;AACvG,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,OAAO,CAA8B,CAAA;AAE/E;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,GAAG,OAAO,KAAK,CAAC,GAAG,SAAS,CAIjE;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,8BAA8B,CAAC,CAAC,EAC9C,QAAQ,EAAE,SAAS,EACnB,SAAS,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAC/B,SAAS,CAcX"}
1
+ {"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAQjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAclD;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,GAAG,OAAO;IAC7C,KAAK,IAAI,OAAO,CAAC,CAAC,CAAC,CAAA;IACnB,MAAM,CAAC,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,QAAQ,CAAC,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/B;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,kBAAkB,CAAC,IAAI,CAAC,CAM/D;AAED,uGAAuG;AACvG,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,OAAO,CAA8B,CAAA;AAE/E;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,GAAG,OAAO,KAAK,CAAC,GAAG,SAAS,CAIjE;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,8BAA8B,CAAC,CAAC,EAC9C,QAAQ,EAAE,SAAS,EACnB,SAAS,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAC/B,SAAS,CAcX;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kCAAkC,CAAC,CAAC,EAClD,QAAQ,EAAE,SAAS,EACnB,SAAS,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAC/B,SAAS,CA0BX;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,OAAO,KAAK,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAUvF"}
@@ -1,5 +1,16 @@
1
1
  import { resourceKey } from "@kronos-ts/common";
2
2
  import { processingStateStorage, setResource, on, onError, Phase, } from "./processing-state.js";
3
+ /**
4
+ * Resource key holding a deferred-begin factory installed by
5
+ * {@link lazyTransactionalUnitOfWorkFactory}. The factory begins the tx on
6
+ * first call, registers commit/rollback hooks on the UoW, caches the
7
+ * resulting tx in {@link TRANSACTION_KEY}, and returns it. Subsequent calls
8
+ * return the cached tx without a re-begin.
9
+ *
10
+ * NOT exported from the package barrel — components reach the lazily-begun
11
+ * tx through {@link getOrBeginActiveTransaction}.
12
+ */
13
+ const LAZY_TX_FACTORY_KEY = resourceKey("lazyTxFactory");
3
14
  /**
4
15
  * A no-op transaction manager for when no database transactions are needed.
5
16
  */
@@ -71,4 +82,73 @@ export function transactionalUnitOfWorkFactory(delegate, txManager) {
71
82
  });
72
83
  };
73
84
  }
85
+ /**
86
+ * Lazy variant of {@link transactionalUnitOfWorkFactory}.
87
+ *
88
+ * Unlike the eager factory, no transaction is begun on UoW entry. Instead,
89
+ * a factory is installed in the UoW that opens the tx on the first call to
90
+ * {@link getOrBeginActiveTransaction}. Pure-read UoWs that never request a
91
+ * tx pay zero begin/commit cost and never claim a connection from the pool.
92
+ *
93
+ * On first request: the tx is begun, stored in {@link TRANSACTION_KEY},
94
+ * and commit/rollback hooks are registered. Subsequent requests within
95
+ * the same UoW return the cached tx — there is exactly one tx per UoW.
96
+ *
97
+ * Components that may write to the underlying store (event stores,
98
+ * schedulers, ORM integrations) reach the tx via
99
+ * {@link getOrBeginActiveTransaction}; read-only paths use
100
+ * {@link getActiveTransaction} so they observe an existing tx but do not
101
+ * provoke one to open.
102
+ */
103
+ export function lazyTransactionalUnitOfWorkFactory(delegate, txManager) {
104
+ return async (metadata, action) => {
105
+ return delegate(metadata, async () => {
106
+ let tx;
107
+ let committed = false;
108
+ const factory = async () => {
109
+ if (tx !== undefined)
110
+ return tx;
111
+ tx = await txManager.begin();
112
+ setResource(TRANSACTION_KEY, tx);
113
+ on(Phase.COMMIT, async () => {
114
+ if (tx === undefined)
115
+ return;
116
+ await txManager.commit(tx);
117
+ committed = true;
118
+ });
119
+ onError(async () => {
120
+ if (tx === undefined || committed)
121
+ return;
122
+ await txManager.rollback(tx);
123
+ });
124
+ return tx;
125
+ };
126
+ setResource(LAZY_TX_FACTORY_KEY, factory);
127
+ return action();
128
+ });
129
+ };
130
+ }
131
+ /**
132
+ * Return the active UoW transaction, opening it if a lazy factory is
133
+ * installed and no tx has been begun yet. Returns the cached tx on
134
+ * subsequent calls within the same UoW.
135
+ *
136
+ * Returns `undefined` when no UoW is active OR when an active UoW has
137
+ * neither an existing tx nor a lazy factory installed (e.g., the app
138
+ * doesn't compose a TransactionManager). Callers that need a tx must
139
+ * decide what to do with `undefined` — typically fall back to opening
140
+ * an ad-hoc tx on their own driver.
141
+ */
142
+ export async function getOrBeginActiveTransaction() {
143
+ const state = processingStateStorage.getStore();
144
+ if (!state)
145
+ return undefined;
146
+ const existing = state.resources.get(TRANSACTION_KEY.symbol);
147
+ if (existing !== undefined)
148
+ return existing;
149
+ const factory = state.resources.get(LAZY_TX_FACTORY_KEY.symbol);
150
+ if (factory === undefined)
151
+ return undefined;
152
+ return await factory();
153
+ }
74
154
  //# sourceMappingURL=transaction.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"transaction.js","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAoB,MAAM,mBAAmB,CAAA;AACjE,OAAO,EACL,sBAAsB,EACtB,WAAW,EACX,EAAE,EACF,OAAO,EACP,KAAK,GACN,MAAM,uBAAuB,CAAA;AAe9B;;GAEG;AACH,MAAM,UAAU,oBAAoB;IAClC,OAAO;QACL,KAAK,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;QACrB,MAAM,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;QACtB,QAAQ,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;KACzB,CAAA;AACH,CAAC;AAED,uGAAuG;AACvG,MAAM,CAAC,MAAM,eAAe,GAAyB,WAAW,CAAC,aAAa,CAAC,CAAA;AAE/E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,KAAK,GAAG,sBAAsB,CAAC,QAAQ,EAAE,CAAA;IAC/C,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAA;IAC5B,OAAO,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,eAAe,CAAC,MAAM,CAAkB,CAAA;AACrE,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,8BAA8B,CAC5C,QAAmB,EACnB,SAAgC;IAEhC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE;QAChC,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE,CAAA;QAClC,OAAO,QAAQ,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;YACnC,WAAW,CAAC,eAAe,EAAE,EAAE,CAAC,CAAA;YAChC,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,EAAE;gBAC1B,MAAM,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;YAC5B,CAAC,CAAC,CAAA;YACF,OAAO,CAAC,KAAK,IAAI,EAAE;gBACjB,MAAM,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;YAC9B,CAAC,CAAC,CAAA;YACF,OAAO,MAAM,EAAE,CAAA;QACjB,CAAC,CAAC,CAAA;IACJ,CAAC,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"transaction.js","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kronos-ts/messaging",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Messaging primitives for Kronos — commands, events, handlers, and event processors.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -55,8 +55,8 @@
55
55
  }
56
56
  },
57
57
  "dependencies": {
58
- "@kronos-ts/common": "workspace:*",
59
- "@kronos-ts/eventsourcing": "workspace:*",
58
+ "@kronos-ts/common": "0.1.0",
59
+ "@kronos-ts/eventsourcing": "0.1.0",
60
60
  "zod": "^4.3.6"
61
61
  },
62
62
  "devDependencies": {
@@ -3,12 +3,13 @@ import { resourceKey, qualifiedNameToString, type ResourceKey } from "@kronos-ts
3
3
  import { requireInvocationPhase } from "./processing-state.js"
4
4
  import type { QueryBus } from "./query-bus.js"
5
5
  import type { QueryDescriptor } from "./descriptor.js"
6
+ import type { SubscriptionFilter } from "./subscription-filter.js"
6
7
 
7
8
  /** Emit a subscription-query update from within the current processing context. */
8
9
  export interface EmitUpdateFunction {
9
10
  <Q extends z.ZodType>(
10
11
  query: QueryDescriptor<Q>,
11
- filter: (query: z.infer<Q>) => boolean,
12
+ filter: SubscriptionFilter<z.infer<Q>>,
12
13
  update: unknown,
13
14
  ): void
14
15
  }
@@ -31,5 +32,5 @@ export const emitUpdate: EmitUpdateFunction = (queryDescriptor, filter, update)
31
32
  const bus = state.resources.get(QUERY_BUS_KEY.symbol) as QueryBus | undefined
32
33
  if (!bus) throw new Error("No query bus configured")
33
34
  const queryName = qualifiedNameToString(queryDescriptor.name)
34
- bus.emitUpdate(queryName, filter as (q: unknown) => boolean, update)
35
+ bus.emitUpdate(queryName, filter as SubscriptionFilter, update)
35
36
  }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * EventScheduler — schedule an event to be appended to the event store at
3
+ * a future time, with the option to cancel before the fire-time.
4
+ *
5
+ * # Semantics
6
+ *
7
+ * - `schedule(event, at)` MUST be called from inside a UnitOfWork (i.e.,
8
+ * from a command/event/query handler, or any code that opened a UoW
9
+ * via `runInNewUoW`). The scheduled record participates in the active
10
+ * UoW transaction: if the UoW rolls back, the schedule is not persisted;
11
+ * if the UoW commits, the schedule is durably stored.
12
+ *
13
+ * - Once the schedule is committed, the implementation guarantees that the
14
+ * event WILL be appended to the event store at or after `at`, unless
15
+ * {@link EventScheduler.cancel} is called and succeeds before the
16
+ * fire-time. "At or after" because workers poll on an interval — fire
17
+ * times are not real-time deadlines.
18
+ *
19
+ * - `cancel(token)` returns a {@link CancelResult} discriminated union so
20
+ * callers can branch on three distinct outcomes: the schedule was
21
+ * cancelled before firing, the event had already been appended (too
22
+ * late), or no such schedule exists (already-cancelled, never-existed,
23
+ * or token from a different deployment).
24
+ *
25
+ * - Cancel is also UoW-aware: when called inside a UoW, it participates
26
+ * in the active tx so a handler that cancels and then throws does NOT
27
+ * leave the schedule cancelled. When called outside any UoW (rare —
28
+ * typically an ops/admin path), it commits standalone.
29
+ *
30
+ * # Calling from handlers
31
+ *
32
+ * Implementations resolve themselves from the active UoW's resources
33
+ * (similar to {@link send} and {@link emitUpdate}), so handler code uses
34
+ * the scheduler the framework configured for it. The interface itself
35
+ * is transport-agnostic — postgres and in-memory implementations live
36
+ * in their respective packages.
37
+ *
38
+ * # NOT what this is
39
+ *
40
+ * - This is NOT a command scheduler. AF5 schedules events, not commands;
41
+ * if you want a command to run later, schedule an event and run an
42
+ * automation processor that turns the event into a command on receipt.
43
+ * - This is NOT a cron / recurring scheduler. Each `schedule()` produces
44
+ * a single one-shot fire.
45
+ */
46
+
47
+ import type { EventMessage } from "./message.js"
48
+
49
+ /**
50
+ * Opaque handle returned by {@link EventScheduler.schedule}. Pass back to
51
+ * {@link EventScheduler.cancel} to attempt cancellation. Tokens are
52
+ * implementation-specific (postgres uses the row PK; in-memory uses a
53
+ * UUID) but always carry a stable `id`.
54
+ */
55
+ export interface ScheduleToken {
56
+ readonly id: string
57
+ }
58
+
59
+ /**
60
+ * Outcome of {@link EventScheduler.cancel}.
61
+ *
62
+ * - `cancelled` — the schedule existed in `pending` state and was
63
+ * successfully marked `cancelled`. Event will NOT
64
+ * be appended.
65
+ * - `already-appended` — a worker already fired this schedule; the event
66
+ * is in the event store. Caller decides whether
67
+ * compensation is needed.
68
+ * - `not-found` — no row matches the token. Could mean: already
69
+ * cancelled, never existed, or wrong store. Caller
70
+ * usually treats this as a no-op.
71
+ */
72
+ export type CancelResult =
73
+ | { readonly kind: "cancelled" }
74
+ | { readonly kind: "already-appended" }
75
+ | { readonly kind: "not-found" }
76
+
77
+ export interface EventScheduler {
78
+ /**
79
+ * Schedule {@link event} for append at {@link at}. Must be called inside
80
+ * a UoW; throws otherwise. The schedule participates in the active UoW
81
+ * tx and is only durable once the UoW commits.
82
+ *
83
+ * `at` is the wall-clock fire-time. Past dates are valid — they cause
84
+ * the worker to fire the schedule on its next poll.
85
+ */
86
+ schedule(event: EventMessage, at: Date): Promise<ScheduleToken>
87
+
88
+ /**
89
+ * Attempt to cancel a pending schedule. See {@link CancelResult} for
90
+ * the three possible outcomes.
91
+ *
92
+ * Safe to call from inside a UoW (joins the active tx) or outside
93
+ * (commits standalone).
94
+ */
95
+ cancel(token: ScheduleToken): Promise<CancelResult>
96
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * In-memory {@link EventScheduler} — intended for tests only.
3
+ *
4
+ * Backed by a `Map<scheduleId, record>` and `setTimeout`. Publishes fired
5
+ * events through a supplied {@link EventSink} (typically the in-memory
6
+ * event bus or a test spy).
7
+ *
8
+ * # UoW semantics (best-effort, test-grade)
9
+ *
10
+ * - `schedule()` must be called inside a UoW (INVOCATION phase). The
11
+ * record is staged immediately so cancel() inside the SAME UoW sees it,
12
+ * but the `setTimeout` arming is deferred to AFTER_COMMIT — so if the
13
+ * UoW rolls back the schedule never fires. `onError` cleans the staged
14
+ * record so callers see `not-found` on the rolled-back token.
15
+ *
16
+ * - `cancel()` may be called inside or outside a UoW. State change is
17
+ * applied immediately for caller-visibility; this means a UoW that
18
+ * cancels and then rolls back does NOT restore the schedule. This
19
+ * differs from the postgres implementation (which is true
20
+ * transactional) and is acceptable for the in-memory's test-only
21
+ * remit. Document this when writing tests that depend on cancel
22
+ * rollback semantics — use the postgres scheduler for that.
23
+ *
24
+ * # NOT production
25
+ *
26
+ * No persistence, no recovery on restart, no at-least-once. A test-only
27
+ * spy with a real-enough surface to exercise framework wiring.
28
+ */
29
+
30
+ import type { EventMessage } from "./message.js"
31
+ import type { EventSink } from "./event-sink.js"
32
+ import type { EventScheduler, ScheduleToken, CancelResult } from "./event-scheduler.js"
33
+ import {
34
+ requireInvocationPhase,
35
+ onAfterCommit,
36
+ onError,
37
+ processingStateStorage,
38
+ } from "./processing-state.js"
39
+ import { generateIdentifier } from "@kronos-ts/common"
40
+
41
+ type RecordStatus = "pending" | "appended" | "cancelled"
42
+
43
+ interface ScheduleRecord {
44
+ status: RecordStatus
45
+ event: EventMessage
46
+ fireAt: number
47
+ timer?: ReturnType<typeof setTimeout>
48
+ }
49
+
50
+ export interface InMemoryEventSchedulerOptions {
51
+ readonly eventSink: EventSink
52
+ /** Override `Date.now` for deterministic tests. Defaults to `Date.now`. */
53
+ readonly now?: () => number
54
+ }
55
+
56
+ export interface InMemoryEventScheduler extends EventScheduler {
57
+ /**
58
+ * Cancel any armed timers and drop all internal state. Tests call this
59
+ * in `afterEach` to ensure schedulers from one test do not fire into
60
+ * another. Not part of the public {@link EventScheduler} contract.
61
+ */
62
+ stop(): Promise<void>
63
+ }
64
+
65
+ export function createInMemoryEventScheduler(
66
+ options: InMemoryEventSchedulerOptions,
67
+ ): InMemoryEventScheduler {
68
+ const { eventSink } = options
69
+ const now = options.now ?? Date.now
70
+ const records = new Map<string, ScheduleRecord>()
71
+
72
+ function armTimer(id: string, record: ScheduleRecord): void {
73
+ const delay = Math.max(0, record.fireAt - now())
74
+ record.timer = setTimeout(() => {
75
+ const rec = records.get(id)
76
+ if (!rec || rec.status !== "pending") return
77
+ rec.status = "appended"
78
+ rec.timer = undefined
79
+ eventSink.publish([rec.event]).catch((err) => {
80
+ // Test-only: surface but do not crash the process. Real
81
+ // implementations need an at-least-once retry; not modelled here.
82
+ console.warn("inMemoryEventScheduler: publish failed:", err)
83
+ })
84
+ }, delay)
85
+ }
86
+
87
+ return {
88
+ async schedule(event: EventMessage, at: Date): Promise<ScheduleToken> {
89
+ requireInvocationPhase()
90
+
91
+ const id = generateIdentifier()
92
+ const record: ScheduleRecord = {
93
+ status: "pending",
94
+ event,
95
+ fireAt: at.getTime(),
96
+ }
97
+ records.set(id, record)
98
+
99
+ onAfterCommit(() => {
100
+ const rec = records.get(id)
101
+ if (!rec || rec.status !== "pending") return
102
+ armTimer(id, rec)
103
+ })
104
+
105
+ onError(() => {
106
+ // Roll back the staged record so post-rollback cancel() sees
107
+ // `not-found` rather than `cancelled`.
108
+ const rec = records.get(id)
109
+ if (rec && rec.status === "pending") records.delete(id)
110
+ })
111
+
112
+ return { id }
113
+ },
114
+
115
+ async cancel(token: ScheduleToken): Promise<CancelResult> {
116
+ const rec = records.get(token.id)
117
+ if (!rec) return { kind: "not-found" }
118
+ if (rec.status === "appended") return { kind: "already-appended" }
119
+ if (rec.status === "cancelled") return { kind: "not-found" }
120
+
121
+ rec.status = "cancelled"
122
+ if (rec.timer !== undefined) {
123
+ clearTimeout(rec.timer)
124
+ rec.timer = undefined
125
+ }
126
+
127
+ // Best-effort UoW participation: if we're inside a UoW that later
128
+ // errors, revert the cancel so the schedule's pending state
129
+ // re-materialises. The original timer (if it was armed) has already
130
+ // been cleared — the AFTER_COMMIT re-arm cycle is not re-driven
131
+ // here, which means a cancel + rollback inside a post-commit window
132
+ // would not re-fire. Acceptable for the in-memory's test-only remit.
133
+ if (processingStateStorage.getStore() !== undefined) {
134
+ onError(() => {
135
+ const r = records.get(token.id)
136
+ if (r && r.status === "cancelled") r.status = "pending"
137
+ })
138
+ }
139
+
140
+ return { kind: "cancelled" }
141
+ },
142
+
143
+ async stop(): Promise<void> {
144
+ for (const rec of records.values()) {
145
+ if (rec.timer !== undefined) clearTimeout(rec.timer)
146
+ }
147
+ records.clear()
148
+ },
149
+ }
150
+ }
package/src/index.ts CHANGED
@@ -145,6 +145,14 @@ export {
145
145
  runAfterCommitOrImmediately,
146
146
  } from "./subscription-query.js"
147
147
 
148
+ export {
149
+ type SubscriptionFilter,
150
+ payloadEquals,
151
+ applySubscriptionFilter,
152
+ extractStructuredFilter,
153
+ matchesPayloadEquals,
154
+ } from "./subscription-filter.js"
155
+
148
156
  // Event sink (publish-only)
149
157
  export { type EventSink } from "./event-sink.js"
150
158
 
@@ -237,7 +245,9 @@ export {
237
245
  type TransactionManager,
238
246
  noTransactionManager,
239
247
  getActiveTransaction,
248
+ getOrBeginActiveTransaction,
240
249
  transactionalUnitOfWorkFactory,
250
+ lazyTransactionalUnitOfWorkFactory,
241
251
  TRANSACTION_KEY,
242
252
  } from "./transaction.js"
243
253
 
@@ -252,6 +262,18 @@ export {
252
262
  export { send, COMMAND_BUS_KEY } from "./send.js"
253
263
  export { emitUpdate, QUERY_BUS_KEY } from "./emit-update.js"
254
264
 
265
+ // Event scheduling
266
+ export {
267
+ type EventScheduler,
268
+ type ScheduleToken,
269
+ type CancelResult,
270
+ } from "./event-scheduler.js"
271
+ export {
272
+ type InMemoryEventScheduler,
273
+ type InMemoryEventSchedulerOptions,
274
+ createInMemoryEventScheduler,
275
+ } from "./in-memory-event-scheduler.js"
276
+
255
277
  // Modules — Plan 08-03a (D-82): function-style helpers replace Module-shape factories
256
278
  export {
257
279
  registerCommandHandlersNatively,
@@ -1,6 +1,7 @@
1
1
  import type { QueryBus } from "./query-bus.js"
2
2
  import type { QueryMessage } from "./message.js"
3
3
  import type { SubscriptionQueryResult } from "./subscription-query.js"
4
+ import type { SubscriptionFilter } from "./subscription-filter.js"
4
5
  import type { DispatchInterceptor, HandlerInterceptor } from "./interceptor.js"
5
6
 
6
7
  /**
@@ -64,7 +65,7 @@ export function createInterceptingQueryBus(
64
65
 
65
66
  emitUpdate(
66
67
  queryName: string,
67
- filter: (queryPayload: unknown) => boolean,
68
+ filter: SubscriptionFilter,
68
69
  update: unknown,
69
70
  ): Promise<void> {
70
71
  return delegate.emitUpdate(queryName, filter, update)
@@ -72,7 +73,7 @@ export function createInterceptingQueryBus(
72
73
 
73
74
  completeSubscription(
74
75
  queryName: string,
75
- filter?: (queryPayload: unknown) => boolean,
76
+ filter?: SubscriptionFilter,
76
77
  ): Promise<void> {
77
78
  return delegate.completeSubscription(queryName, filter)
78
79
  },
@@ -80,7 +81,7 @@ export function createInterceptingQueryBus(
80
81
  completeSubscriptionExceptionally(
81
82
  queryName: string,
82
83
  error: Error,
83
- filter?: (queryPayload: unknown) => boolean,
84
+ filter?: SubscriptionFilter,
84
85
  ): Promise<void> {
85
86
  return delegate.completeSubscriptionExceptionally(queryName, error, filter)
86
87
  },
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: (queryPayload: unknown) => boolean,
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?: (queryPayload: unknown) => boolean,
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?: (queryPayload: unknown) => boolean,
72
+ filter?: SubscriptionFilter,
68
73
  ): Promise<void>
69
74
  }