@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.
- package/cli.ts +453 -0
- package/dist/checkpoint/context.d.ts +59 -0
- package/dist/checkpoint/context.d.ts.map +1 -0
- package/dist/checkpoint/context.js +140 -0
- package/dist/checkpoint/context.test.d.ts +2 -0
- package/dist/checkpoint/context.test.d.ts.map +1 -0
- package/dist/checkpoint/context.test.js +424 -0
- package/dist/checkpoint/index.d.ts +7 -0
- package/dist/checkpoint/index.d.ts.map +1 -0
- package/dist/checkpoint/index.js +6 -0
- package/dist/checkpoint/stores/memory.d.ts +29 -0
- package/dist/checkpoint/stores/memory.d.ts.map +1 -0
- package/dist/checkpoint/stores/memory.js +39 -0
- package/dist/checkpoint/stores/noop.d.ts +14 -0
- package/dist/checkpoint/stores/noop.d.ts.map +1 -0
- package/dist/checkpoint/stores/noop.js +18 -0
- package/dist/checkpoint/stores/stores.test.d.ts +2 -0
- package/dist/checkpoint/stores/stores.test.d.ts.map +1 -0
- package/dist/checkpoint/stores/stores.test.js +146 -0
- package/dist/checkpoint/types.d.ts +119 -0
- package/dist/checkpoint/types.d.ts.map +1 -0
- package/dist/checkpoint/types.js +1 -0
- package/dist/codec/codec.d.ts +29 -0
- package/dist/codec/codec.d.ts.map +1 -0
- package/dist/codec/codec.js +15 -0
- package/dist/codec/header-aware-codec.d.ts +36 -0
- package/dist/codec/header-aware-codec.d.ts.map +1 -0
- package/dist/codec/header-aware-codec.js +1 -0
- package/dist/codec/index.d.ts +6 -0
- package/dist/codec/index.d.ts.map +1 -0
- package/dist/codec/index.js +3 -0
- package/dist/codec/json-codec.d.ts +13 -0
- package/dist/codec/json-codec.d.ts.map +1 -0
- package/dist/codec/json-codec.js +64 -0
- package/dist/codec/rabbitmq-codec.d.ts +28 -0
- package/dist/codec/rabbitmq-codec.d.ts.map +1 -0
- package/dist/codec/rabbitmq-codec.js +242 -0
- package/dist/codec/rabbitmq-codec.test.d.ts +2 -0
- package/dist/codec/rabbitmq-codec.test.d.ts.map +1 -0
- package/dist/codec/rabbitmq-codec.test.js +433 -0
- package/dist/core/fanout.d.ts +59 -0
- package/dist/core/fanout.d.ts.map +1 -0
- package/dist/core/fanout.js +121 -0
- package/dist/core/fanout.test.d.ts +2 -0
- package/dist/core/fanout.test.d.ts.map +1 -0
- package/dist/core/fanout.test.js +1055 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +3 -0
- package/dist/core/matador.d.ts +133 -0
- package/dist/core/matador.d.ts.map +1 -0
- package/dist/core/matador.js +222 -0
- package/dist/core/matador.test.d.ts +2 -0
- package/dist/core/matador.test.d.ts.map +1 -0
- package/dist/core/matador.test.js +446 -0
- package/dist/core/shutdown.d.ts +78 -0
- package/dist/core/shutdown.d.ts.map +1 -0
- package/dist/core/shutdown.js +111 -0
- package/dist/core/shutdown.test.d.ts +2 -0
- package/dist/core/shutdown.test.d.ts.map +1 -0
- package/dist/core/shutdown.test.js +524 -0
- package/dist/errors/checkpoint-errors.d.ts +30 -0
- package/dist/errors/checkpoint-errors.d.ts.map +1 -0
- package/dist/errors/checkpoint-errors.js +49 -0
- package/dist/errors/has-description.d.ts +18 -0
- package/dist/errors/has-description.d.ts.map +1 -0
- package/dist/errors/has-description.js +9 -0
- package/dist/errors/index.d.ts +6 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +22 -0
- package/dist/errors/matador-errors.d.ts +183 -0
- package/dist/errors/matador-errors.d.ts.map +1 -0
- package/dist/errors/matador-errors.js +376 -0
- package/dist/errors/retry-errors.d.ts +67 -0
- package/dist/errors/retry-errors.d.ts.map +1 -0
- package/dist/errors/retry-errors.js +108 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/safe-hooks.d.ts +34 -0
- package/dist/hooks/safe-hooks.d.ts.map +1 -0
- package/dist/hooks/safe-hooks.js +135 -0
- package/dist/hooks/types.d.ts +189 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +9 -0
- package/dist/index.cjs +3156 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline/index.d.ts +3 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/pipeline.d.ts +60 -0
- package/dist/pipeline/pipeline.d.ts.map +1 -0
- package/dist/pipeline/pipeline.js +261 -0
- package/dist/pipeline/pipeline.test.d.ts +2 -0
- package/dist/pipeline/pipeline.test.d.ts.map +1 -0
- package/dist/pipeline/pipeline.test.js +1065 -0
- package/dist/retry/index.d.ts +4 -0
- package/dist/retry/index.d.ts.map +1 -0
- package/dist/retry/index.js +1 -0
- package/dist/retry/policy.d.ts +43 -0
- package/dist/retry/policy.d.ts.map +1 -0
- package/dist/retry/policy.js +1 -0
- package/dist/retry/standard-policy.d.ts +44 -0
- package/dist/retry/standard-policy.d.ts.map +1 -0
- package/dist/retry/standard-policy.js +102 -0
- package/dist/retry/standard-policy.test.d.ts +2 -0
- package/dist/retry/standard-policy.test.d.ts.map +1 -0
- package/dist/retry/standard-policy.test.js +190 -0
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +2 -0
- package/dist/schema/registry.d.ts +63 -0
- package/dist/schema/registry.d.ts.map +1 -0
- package/dist/schema/registry.js +171 -0
- package/dist/schema/registry.test.d.ts +2 -0
- package/dist/schema/registry.test.d.ts.map +1 -0
- package/dist/schema/registry.test.js +278 -0
- package/dist/schema/types.d.ts +158 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +74 -0
- package/dist/schema/types.test.d.ts +2 -0
- package/dist/schema/types.test.d.ts.map +1 -0
- package/dist/schema/types.test.js +243 -0
- package/dist/topology/builder.d.ts +66 -0
- package/dist/topology/builder.d.ts.map +1 -0
- package/dist/topology/builder.js +156 -0
- package/dist/topology/builder.test.d.ts +2 -0
- package/dist/topology/builder.test.d.ts.map +1 -0
- package/dist/topology/builder.test.js +222 -0
- package/dist/topology/index.d.ts +5 -0
- package/dist/topology/index.d.ts.map +1 -0
- package/dist/topology/index.js +2 -0
- package/dist/topology/types.d.ts +76 -0
- package/dist/topology/types.d.ts.map +1 -0
- package/dist/topology/types.js +18 -0
- package/dist/transport/capabilities.d.ts +66 -0
- package/dist/transport/capabilities.d.ts.map +1 -0
- package/dist/transport/capabilities.js +18 -0
- package/dist/transport/connection-manager.d.ts +95 -0
- package/dist/transport/connection-manager.d.ts.map +1 -0
- package/dist/transport/connection-manager.js +144 -0
- package/dist/transport/index.d.ts +11 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +5 -0
- package/dist/transport/local/local-transport.d.ts +62 -0
- package/dist/transport/local/local-transport.d.ts.map +1 -0
- package/dist/transport/local/local-transport.js +241 -0
- package/dist/transport/local/local-transport.test.d.ts +2 -0
- package/dist/transport/local/local-transport.test.d.ts.map +1 -0
- package/dist/transport/local/local-transport.test.js +192 -0
- package/dist/transport/multi/multi-transport.d.ts +94 -0
- package/dist/transport/multi/multi-transport.d.ts.map +1 -0
- package/dist/transport/multi/multi-transport.js +184 -0
- package/dist/transport/multi/multi-transport.test.d.ts +2 -0
- package/dist/transport/multi/multi-transport.test.d.ts.map +1 -0
- package/dist/transport/multi/multi-transport.test.js +236 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts +73 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.d.ts.map +1 -0
- package/dist/transport/rabbitmq/rabbitmq-transport.js +518 -0
- package/dist/transport/transport.d.ts +152 -0
- package/dist/transport/transport.d.ts.map +1 -0
- package/dist/transport/transport.js +1 -0
- package/dist/types/common.d.ts +41 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +12 -0
- package/dist/types/envelope.d.ts +82 -0
- package/dist/types/envelope.d.ts.map +1 -0
- package/dist/types/envelope.js +36 -0
- package/dist/types/event.d.ts +96 -0
- package/dist/types/event.d.ts.map +1 -0
- package/dist/types/event.js +26 -0
- package/dist/types/event.test.d.ts +2 -0
- package/dist/types/event.test.d.ts.map +1 -0
- package/dist/types/event.test.js +130 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/dist/types/subscriber.d.ts +207 -0
- package/dist/types/subscriber.d.ts.map +1 -0
- package/dist/types/subscriber.js +99 -0
- package/examples/config.ts +126 -0
- package/examples/event.ts +26 -0
- package/examples/order-event.json +19 -0
- package/package.json +66 -0
- package/src/checkpoint/context.test.ts +510 -0
- package/src/checkpoint/context.ts +213 -0
- package/src/checkpoint/index.ts +30 -0
- package/src/checkpoint/stores/memory.ts +47 -0
- package/src/checkpoint/stores/noop.ts +22 -0
- package/src/checkpoint/stores/stores.test.ts +177 -0
- package/src/checkpoint/types.ts +147 -0
- package/src/codec/codec.ts +42 -0
- package/src/codec/header-aware-codec.ts +41 -0
- package/src/codec/index.ts +11 -0
- package/src/codec/json-codec.ts +69 -0
- package/src/codec/rabbitmq-codec.test.ts +516 -0
- package/src/codec/rabbitmq-codec.ts +336 -0
- package/src/core/fanout.test.ts +1351 -0
- package/src/core/fanout.ts +184 -0
- package/src/core/index.ts +12 -0
- package/src/core/matador.test.ts +575 -0
- package/src/core/matador.ts +344 -0
- package/src/core/shutdown.test.ts +853 -0
- package/src/core/shutdown.ts +165 -0
- package/src/errors/checkpoint-errors.ts +62 -0
- package/src/errors/has-description.ts +25 -0
- package/src/errors/index.ts +57 -0
- package/src/errors/matador-errors.ts +477 -0
- package/src/errors/retry-errors.ts +134 -0
- package/src/hooks/index.ts +15 -0
- package/src/hooks/safe-hooks.ts +223 -0
- package/src/hooks/types.ts +248 -0
- package/src/index.ts +231 -0
- package/src/pipeline/index.ts +2 -0
- package/src/pipeline/pipeline.test.ts +1317 -0
- package/src/pipeline/pipeline.ts +371 -0
- package/src/retry/index.ts +4 -0
- package/src/retry/policy.ts +46 -0
- package/src/retry/standard-policy.test.ts +282 -0
- package/src/retry/standard-policy.ts +156 -0
- package/src/schema/index.ts +16 -0
- package/src/schema/registry.test.ts +339 -0
- package/src/schema/registry.ts +226 -0
- package/src/schema/types.test.ts +281 -0
- package/src/schema/types.ts +217 -0
- package/src/topology/builder.test.ts +275 -0
- package/src/topology/builder.ts +199 -0
- package/src/topology/index.ts +15 -0
- package/src/topology/types.ts +109 -0
- package/src/transport/capabilities.ts +88 -0
- package/src/transport/connection-manager.ts +218 -0
- package/src/transport/index.ts +42 -0
- package/src/transport/local/local-transport.test.ts +262 -0
- package/src/transport/local/local-transport.ts +327 -0
- package/src/transport/multi/multi-transport.test.ts +320 -0
- package/src/transport/multi/multi-transport.ts +294 -0
- package/src/transport/rabbitmq/rabbitmq-transport.ts +753 -0
- package/src/transport/transport.ts +200 -0
- package/src/types/common.ts +53 -0
- package/src/types/envelope.ts +152 -0
- package/src/types/event.test.ts +157 -0
- package/src/types/event.ts +118 -0
- package/src/types/index.ts +52 -0
- package/src/types/subscriber.ts +310 -0
- package/test/e2e/multi-transport.e2e.test.ts +236 -0
- package/test/e2e/rabbitmq-transport.e2e.test.ts +327 -0
- package/test/e2e/transport-compliance.e2e.test.ts +505 -0
- package/test/integration/matador.integration.test.ts +634 -0
- package/tsconfig.json +29 -0
- package/tsconfig.tsbuildinfo +1 -0
- 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 @@
|
|
|
1
|
+
{"version":3,"file":"context.test.d.ts","sourceRoot":"","sources":["../../src/checkpoint/context.test.ts"],"names":[],"mappings":""}
|