@zdavison/matador 2.0.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.
Files changed (256) hide show
  1. package/cli.ts +453 -0
  2. package/dist/checkpoint/context.d.ts +59 -0
  3. package/dist/checkpoint/context.d.ts.map +1 -0
  4. package/dist/checkpoint/context.js +140 -0
  5. package/dist/checkpoint/context.test.d.ts +2 -0
  6. package/dist/checkpoint/context.test.d.ts.map +1 -0
  7. package/dist/checkpoint/context.test.js +424 -0
  8. package/dist/checkpoint/index.d.ts +7 -0
  9. package/dist/checkpoint/index.d.ts.map +1 -0
  10. package/dist/checkpoint/index.js +6 -0
  11. package/dist/checkpoint/stores/memory.d.ts +29 -0
  12. package/dist/checkpoint/stores/memory.d.ts.map +1 -0
  13. package/dist/checkpoint/stores/memory.js +39 -0
  14. package/dist/checkpoint/stores/noop.d.ts +14 -0
  15. package/dist/checkpoint/stores/noop.d.ts.map +1 -0
  16. package/dist/checkpoint/stores/noop.js +18 -0
  17. package/dist/checkpoint/stores/stores.test.d.ts +2 -0
  18. package/dist/checkpoint/stores/stores.test.d.ts.map +1 -0
  19. package/dist/checkpoint/stores/stores.test.js +146 -0
  20. package/dist/checkpoint/types.d.ts +119 -0
  21. package/dist/checkpoint/types.d.ts.map +1 -0
  22. package/dist/checkpoint/types.js +1 -0
  23. package/dist/codec/codec.d.ts +29 -0
  24. package/dist/codec/codec.d.ts.map +1 -0
  25. package/dist/codec/codec.js +15 -0
  26. package/dist/codec/header-aware-codec.d.ts +36 -0
  27. package/dist/codec/header-aware-codec.d.ts.map +1 -0
  28. package/dist/codec/header-aware-codec.js +1 -0
  29. package/dist/codec/index.d.ts +6 -0
  30. package/dist/codec/index.d.ts.map +1 -0
  31. package/dist/codec/index.js +3 -0
  32. package/dist/codec/json-codec.d.ts +13 -0
  33. package/dist/codec/json-codec.d.ts.map +1 -0
  34. package/dist/codec/json-codec.js +64 -0
  35. package/dist/codec/rabbitmq-codec.d.ts +28 -0
  36. package/dist/codec/rabbitmq-codec.d.ts.map +1 -0
  37. package/dist/codec/rabbitmq-codec.js +242 -0
  38. package/dist/codec/rabbitmq-codec.test.d.ts +2 -0
  39. package/dist/codec/rabbitmq-codec.test.d.ts.map +1 -0
  40. package/dist/codec/rabbitmq-codec.test.js +433 -0
  41. package/dist/core/fanout.d.ts +59 -0
  42. package/dist/core/fanout.d.ts.map +1 -0
  43. package/dist/core/fanout.js +121 -0
  44. package/dist/core/fanout.test.d.ts +2 -0
  45. package/dist/core/fanout.test.d.ts.map +1 -0
  46. package/dist/core/fanout.test.js +1055 -0
  47. package/dist/core/index.d.ts +7 -0
  48. package/dist/core/index.d.ts.map +1 -0
  49. package/dist/core/index.js +3 -0
  50. package/dist/core/matador.d.ts +133 -0
  51. package/dist/core/matador.d.ts.map +1 -0
  52. package/dist/core/matador.js +222 -0
  53. package/dist/core/matador.test.d.ts +2 -0
  54. package/dist/core/matador.test.d.ts.map +1 -0
  55. package/dist/core/matador.test.js +446 -0
  56. package/dist/core/shutdown.d.ts +78 -0
  57. package/dist/core/shutdown.d.ts.map +1 -0
  58. package/dist/core/shutdown.js +111 -0
  59. package/dist/core/shutdown.test.d.ts +2 -0
  60. package/dist/core/shutdown.test.d.ts.map +1 -0
  61. package/dist/core/shutdown.test.js +524 -0
  62. package/dist/errors/checkpoint-errors.d.ts +30 -0
  63. package/dist/errors/checkpoint-errors.d.ts.map +1 -0
  64. package/dist/errors/checkpoint-errors.js +49 -0
  65. package/dist/errors/has-description.d.ts +18 -0
  66. package/dist/errors/has-description.d.ts.map +1 -0
  67. package/dist/errors/has-description.js +9 -0
  68. package/dist/errors/index.d.ts +6 -0
  69. package/dist/errors/index.d.ts.map +1 -0
  70. package/dist/errors/index.js +22 -0
  71. package/dist/errors/matador-errors.d.ts +183 -0
  72. package/dist/errors/matador-errors.d.ts.map +1 -0
  73. package/dist/errors/matador-errors.js +376 -0
  74. package/dist/errors/retry-errors.d.ts +67 -0
  75. package/dist/errors/retry-errors.d.ts.map +1 -0
  76. package/dist/errors/retry-errors.js +108 -0
  77. package/dist/hooks/index.d.ts +4 -0
  78. package/dist/hooks/index.d.ts.map +1 -0
  79. package/dist/hooks/index.js +2 -0
  80. package/dist/hooks/safe-hooks.d.ts +34 -0
  81. package/dist/hooks/safe-hooks.d.ts.map +1 -0
  82. package/dist/hooks/safe-hooks.js +135 -0
  83. package/dist/hooks/types.d.ts +189 -0
  84. package/dist/hooks/types.d.ts.map +1 -0
  85. package/dist/hooks/types.js +9 -0
  86. package/dist/index.cjs +3156 -0
  87. package/dist/index.cjs.map +1 -0
  88. package/dist/index.d.cts +23 -0
  89. package/dist/index.d.ts +23 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +31 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/pipeline/index.d.ts +3 -0
  94. package/dist/pipeline/index.d.ts.map +1 -0
  95. package/dist/pipeline/index.js +1 -0
  96. package/dist/pipeline/pipeline.d.ts +60 -0
  97. package/dist/pipeline/pipeline.d.ts.map +1 -0
  98. package/dist/pipeline/pipeline.js +261 -0
  99. package/dist/pipeline/pipeline.test.d.ts +2 -0
  100. package/dist/pipeline/pipeline.test.d.ts.map +1 -0
  101. package/dist/pipeline/pipeline.test.js +1065 -0
  102. package/dist/retry/index.d.ts +4 -0
  103. package/dist/retry/index.d.ts.map +1 -0
  104. package/dist/retry/index.js +1 -0
  105. package/dist/retry/policy.d.ts +43 -0
  106. package/dist/retry/policy.d.ts.map +1 -0
  107. package/dist/retry/policy.js +1 -0
  108. package/dist/retry/standard-policy.d.ts +44 -0
  109. package/dist/retry/standard-policy.d.ts.map +1 -0
  110. package/dist/retry/standard-policy.js +102 -0
  111. package/dist/retry/standard-policy.test.d.ts +2 -0
  112. package/dist/retry/standard-policy.test.d.ts.map +1 -0
  113. package/dist/retry/standard-policy.test.js +190 -0
  114. package/dist/schema/index.d.ts +4 -0
  115. package/dist/schema/index.d.ts.map +1 -0
  116. package/dist/schema/index.js +2 -0
  117. package/dist/schema/registry.d.ts +63 -0
  118. package/dist/schema/registry.d.ts.map +1 -0
  119. package/dist/schema/registry.js +171 -0
  120. package/dist/schema/registry.test.d.ts +2 -0
  121. package/dist/schema/registry.test.d.ts.map +1 -0
  122. package/dist/schema/registry.test.js +278 -0
  123. package/dist/schema/types.d.ts +158 -0
  124. package/dist/schema/types.d.ts.map +1 -0
  125. package/dist/schema/types.js +74 -0
  126. package/dist/schema/types.test.d.ts +2 -0
  127. package/dist/schema/types.test.d.ts.map +1 -0
  128. package/dist/schema/types.test.js +243 -0
  129. package/dist/topology/builder.d.ts +66 -0
  130. package/dist/topology/builder.d.ts.map +1 -0
  131. package/dist/topology/builder.js +156 -0
  132. package/dist/topology/builder.test.d.ts +2 -0
  133. package/dist/topology/builder.test.d.ts.map +1 -0
  134. package/dist/topology/builder.test.js +222 -0
  135. package/dist/topology/index.d.ts +5 -0
  136. package/dist/topology/index.d.ts.map +1 -0
  137. package/dist/topology/index.js +2 -0
  138. package/dist/topology/types.d.ts +76 -0
  139. package/dist/topology/types.d.ts.map +1 -0
  140. package/dist/topology/types.js +18 -0
  141. package/dist/transport/capabilities.d.ts +66 -0
  142. package/dist/transport/capabilities.d.ts.map +1 -0
  143. package/dist/transport/capabilities.js +18 -0
  144. package/dist/transport/connection-manager.d.ts +95 -0
  145. package/dist/transport/connection-manager.d.ts.map +1 -0
  146. package/dist/transport/connection-manager.js +144 -0
  147. package/dist/transport/index.d.ts +11 -0
  148. package/dist/transport/index.d.ts.map +1 -0
  149. package/dist/transport/index.js +5 -0
  150. package/dist/transport/local/local-transport.d.ts +62 -0
  151. package/dist/transport/local/local-transport.d.ts.map +1 -0
  152. package/dist/transport/local/local-transport.js +241 -0
  153. package/dist/transport/local/local-transport.test.d.ts +2 -0
  154. package/dist/transport/local/local-transport.test.d.ts.map +1 -0
  155. package/dist/transport/local/local-transport.test.js +192 -0
  156. package/dist/transport/multi/multi-transport.d.ts +94 -0
  157. package/dist/transport/multi/multi-transport.d.ts.map +1 -0
  158. package/dist/transport/multi/multi-transport.js +184 -0
  159. package/dist/transport/multi/multi-transport.test.d.ts +2 -0
  160. package/dist/transport/multi/multi-transport.test.d.ts.map +1 -0
  161. package/dist/transport/multi/multi-transport.test.js +236 -0
  162. package/dist/transport/rabbitmq/rabbitmq-transport.d.ts +73 -0
  163. package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -0
  164. package/dist/transport/rabbitmq/rabbitmq-transport.js +518 -0
  165. package/dist/transport/transport.d.ts +152 -0
  166. package/dist/transport/transport.d.ts.map +1 -0
  167. package/dist/transport/transport.js +1 -0
  168. package/dist/types/common.d.ts +41 -0
  169. package/dist/types/common.d.ts.map +1 -0
  170. package/dist/types/common.js +12 -0
  171. package/dist/types/envelope.d.ts +82 -0
  172. package/dist/types/envelope.d.ts.map +1 -0
  173. package/dist/types/envelope.js +36 -0
  174. package/dist/types/event.d.ts +96 -0
  175. package/dist/types/event.d.ts.map +1 -0
  176. package/dist/types/event.js +26 -0
  177. package/dist/types/event.test.d.ts +2 -0
  178. package/dist/types/event.test.d.ts.map +1 -0
  179. package/dist/types/event.test.js +130 -0
  180. package/dist/types/index.d.ts +9 -0
  181. package/dist/types/index.d.ts.map +1 -0
  182. package/dist/types/index.js +4 -0
  183. package/dist/types/subscriber.d.ts +207 -0
  184. package/dist/types/subscriber.d.ts.map +1 -0
  185. package/dist/types/subscriber.js +99 -0
  186. package/examples/config.ts +126 -0
  187. package/examples/event.ts +26 -0
  188. package/examples/order-event.json +19 -0
  189. package/package.json +66 -0
  190. package/src/checkpoint/context.test.ts +510 -0
  191. package/src/checkpoint/context.ts +213 -0
  192. package/src/checkpoint/index.ts +30 -0
  193. package/src/checkpoint/stores/memory.ts +47 -0
  194. package/src/checkpoint/stores/noop.ts +22 -0
  195. package/src/checkpoint/stores/stores.test.ts +177 -0
  196. package/src/checkpoint/types.ts +147 -0
  197. package/src/codec/codec.ts +42 -0
  198. package/src/codec/header-aware-codec.ts +41 -0
  199. package/src/codec/index.ts +11 -0
  200. package/src/codec/json-codec.ts +69 -0
  201. package/src/codec/rabbitmq-codec.test.ts +516 -0
  202. package/src/codec/rabbitmq-codec.ts +336 -0
  203. package/src/core/fanout.test.ts +1351 -0
  204. package/src/core/fanout.ts +184 -0
  205. package/src/core/index.ts +12 -0
  206. package/src/core/matador.test.ts +575 -0
  207. package/src/core/matador.ts +344 -0
  208. package/src/core/shutdown.test.ts +853 -0
  209. package/src/core/shutdown.ts +165 -0
  210. package/src/errors/checkpoint-errors.ts +62 -0
  211. package/src/errors/has-description.ts +25 -0
  212. package/src/errors/index.ts +57 -0
  213. package/src/errors/matador-errors.ts +477 -0
  214. package/src/errors/retry-errors.ts +134 -0
  215. package/src/hooks/index.ts +15 -0
  216. package/src/hooks/safe-hooks.ts +223 -0
  217. package/src/hooks/types.ts +248 -0
  218. package/src/index.ts +231 -0
  219. package/src/pipeline/index.ts +2 -0
  220. package/src/pipeline/pipeline.test.ts +1317 -0
  221. package/src/pipeline/pipeline.ts +371 -0
  222. package/src/retry/index.ts +4 -0
  223. package/src/retry/policy.ts +46 -0
  224. package/src/retry/standard-policy.test.ts +282 -0
  225. package/src/retry/standard-policy.ts +156 -0
  226. package/src/schema/index.ts +16 -0
  227. package/src/schema/registry.test.ts +339 -0
  228. package/src/schema/registry.ts +226 -0
  229. package/src/schema/types.test.ts +281 -0
  230. package/src/schema/types.ts +217 -0
  231. package/src/topology/builder.test.ts +275 -0
  232. package/src/topology/builder.ts +199 -0
  233. package/src/topology/index.ts +15 -0
  234. package/src/topology/types.ts +109 -0
  235. package/src/transport/capabilities.ts +88 -0
  236. package/src/transport/connection-manager.ts +218 -0
  237. package/src/transport/index.ts +42 -0
  238. package/src/transport/local/local-transport.test.ts +262 -0
  239. package/src/transport/local/local-transport.ts +327 -0
  240. package/src/transport/multi/multi-transport.test.ts +320 -0
  241. package/src/transport/multi/multi-transport.ts +294 -0
  242. package/src/transport/rabbitmq/rabbitmq-transport.ts +753 -0
  243. package/src/transport/transport.ts +200 -0
  244. package/src/types/common.ts +53 -0
  245. package/src/types/envelope.ts +152 -0
  246. package/src/types/event.test.ts +157 -0
  247. package/src/types/event.ts +118 -0
  248. package/src/types/index.ts +52 -0
  249. package/src/types/subscriber.ts +310 -0
  250. package/test/e2e/multi-transport.e2e.test.ts +236 -0
  251. package/test/e2e/rabbitmq-transport.e2e.test.ts +327 -0
  252. package/test/e2e/transport-compliance.e2e.test.ts +505 -0
  253. package/test/integration/matador.integration.test.ts +634 -0
  254. package/tsconfig.json +29 -0
  255. package/tsconfig.tsbuildinfo +1 -0
  256. package/tsup.config.ts +13 -0
package/cli.ts ADDED
@@ -0,0 +1,453 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Matador CLI - Quick local testing of your Matador config
4
+ *
5
+ * Usage:
6
+ * bunx matador send <config-file> <event-file> [options]
7
+ * bunx matador send-test-event <config-file>
8
+ *
9
+ * Commands:
10
+ * send Send an event using a config and event file
11
+ * send-test-event Send a test event using the examples in config
12
+ *
13
+ * Config file should export:
14
+ * - schema: MatadorSchema - Map of event keys to [EventClass, Subscribers[]]
15
+ * - topology?: Topology - Optional topology (defaults to simple 'events' queue)
16
+ * - hooks?: MatadorHooks - Optional hooks
17
+ * - testEvent?: { eventKey, data, before?, options? } - Test event for send-test-event command
18
+ *
19
+ * Event file should export:
20
+ * - eventKey: string - The key of the event to dispatch
21
+ * - data: unknown - The event data payload
22
+ * - before?: unknown - Optional 'before' data for change events
23
+ * - options?: EventOptions - Optional dispatch options (correlationId, metadata, delayMs)
24
+ */
25
+
26
+ import { resolve } from 'node:path';
27
+ import { parseArgs } from 'node:util';
28
+ import {
29
+ type EventOptions,
30
+ LocalTransport,
31
+ Matador,
32
+ type MatadorHooks,
33
+ type MatadorSchema,
34
+ type Topology,
35
+ TopologyBuilder,
36
+ consoleLogger,
37
+ isSchemaEntryTuple,
38
+ } from './src/index.js';
39
+
40
+ // Colors for terminal output
41
+ const colors = {
42
+ reset: '\x1b[0m',
43
+ bold: '\x1b[1m',
44
+ dim: '\x1b[2m',
45
+ red: '\x1b[31m',
46
+ green: '\x1b[32m',
47
+ yellow: '\x1b[33m',
48
+ blue: '\x1b[34m',
49
+ magenta: '\x1b[35m',
50
+ cyan: '\x1b[36m',
51
+ };
52
+
53
+ function log(message: string, color: keyof typeof colors = 'reset'): void {
54
+ console.log(`${colors[color]}${message}${colors.reset}`);
55
+ }
56
+
57
+ function logSection(title: string): void {
58
+ console.log();
59
+ log(`━━━ ${title} ━━━`, 'cyan');
60
+ }
61
+
62
+ function logSuccess(message: string): void {
63
+ log(`✓ ${message}`, 'green');
64
+ }
65
+
66
+ function logError(message: string): void {
67
+ log(`✗ ${message}`, 'red');
68
+ }
69
+
70
+ function logInfo(message: string): void {
71
+ log(`ℹ ${message}`, 'blue');
72
+ }
73
+
74
+ function logWarning(message: string): void {
75
+ log(`⚠ ${message}`, 'yellow');
76
+ }
77
+
78
+ interface TestEventSpec {
79
+ eventKey: string;
80
+ data: unknown;
81
+ before?: unknown;
82
+ options?: EventOptions;
83
+ }
84
+
85
+ interface ConfigExport {
86
+ schema: MatadorSchema;
87
+ topology?: Topology;
88
+ hooks?: MatadorHooks;
89
+ testEvent?: TestEventSpec;
90
+ }
91
+
92
+ interface EventExport {
93
+ eventKey: string;
94
+ data: unknown;
95
+ before?: unknown;
96
+ options?: EventOptions;
97
+ }
98
+
99
+ function printUsage(): void {
100
+ console.log(`
101
+ ${colors.bold}Matador CLI${colors.reset} - Quick local testing of your Matador config
102
+
103
+ ${colors.cyan}Usage:${colors.reset}
104
+ bunx matador send <config-file> <event-file> [options]
105
+ bunx matador send-test-event <config-file>
106
+
107
+ ${colors.cyan}Commands:${colors.reset}
108
+ send Send an event using a config and event file
109
+ send-test-event Send a test event defined in the config file
110
+
111
+ ${colors.cyan}Options:${colors.reset}
112
+ --help, -h Show this help message
113
+ --dry-run Validate config and event without dispatching
114
+ --timeout Timeout in milliseconds for processing (default: 5000)
115
+ --verbose Show verbose output including all hook logs
116
+
117
+ ${colors.cyan}Config file exports:${colors.reset}
118
+ schema MatadorSchema - Map of event keys to [EventClass, Subscribers[]]
119
+ topology? Topology - Optional topology config
120
+ hooks? MatadorHooks - Optional hooks for logging
121
+ testEvent? { eventKey, data, before?, options? } - Test event for send-test-event
122
+
123
+ ${colors.cyan}Event file exports:${colors.reset}
124
+ eventKey string - The key of the event to dispatch
125
+ data unknown - The event data payload
126
+ before? unknown - Optional 'before' data for change events
127
+ options? EventOptions - Optional dispatch options
128
+
129
+ ${colors.cyan}Examples:${colors.reset}
130
+ bunx matador send ./my-config.ts ./test-event.ts
131
+ bunx matador send ./config.ts ./event.json --verbose --timeout 10000
132
+ bunx matador send-test-event ./my-config.ts
133
+ `);
134
+ }
135
+
136
+ async function loadModule<T>(filePath: string): Promise<T> {
137
+ const absolutePath = resolve(process.cwd(), filePath);
138
+
139
+ if (filePath.endsWith('.json')) {
140
+ const file = Bun.file(absolutePath);
141
+ return (await file.json()) as T;
142
+ }
143
+
144
+ const module = await import(absolutePath);
145
+ return module.default ?? module;
146
+ }
147
+
148
+ async function runSend(
149
+ configPath: string,
150
+ eventPath: string,
151
+ options: { dryRun: boolean; timeout: number; verbose: boolean },
152
+ ): Promise<void> {
153
+ logSection('Loading Configuration');
154
+
155
+ // Load config file
156
+ let config: ConfigExport;
157
+ try {
158
+ logInfo(`Loading config from: ${configPath}`);
159
+ config = await loadModule<ConfigExport>(configPath);
160
+
161
+ if (!config.schema || typeof config.schema !== 'object') {
162
+ throw new Error('Config must export a "schema" object');
163
+ }
164
+
165
+ const eventCount = Object.keys(config.schema).length;
166
+ const subscriberCount = Object.values(config.schema).reduce(
167
+ (acc, entry) => {
168
+ if (isSchemaEntryTuple(entry)) {
169
+ return acc + entry[1].length;
170
+ }
171
+ return acc + entry.subscribers.length;
172
+ },
173
+ 0,
174
+ );
175
+ logSuccess(
176
+ `Loaded ${eventCount} event(s) and ${subscriberCount} subscriber(s)`,
177
+ );
178
+ } catch (err) {
179
+ logError(
180
+ `Failed to load config: ${err instanceof Error ? err.message : err}`,
181
+ );
182
+ process.exit(1);
183
+ }
184
+
185
+ // Load event file
186
+ let eventSpec: EventExport;
187
+ try {
188
+ logInfo(`Loading event from: ${eventPath}`);
189
+ eventSpec = await loadModule<EventExport>(eventPath);
190
+
191
+ if (!eventSpec.eventKey || typeof eventSpec.eventKey !== 'string') {
192
+ throw new Error('Event file must export an "eventKey" string');
193
+ }
194
+ if (eventSpec.data === undefined) {
195
+ throw new Error('Event file must export a "data" property');
196
+ }
197
+
198
+ logSuccess(`Event key: ${eventSpec.eventKey}`);
199
+ } catch (err) {
200
+ logError(
201
+ `Failed to load event: ${err instanceof Error ? err.message : err}`,
202
+ );
203
+ process.exit(1);
204
+ }
205
+
206
+ await dispatchEvent(config, eventSpec, options);
207
+ }
208
+
209
+ async function runSendTestEvent(
210
+ configPath: string,
211
+ options: { dryRun: boolean; timeout: number; verbose: boolean },
212
+ ): Promise<void> {
213
+ logSection('Loading Configuration');
214
+
215
+ // Load config file
216
+ let config: ConfigExport;
217
+ try {
218
+ logInfo(`Loading config from: ${configPath}`);
219
+ config = await loadModule<ConfigExport>(configPath);
220
+
221
+ if (!config.schema || typeof config.schema !== 'object') {
222
+ throw new Error('Config must export a "schema" object');
223
+ }
224
+
225
+ if (!config.testEvent) {
226
+ throw new Error(
227
+ 'Config must export a "testEvent" object for send-test-event command',
228
+ );
229
+ }
230
+
231
+ const eventCount = Object.keys(config.schema).length;
232
+ const subscriberCount = Object.values(config.schema).reduce(
233
+ (acc, entry) => {
234
+ if (isSchemaEntryTuple(entry)) {
235
+ return acc + entry[1].length;
236
+ }
237
+ return acc + entry.subscribers.length;
238
+ },
239
+ 0,
240
+ );
241
+ logSuccess(
242
+ `Loaded ${eventCount} event(s) and ${subscriberCount} subscriber(s)`,
243
+ );
244
+ logSuccess(`Test event key: ${config.testEvent.eventKey}`);
245
+ } catch (err) {
246
+ logError(
247
+ `Failed to load config: ${err instanceof Error ? err.message : err}`,
248
+ );
249
+ process.exit(1);
250
+ }
251
+
252
+ await dispatchEvent(config, config.testEvent, options);
253
+ }
254
+
255
+ async function dispatchEvent(
256
+ config: ConfigExport,
257
+ eventSpec: EventExport,
258
+ options: { dryRun: boolean; timeout: number; verbose: boolean },
259
+ ): Promise<void> {
260
+ // Validate event exists in config
261
+ const schemaEntry = config.schema[eventSpec.eventKey];
262
+ if (!schemaEntry) {
263
+ logError(`Event "${eventSpec.eventKey}" not found in config`);
264
+ logInfo(`Available events: ${Object.keys(config.schema).join(', ')}`);
265
+ process.exit(1);
266
+ }
267
+
268
+ // Extract EventClass and subscribers from schema entry
269
+ const EventClass = isSchemaEntryTuple(schemaEntry)
270
+ ? schemaEntry[0]
271
+ : schemaEntry.eventClass;
272
+ const subscribers = isSchemaEntryTuple(schemaEntry)
273
+ ? schemaEntry[1]
274
+ : schemaEntry.subscribers;
275
+
276
+ if (!subscribers || subscribers.length === 0) {
277
+ logWarning(`No subscribers registered for event "${eventSpec.eventKey}"`);
278
+ } else {
279
+ logInfo(`Subscribers: ${subscribers.map((s) => s.name).join(', ')}`);
280
+ }
281
+
282
+ if (options.dryRun) {
283
+ logSection('Dry Run Complete');
284
+ logSuccess('Config and event validated successfully');
285
+ logInfo('Use without --dry-run to actually dispatch the event');
286
+ process.exit(0);
287
+ }
288
+
289
+ logSection('Dispatching Event');
290
+
291
+ // Create topology
292
+ const topology =
293
+ config.topology ??
294
+ TopologyBuilder.create()
295
+ .withNamespace('cli-test')
296
+ .addQueue('events')
297
+ .withoutDeadLetter()
298
+ .build();
299
+
300
+ // Create transport
301
+ const transport = new LocalTransport();
302
+
303
+ // Create hooks for logging
304
+ const hooks: MatadorHooks = {
305
+ logger: options.verbose ? consoleLogger : undefined,
306
+ onWorkerSuccess: (ctx) => {
307
+ logSuccess(`[${ctx.subscriber.name}] processed in ${ctx.durationMs}ms`);
308
+ },
309
+ onWorkerError: (ctx) => {
310
+ logError(
311
+ `[${ctx.subscriber.name}] failed after ${ctx.durationMs}ms: ${ctx.error.message}`,
312
+ );
313
+ },
314
+ ...config.hooks,
315
+ };
316
+
317
+ // Create Matador instance with schema and hooks
318
+ const matador = new Matador(
319
+ {
320
+ transport,
321
+ topology,
322
+ schema: config.schema,
323
+ consumeFrom: topology.queues.map((q) => q.name),
324
+ },
325
+ hooks,
326
+ );
327
+
328
+ try {
329
+ await matador.start();
330
+ logSuccess('Matador started');
331
+
332
+ // Create and dispatch the event
333
+ const event = new EventClass(eventSpec.data, eventSpec.before);
334
+ logInfo(`Dispatching: ${eventSpec.eventKey}`);
335
+
336
+ if (options.verbose) {
337
+ logInfo(`Data: ${JSON.stringify(eventSpec.data, null, 2)}`);
338
+ if (eventSpec.before) {
339
+ logInfo(`Before: ${JSON.stringify(eventSpec.before, null, 2)}`);
340
+ }
341
+ if (eventSpec.options) {
342
+ logInfo(`Options: ${JSON.stringify(eventSpec.options, null, 2)}`);
343
+ }
344
+ }
345
+
346
+ const result = await matador.send(event, eventSpec.options);
347
+
348
+ logSection('Send Result');
349
+ logInfo(`Event key: ${result.eventKey}`);
350
+ logInfo(`Subscribers sent: ${result.subscribersSent}`);
351
+ logInfo(`Subscribers skipped: ${result.subscribersSkipped}`);
352
+
353
+ if (result.errors.length > 0) {
354
+ logWarning(`Dispatch errors: ${result.errors.length}`);
355
+ for (const err of result.errors) {
356
+ logError(` [${err.subscriberName}] ${err.error.message}`);
357
+ }
358
+ }
359
+
360
+ // Wait for processing
361
+ logSection('Processing');
362
+ const idle = await matador.waitForIdle(options.timeout);
363
+
364
+ if (idle) {
365
+ logSuccess('All subscribers finished processing');
366
+ } else {
367
+ logWarning(`Timed out after ${options.timeout}ms waiting for processing`);
368
+ }
369
+
370
+ await matador.shutdown();
371
+ logSuccess('Matador shutdown complete');
372
+
373
+ logSection('Summary');
374
+ if (result.errors.length === 0 && idle) {
375
+ logSuccess('Event dispatched and processed successfully!');
376
+ process.exit(0);
377
+ } else {
378
+ logWarning('Event dispatched with issues');
379
+ process.exit(1);
380
+ }
381
+ } catch (err) {
382
+ logError(`Error: ${err instanceof Error ? err.message : err}`);
383
+ await matador.shutdown().catch(() => {});
384
+ process.exit(1);
385
+ }
386
+ }
387
+
388
+ async function main(): Promise<void> {
389
+ const args = Bun.argv.slice(2);
390
+
391
+ // Check for help flag first
392
+ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
393
+ printUsage();
394
+ process.exit(0);
395
+ }
396
+
397
+ const command = args[0];
398
+
399
+ if (command === 'send') {
400
+ const { values, positionals } = parseArgs({
401
+ args: args.slice(1),
402
+ options: {
403
+ 'dry-run': { type: 'boolean' },
404
+ timeout: { type: 'string', default: '5000' },
405
+ verbose: { type: 'boolean' },
406
+ },
407
+ allowPositionals: true,
408
+ });
409
+
410
+ if (positionals.length < 2) {
411
+ logError('Missing required arguments: <config-file> <event-file>');
412
+ logInfo('Usage: bunx matador send <config-file> <event-file> [options]');
413
+ process.exit(1);
414
+ }
415
+
416
+ const [configPath, eventPath] = positionals;
417
+ await runSend(configPath!, eventPath!, {
418
+ dryRun: values['dry-run'] ?? false,
419
+ timeout: Number.parseInt(values.timeout ?? '5000', 10),
420
+ verbose: values.verbose ?? false,
421
+ });
422
+ } else if (command === 'send-test-event') {
423
+ const { values, positionals } = parseArgs({
424
+ args: args.slice(1),
425
+ options: {
426
+ 'dry-run': { type: 'boolean' },
427
+ timeout: { type: 'string', default: '5000' },
428
+ verbose: { type: 'boolean' },
429
+ },
430
+ allowPositionals: true,
431
+ });
432
+
433
+ if (positionals.length < 1) {
434
+ logError('Missing required argument: <config-file>');
435
+ logInfo('Usage: bunx matador send-test-event <config-file> [options]');
436
+ process.exit(1);
437
+ }
438
+
439
+ const [configPath] = positionals;
440
+ await runSendTestEvent(configPath!, {
441
+ dryRun: values['dry-run'] ?? false,
442
+ timeout: Number.parseInt(values.timeout ?? '5000', 10),
443
+ verbose: values.verbose ?? false,
444
+ });
445
+ } else {
446
+ logError(`Unknown command: ${command}`);
447
+ logInfo('Available commands: send, send-test-event');
448
+ logInfo('Run "bunx matador --help" for usage information');
449
+ process.exit(1);
450
+ }
451
+ }
452
+
453
+ main();
@@ -0,0 +1,59 @@
1
+ import type { Envelope, SubscriberDefinition } from '../types/index.js';
2
+ import type { Checkpoint, CheckpointHitContext, CheckpointMissContext, CheckpointStore, JsonSerializable, SubscriberContext } from './types.js';
3
+ /**
4
+ * Hooks for observability during context operations.
5
+ */
6
+ export interface ResumableContextHooks {
7
+ onCheckpointHit?(context: CheckpointHitContext): void | Promise<void>;
8
+ onCheckpointMiss?(context: CheckpointMissContext): void | Promise<void>;
9
+ }
10
+ /**
11
+ * Configuration for creating a ResumableContext.
12
+ */
13
+ export interface ResumableContextConfig {
14
+ readonly store: CheckpointStore;
15
+ readonly envelope: Envelope;
16
+ readonly subscriber: SubscriberDefinition;
17
+ readonly existingCheckpoint?: Checkpoint | undefined;
18
+ readonly hooks?: ResumableContextHooks | undefined;
19
+ }
20
+ /**
21
+ * Implementation of SubscriberContext that provides io() caching.
22
+ *
23
+ * On first execution, io() calls execute their lambdas and cache results.
24
+ * On retry (when existingCheckpoint is provided), cached results are returned
25
+ * without re-executing the lambda.
26
+ */
27
+ export declare class ResumableContext implements SubscriberContext {
28
+ private checkpoint;
29
+ private readonly usedKeys;
30
+ private readonly store;
31
+ private readonly envelope;
32
+ private readonly subscriber;
33
+ private readonly hooks;
34
+ constructor(config: ResumableContextConfig);
35
+ get attempt(): number;
36
+ get isRetry(): boolean;
37
+ io<T extends JsonSerializable>(key: string, fn: () => Promise<T> | T): Promise<T>;
38
+ all<T extends readonly [
39
+ string,
40
+ () => Promise<JsonSerializable> | JsonSerializable
41
+ ][]>(ops: T): Promise<{
42
+ [K in keyof T]: T[K] extends [string, () => Promise<infer R> | infer R] ? R : never;
43
+ }>;
44
+ /**
45
+ * Clears the checkpoint from storage.
46
+ * Called after successful completion or dead-letter.
47
+ */
48
+ clear(): Promise<void>;
49
+ /**
50
+ * Gets the current checkpoint state.
51
+ * Useful for debugging and testing.
52
+ */
53
+ getCheckpoint(): Checkpoint;
54
+ /**
55
+ * Gets the number of cached steps.
56
+ */
57
+ get cachedStepCount(): number;
58
+ }
59
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/checkpoint/context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,KAAK,EACV,UAAU,EACV,oBAAoB,EACpB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAEpB;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,eAAe,CAAC,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,gBAAgB,CAAC,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzE;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,oBAAoB,CAAC;IAC1C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,KAAK,CAAC,EAAE,qBAAqB,GAAG,SAAS,CAAC;CACpD;AAED;;;;;;GAMG;AACH,qBAAa,gBAAiB,YAAW,iBAAiB;IACxD,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAuB;IAClD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoC;gBAE9C,MAAM,EAAE,sBAAsB;IAa1C,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAEK,EAAE,CAAC,CAAC,SAAS,gBAAgB,EACjC,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GACvB,OAAO,CAAC,CAAC,CAAC;IA0CP,GAAG,CACP,CAAC,SAAS,SAAS;QACjB,MAAM;QACN,MAAM,OAAO,CAAC,gBAAgB,CAAC,GAAG,gBAAgB;KACnD,EAAE,EAEH,GAAG,EAAE,CAAC,GACL,OAAO,CAAC;SACR,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,GACnE,CAAC,GACD,KAAK;KACV,CAAC;IAoEF;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B;;;OAGG;IACH,aAAa,IAAI,UAAU;IAI3B;;OAEG;IACH,IAAI,eAAe,IAAI,MAAM,CAE5B;CACF"}
@@ -0,0 +1,140 @@
1
+ import { DuplicateIoKeyError } from '../errors/index.js';
2
+ /**
3
+ * Implementation of SubscriberContext that provides io() caching.
4
+ *
5
+ * On first execution, io() calls execute their lambdas and cache results.
6
+ * On retry (when existingCheckpoint is provided), cached results are returned
7
+ * without re-executing the lambda.
8
+ */
9
+ export class ResumableContext {
10
+ checkpoint;
11
+ usedKeys = new Set();
12
+ store;
13
+ envelope;
14
+ subscriber;
15
+ hooks;
16
+ constructor(config) {
17
+ this.store = config.store;
18
+ this.envelope = config.envelope;
19
+ this.subscriber = config.subscriber;
20
+ this.hooks = config.hooks;
21
+ this.checkpoint = config.existingCheckpoint ?? {
22
+ envelopeId: config.envelope.id,
23
+ subscriberName: config.subscriber.name,
24
+ completedSteps: {},
25
+ };
26
+ }
27
+ get attempt() {
28
+ return this.envelope.docket.attempts;
29
+ }
30
+ get isRetry() {
31
+ return this.attempt > 1;
32
+ }
33
+ async io(key, fn) {
34
+ // Validate key uniqueness within this execution
35
+ if (this.usedKeys.has(key)) {
36
+ throw new DuplicateIoKeyError(key, this.subscriber.name);
37
+ }
38
+ this.usedKeys.add(key);
39
+ // Check cache first
40
+ if (key in this.checkpoint.completedSteps) {
41
+ await this.hooks?.onCheckpointHit?.({
42
+ envelope: this.envelope,
43
+ subscriber: this.subscriber,
44
+ stepKey: key,
45
+ });
46
+ return this.checkpoint.completedSteps[key];
47
+ }
48
+ // Notify cache miss
49
+ await this.hooks?.onCheckpointMiss?.({
50
+ envelope: this.envelope,
51
+ subscriber: this.subscriber,
52
+ stepKey: key,
53
+ });
54
+ // Execute the function - errors propagate, no caching on failure
55
+ const result = await fn();
56
+ // Cache the result
57
+ this.checkpoint = {
58
+ ...this.checkpoint,
59
+ completedSteps: {
60
+ ...this.checkpoint.completedSteps,
61
+ [key]: result,
62
+ },
63
+ };
64
+ // Persist checkpoint immediately (incremental persistence)
65
+ await this.store.set(this.envelope.id, this.checkpoint);
66
+ return result;
67
+ }
68
+ async all(ops) {
69
+ // First, check for duplicates within this all() call
70
+ const keysInThisCall = new Set();
71
+ for (const [key] of ops) {
72
+ if (keysInThisCall.has(key)) {
73
+ throw new DuplicateIoKeyError(key, this.subscriber.name);
74
+ }
75
+ keysInThisCall.add(key);
76
+ }
77
+ // Then validate against previously used keys
78
+ for (const [key] of ops) {
79
+ if (this.usedKeys.has(key)) {
80
+ throw new DuplicateIoKeyError(key, this.subscriber.name);
81
+ }
82
+ }
83
+ // Mark all keys as used
84
+ for (const [key] of ops) {
85
+ this.usedKeys.add(key);
86
+ }
87
+ // Execute all operations in parallel
88
+ const results = await Promise.all(ops.map(async ([key, fn]) => {
89
+ // Check cache first
90
+ if (key in this.checkpoint.completedSteps) {
91
+ await this.hooks?.onCheckpointHit?.({
92
+ envelope: this.envelope,
93
+ subscriber: this.subscriber,
94
+ stepKey: key,
95
+ });
96
+ return this.checkpoint.completedSteps[key];
97
+ }
98
+ // Notify cache miss
99
+ await this.hooks?.onCheckpointMiss?.({
100
+ envelope: this.envelope,
101
+ subscriber: this.subscriber,
102
+ stepKey: key,
103
+ });
104
+ // Execute - errors propagate
105
+ const result = await fn();
106
+ // Cache individually (in local state, will persist after Promise.all)
107
+ this.checkpoint = {
108
+ ...this.checkpoint,
109
+ completedSteps: {
110
+ ...this.checkpoint.completedSteps,
111
+ [key]: result,
112
+ },
113
+ };
114
+ return result;
115
+ }));
116
+ // Persist after all parallel operations complete
117
+ await this.store.set(this.envelope.id, this.checkpoint);
118
+ return results;
119
+ }
120
+ /**
121
+ * Clears the checkpoint from storage.
122
+ * Called after successful completion or dead-letter.
123
+ */
124
+ async clear() {
125
+ await this.store.delete(this.envelope.id);
126
+ }
127
+ /**
128
+ * Gets the current checkpoint state.
129
+ * Useful for debugging and testing.
130
+ */
131
+ getCheckpoint() {
132
+ return this.checkpoint;
133
+ }
134
+ /**
135
+ * Gets the number of cached steps.
136
+ */
137
+ get cachedStepCount() {
138
+ return Object.keys(this.checkpoint.completedSteps).length;
139
+ }
140
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=context.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.test.d.ts","sourceRoot":"","sources":["../../src/checkpoint/context.test.ts"],"names":[],"mappings":""}