@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
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'bun:test';
|
|
2
|
+
import { DuplicateIoKeyError } from '../errors/index.js';
|
|
3
|
+
import { ResumableContext } from './context.js';
|
|
4
|
+
import { MemoryCheckpointStore } from './stores/memory.js';
|
|
5
|
+
function createTestEnvelope(id = 'test-envelope-id', attempts = 1) {
|
|
6
|
+
return {
|
|
7
|
+
id,
|
|
8
|
+
data: { userId: '123' },
|
|
9
|
+
docket: {
|
|
10
|
+
eventKey: 'test.event',
|
|
11
|
+
targetSubscriber: 'test-subscriber',
|
|
12
|
+
importance: 'should-investigate',
|
|
13
|
+
attempts,
|
|
14
|
+
createdAt: new Date().toISOString(),
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function createTestSubscriber(name = 'test-subscriber') {
|
|
19
|
+
return {
|
|
20
|
+
name,
|
|
21
|
+
description: 'Test subscriber',
|
|
22
|
+
idempotent: 'resumable',
|
|
23
|
+
importance: 'should-investigate',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
describe('ResumableContext', () => {
|
|
27
|
+
let store;
|
|
28
|
+
let envelope;
|
|
29
|
+
let subscriber;
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
store = new MemoryCheckpointStore();
|
|
32
|
+
envelope = createTestEnvelope();
|
|
33
|
+
subscriber = createTestSubscriber();
|
|
34
|
+
});
|
|
35
|
+
describe('io()', () => {
|
|
36
|
+
it('should execute function and cache result on first call', async () => {
|
|
37
|
+
const context = new ResumableContext({
|
|
38
|
+
store,
|
|
39
|
+
envelope,
|
|
40
|
+
subscriber,
|
|
41
|
+
});
|
|
42
|
+
let executeCount = 0;
|
|
43
|
+
const result = await context.io('step-1', () => {
|
|
44
|
+
executeCount++;
|
|
45
|
+
return 'result-1';
|
|
46
|
+
});
|
|
47
|
+
expect(result).toBe('result-1');
|
|
48
|
+
expect(executeCount).toBe(1);
|
|
49
|
+
expect(store.size).toBe(1);
|
|
50
|
+
});
|
|
51
|
+
it('should return cached result on retry without re-executing', async () => {
|
|
52
|
+
// First execution - cache the result
|
|
53
|
+
const firstContext = new ResumableContext({
|
|
54
|
+
store,
|
|
55
|
+
envelope,
|
|
56
|
+
subscriber,
|
|
57
|
+
});
|
|
58
|
+
let executeCount = 0;
|
|
59
|
+
await firstContext.io('step-1', () => {
|
|
60
|
+
executeCount++;
|
|
61
|
+
return 'result-1';
|
|
62
|
+
});
|
|
63
|
+
expect(executeCount).toBe(1);
|
|
64
|
+
// Simulate retry - load existing checkpoint
|
|
65
|
+
const checkpoint = await store.get(envelope.id);
|
|
66
|
+
const retryContext = new ResumableContext({
|
|
67
|
+
store,
|
|
68
|
+
envelope: createTestEnvelope(envelope.id, 2),
|
|
69
|
+
subscriber,
|
|
70
|
+
existingCheckpoint: checkpoint,
|
|
71
|
+
});
|
|
72
|
+
const result = await retryContext.io('step-1', () => {
|
|
73
|
+
executeCount++;
|
|
74
|
+
return 'should-not-be-used';
|
|
75
|
+
});
|
|
76
|
+
expect(result).toBe('result-1');
|
|
77
|
+
expect(executeCount).toBe(1); // Still 1, not re-executed
|
|
78
|
+
});
|
|
79
|
+
it('should throw DuplicateIoKeyError on duplicate key within same execution', async () => {
|
|
80
|
+
const context = new ResumableContext({
|
|
81
|
+
store,
|
|
82
|
+
envelope,
|
|
83
|
+
subscriber,
|
|
84
|
+
});
|
|
85
|
+
await context.io('same-key', () => 'first');
|
|
86
|
+
await expect(context.io('same-key', () => 'second')).rejects.toThrow(DuplicateIoKeyError);
|
|
87
|
+
});
|
|
88
|
+
it('should handle async functions', async () => {
|
|
89
|
+
const context = new ResumableContext({
|
|
90
|
+
store,
|
|
91
|
+
envelope,
|
|
92
|
+
subscriber,
|
|
93
|
+
});
|
|
94
|
+
const result = await context.io('async-step', async () => {
|
|
95
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
96
|
+
return { value: 42 };
|
|
97
|
+
});
|
|
98
|
+
expect(result).toEqual({ value: 42 });
|
|
99
|
+
});
|
|
100
|
+
it('should not cache failed operations', async () => {
|
|
101
|
+
const context = new ResumableContext({
|
|
102
|
+
store,
|
|
103
|
+
envelope,
|
|
104
|
+
subscriber,
|
|
105
|
+
});
|
|
106
|
+
// First attempt - fails
|
|
107
|
+
await expect(context.io('failing-step', () => {
|
|
108
|
+
throw new Error('Operation failed');
|
|
109
|
+
})).rejects.toThrow('Operation failed');
|
|
110
|
+
// Checkpoint should not contain the failed step
|
|
111
|
+
const checkpoint = await store.get(envelope.id);
|
|
112
|
+
expect(checkpoint?.completedSteps['failing-step']).toBeUndefined();
|
|
113
|
+
});
|
|
114
|
+
it('should persist checkpoint after each io() call', async () => {
|
|
115
|
+
const context = new ResumableContext({
|
|
116
|
+
store,
|
|
117
|
+
envelope,
|
|
118
|
+
subscriber,
|
|
119
|
+
});
|
|
120
|
+
await context.io('step-1', () => 'result-1');
|
|
121
|
+
let checkpoint = await store.get(envelope.id);
|
|
122
|
+
expect(Object.keys(checkpoint.completedSteps)).toHaveLength(1);
|
|
123
|
+
await context.io('step-2', () => 'result-2');
|
|
124
|
+
checkpoint = await store.get(envelope.id);
|
|
125
|
+
expect(Object.keys(checkpoint.completedSteps)).toHaveLength(2);
|
|
126
|
+
});
|
|
127
|
+
it('should support various JSON-serializable return types', async () => {
|
|
128
|
+
const context = new ResumableContext({
|
|
129
|
+
store,
|
|
130
|
+
envelope,
|
|
131
|
+
subscriber,
|
|
132
|
+
});
|
|
133
|
+
expect(await context.io('string', () => 'hello')).toBe('hello');
|
|
134
|
+
expect(await context.io('number', () => 42)).toBe(42);
|
|
135
|
+
expect(await context.io('boolean', () => true)).toBe(true);
|
|
136
|
+
expect(await context.io('null', () => null)).toBe(null);
|
|
137
|
+
expect(await context.io('array', () => [1, 2, 3])).toEqual([1, 2, 3]);
|
|
138
|
+
expect(await context.io('object', () => ({ foo: 'bar' }))).toEqual({
|
|
139
|
+
foo: 'bar',
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe('all()', () => {
|
|
144
|
+
it('should execute multiple operations in parallel', async () => {
|
|
145
|
+
const context = new ResumableContext({
|
|
146
|
+
store,
|
|
147
|
+
envelope,
|
|
148
|
+
subscriber,
|
|
149
|
+
});
|
|
150
|
+
const executionOrder = [];
|
|
151
|
+
const [a, b, c] = await context.all([
|
|
152
|
+
[
|
|
153
|
+
'fetch-a',
|
|
154
|
+
() => {
|
|
155
|
+
executionOrder.push('a');
|
|
156
|
+
return 'result-a';
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
[
|
|
160
|
+
'fetch-b',
|
|
161
|
+
() => {
|
|
162
|
+
executionOrder.push('b');
|
|
163
|
+
return 'result-b';
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
[
|
|
167
|
+
'fetch-c',
|
|
168
|
+
() => {
|
|
169
|
+
executionOrder.push('c');
|
|
170
|
+
return 'result-c';
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
]);
|
|
174
|
+
expect(a).toBe('result-a');
|
|
175
|
+
expect(b).toBe('result-b');
|
|
176
|
+
expect(c).toBe('result-c');
|
|
177
|
+
expect(executionOrder).toHaveLength(3);
|
|
178
|
+
});
|
|
179
|
+
it('should cache all results on retry', async () => {
|
|
180
|
+
// First execution
|
|
181
|
+
const firstContext = new ResumableContext({
|
|
182
|
+
store,
|
|
183
|
+
envelope,
|
|
184
|
+
subscriber,
|
|
185
|
+
});
|
|
186
|
+
let executeCount = 0;
|
|
187
|
+
await firstContext.all([
|
|
188
|
+
[
|
|
189
|
+
'fetch-a',
|
|
190
|
+
() => {
|
|
191
|
+
executeCount++;
|
|
192
|
+
return 'result-a';
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
[
|
|
196
|
+
'fetch-b',
|
|
197
|
+
() => {
|
|
198
|
+
executeCount++;
|
|
199
|
+
return 'result-b';
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
]);
|
|
203
|
+
expect(executeCount).toBe(2);
|
|
204
|
+
// Retry with cached checkpoint
|
|
205
|
+
const checkpoint = await store.get(envelope.id);
|
|
206
|
+
const retryContext = new ResumableContext({
|
|
207
|
+
store,
|
|
208
|
+
envelope: createTestEnvelope(envelope.id, 2),
|
|
209
|
+
subscriber,
|
|
210
|
+
existingCheckpoint: checkpoint,
|
|
211
|
+
});
|
|
212
|
+
const [a, b] = await retryContext.all([
|
|
213
|
+
[
|
|
214
|
+
'fetch-a',
|
|
215
|
+
() => {
|
|
216
|
+
executeCount++;
|
|
217
|
+
return 'new-a';
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
[
|
|
221
|
+
'fetch-b',
|
|
222
|
+
() => {
|
|
223
|
+
executeCount++;
|
|
224
|
+
return 'new-b';
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
]);
|
|
228
|
+
expect(a).toBe('result-a'); // Cached
|
|
229
|
+
expect(b).toBe('result-b'); // Cached
|
|
230
|
+
expect(executeCount).toBe(2); // Still 2, not re-executed
|
|
231
|
+
});
|
|
232
|
+
it('should throw on duplicate key in all()', async () => {
|
|
233
|
+
const context = new ResumableContext({
|
|
234
|
+
store,
|
|
235
|
+
envelope,
|
|
236
|
+
subscriber,
|
|
237
|
+
});
|
|
238
|
+
await expect(context.all([
|
|
239
|
+
['same-key', () => 'first'],
|
|
240
|
+
['same-key', () => 'second'],
|
|
241
|
+
])).rejects.toThrow(DuplicateIoKeyError);
|
|
242
|
+
});
|
|
243
|
+
it('should throw if all() key conflicts with previous io() key', async () => {
|
|
244
|
+
const context = new ResumableContext({
|
|
245
|
+
store,
|
|
246
|
+
envelope,
|
|
247
|
+
subscriber,
|
|
248
|
+
});
|
|
249
|
+
await context.io('used-key', () => 'first');
|
|
250
|
+
await expect(context.all([
|
|
251
|
+
['used-key', () => 'second'],
|
|
252
|
+
['new-key', () => 'third'],
|
|
253
|
+
])).rejects.toThrow(DuplicateIoKeyError);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
describe('attempt and isRetry', () => {
|
|
257
|
+
it('should report attempt number from envelope', async () => {
|
|
258
|
+
const context = new ResumableContext({
|
|
259
|
+
store,
|
|
260
|
+
envelope: createTestEnvelope('test', 3),
|
|
261
|
+
subscriber,
|
|
262
|
+
});
|
|
263
|
+
expect(context.attempt).toBe(3);
|
|
264
|
+
});
|
|
265
|
+
it('should report isRetry as false for first attempt', async () => {
|
|
266
|
+
const context = new ResumableContext({
|
|
267
|
+
store,
|
|
268
|
+
envelope: createTestEnvelope('test', 1),
|
|
269
|
+
subscriber,
|
|
270
|
+
});
|
|
271
|
+
expect(context.isRetry).toBe(false);
|
|
272
|
+
});
|
|
273
|
+
it('should report isRetry as true for subsequent attempts', async () => {
|
|
274
|
+
const context = new ResumableContext({
|
|
275
|
+
store,
|
|
276
|
+
envelope: createTestEnvelope('test', 2),
|
|
277
|
+
subscriber,
|
|
278
|
+
});
|
|
279
|
+
expect(context.isRetry).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
describe('clear()', () => {
|
|
283
|
+
it('should delete checkpoint from store', async () => {
|
|
284
|
+
const context = new ResumableContext({
|
|
285
|
+
store,
|
|
286
|
+
envelope,
|
|
287
|
+
subscriber,
|
|
288
|
+
});
|
|
289
|
+
await context.io('step-1', () => 'result');
|
|
290
|
+
expect(store.size).toBe(1);
|
|
291
|
+
await context.clear();
|
|
292
|
+
expect(store.size).toBe(0);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
describe('hooks', () => {
|
|
296
|
+
it('should call onCheckpointHit when using cached value', async () => {
|
|
297
|
+
const onCheckpointHit = vi.fn();
|
|
298
|
+
// First execution - populate cache
|
|
299
|
+
const firstContext = new ResumableContext({
|
|
300
|
+
store,
|
|
301
|
+
envelope,
|
|
302
|
+
subscriber,
|
|
303
|
+
});
|
|
304
|
+
await firstContext.io('cached-step', () => 'cached-result');
|
|
305
|
+
// Retry with hooks
|
|
306
|
+
const checkpoint = await store.get(envelope.id);
|
|
307
|
+
const retryContext = new ResumableContext({
|
|
308
|
+
store,
|
|
309
|
+
envelope: createTestEnvelope(envelope.id, 2),
|
|
310
|
+
subscriber,
|
|
311
|
+
existingCheckpoint: checkpoint,
|
|
312
|
+
hooks: { onCheckpointHit },
|
|
313
|
+
});
|
|
314
|
+
await retryContext.io('cached-step', () => 'new-result');
|
|
315
|
+
expect(onCheckpointHit).toHaveBeenCalledWith({
|
|
316
|
+
envelope: expect.anything(),
|
|
317
|
+
subscriber,
|
|
318
|
+
stepKey: 'cached-step',
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
it('should call onCheckpointMiss when executing fresh', async () => {
|
|
322
|
+
const onCheckpointMiss = vi.fn();
|
|
323
|
+
const context = new ResumableContext({
|
|
324
|
+
store,
|
|
325
|
+
envelope,
|
|
326
|
+
subscriber,
|
|
327
|
+
hooks: { onCheckpointMiss },
|
|
328
|
+
});
|
|
329
|
+
await context.io('fresh-step', () => 'result');
|
|
330
|
+
expect(onCheckpointMiss).toHaveBeenCalledWith({
|
|
331
|
+
envelope,
|
|
332
|
+
subscriber,
|
|
333
|
+
stepKey: 'fresh-step',
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
describe('real-world scenarios', () => {
|
|
338
|
+
it('should replay cached steps and execute new ones on retry', async () => {
|
|
339
|
+
const executionLog = [];
|
|
340
|
+
let shouldFail = true;
|
|
341
|
+
// First attempt - fails after step-1
|
|
342
|
+
const firstContext = new ResumableContext({
|
|
343
|
+
store,
|
|
344
|
+
envelope,
|
|
345
|
+
subscriber,
|
|
346
|
+
});
|
|
347
|
+
try {
|
|
348
|
+
await firstContext.io('step-0', () => {
|
|
349
|
+
executionLog.push('step-0');
|
|
350
|
+
return 'result-0';
|
|
351
|
+
});
|
|
352
|
+
await firstContext.io('step-1', () => {
|
|
353
|
+
executionLog.push('step-1');
|
|
354
|
+
return 'result-1';
|
|
355
|
+
});
|
|
356
|
+
if (shouldFail) {
|
|
357
|
+
shouldFail = false;
|
|
358
|
+
throw new Error('Simulated failure');
|
|
359
|
+
}
|
|
360
|
+
await firstContext.io('step-2', () => {
|
|
361
|
+
executionLog.push('step-2');
|
|
362
|
+
return 'result-2';
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
// Expected failure
|
|
367
|
+
}
|
|
368
|
+
expect(executionLog).toEqual(['step-0', 'step-1']);
|
|
369
|
+
// Retry - step-0 and step-1 use cache, step-2 executes
|
|
370
|
+
executionLog.length = 0;
|
|
371
|
+
const checkpoint = await store.get(envelope.id);
|
|
372
|
+
const retryContext = new ResumableContext({
|
|
373
|
+
store,
|
|
374
|
+
envelope: createTestEnvelope(envelope.id, 2),
|
|
375
|
+
subscriber,
|
|
376
|
+
existingCheckpoint: checkpoint,
|
|
377
|
+
});
|
|
378
|
+
await retryContext.io('step-0', () => {
|
|
379
|
+
executionLog.push('step-0');
|
|
380
|
+
return 'should-not-use';
|
|
381
|
+
});
|
|
382
|
+
await retryContext.io('step-1', () => {
|
|
383
|
+
executionLog.push('step-1');
|
|
384
|
+
return 'should-not-use';
|
|
385
|
+
});
|
|
386
|
+
await retryContext.io('step-2', () => {
|
|
387
|
+
executionLog.push('step-2');
|
|
388
|
+
return 'result-2';
|
|
389
|
+
});
|
|
390
|
+
expect(executionLog).toEqual(['step-2']); // Only step-2 executed!
|
|
391
|
+
});
|
|
392
|
+
it('should handle conditional io() calls correctly', async () => {
|
|
393
|
+
const context = new ResumableContext({
|
|
394
|
+
store,
|
|
395
|
+
envelope: { ...envelope, data: { sendEmail: true } },
|
|
396
|
+
subscriber,
|
|
397
|
+
});
|
|
398
|
+
const result = await context.io('send-email', () => ({
|
|
399
|
+
messageId: 'msg-123',
|
|
400
|
+
}));
|
|
401
|
+
expect(result).toEqual({ messageId: 'msg-123' });
|
|
402
|
+
});
|
|
403
|
+
it('should handle dynamic loop with unique keys', async () => {
|
|
404
|
+
const context = new ResumableContext({
|
|
405
|
+
store,
|
|
406
|
+
envelope,
|
|
407
|
+
subscriber,
|
|
408
|
+
});
|
|
409
|
+
const items = [
|
|
410
|
+
{ id: 'item-1', value: 10 },
|
|
411
|
+
{ id: 'item-2', value: 20 },
|
|
412
|
+
{ id: 'item-3', value: 30 },
|
|
413
|
+
];
|
|
414
|
+
const results = [];
|
|
415
|
+
for (const item of items) {
|
|
416
|
+
const processed = await context.io(`process-${item.id}`, () => {
|
|
417
|
+
return item.value * 2;
|
|
418
|
+
});
|
|
419
|
+
results.push(processed);
|
|
420
|
+
}
|
|
421
|
+
expect(results).toEqual([20, 40, 60]);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { Checkpoint, CheckpointClearedContext, CheckpointHitContext, CheckpointLoadedContext, CheckpointMissContext, CheckpointStore, JsonSerializable, SubscriberContext, } from './types.js';
|
|
2
|
+
export type { ResumableContextConfig, ResumableContextHooks, } from './context.js';
|
|
3
|
+
export { ResumableContext } from './context.js';
|
|
4
|
+
export { MemoryCheckpointStore } from './stores/memory.js';
|
|
5
|
+
export { NoOpCheckpointStore } from './stores/noop.js';
|
|
6
|
+
export { CheckpointStoreError, DuplicateIoKeyError, isCheckpointStoreError, isDuplicateIoKeyError, } from '../errors/index.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/checkpoint/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,UAAU,EACV,wBAAwB,EACxB,oBAAoB,EACpB,uBAAuB,EACvB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGhD,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ResumableContext } from './context.js';
|
|
2
|
+
// Stores
|
|
3
|
+
export { MemoryCheckpointStore } from './stores/memory.js';
|
|
4
|
+
export { NoOpCheckpointStore } from './stores/noop.js';
|
|
5
|
+
// Errors (re-exported from ../errors for convenience)
|
|
6
|
+
export { CheckpointStoreError, DuplicateIoKeyError, isCheckpointStoreError, isDuplicateIoKeyError, } from '../errors/index.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Checkpoint, CheckpointStore } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* In-memory checkpoint store for testing and development.
|
|
4
|
+
* Checkpoints are lost when the process restarts.
|
|
5
|
+
*
|
|
6
|
+
* For production use, implement a persistent store (e.g., RedisCheckpointStore).
|
|
7
|
+
*/
|
|
8
|
+
export declare class MemoryCheckpointStore implements CheckpointStore {
|
|
9
|
+
private readonly checkpoints;
|
|
10
|
+
get(envelopeId: string): Promise<Checkpoint | undefined>;
|
|
11
|
+
set(envelopeId: string, checkpoint: Checkpoint): Promise<void>;
|
|
12
|
+
delete(envelopeId: string): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Gets the number of stored checkpoints.
|
|
15
|
+
* Useful for testing.
|
|
16
|
+
*/
|
|
17
|
+
get size(): number;
|
|
18
|
+
/**
|
|
19
|
+
* Clears all stored checkpoints.
|
|
20
|
+
* Useful for testing.
|
|
21
|
+
*/
|
|
22
|
+
clear(): void;
|
|
23
|
+
/**
|
|
24
|
+
* Gets all stored checkpoint IDs.
|
|
25
|
+
* Useful for testing.
|
|
26
|
+
*/
|
|
27
|
+
keys(): string[];
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../../src/checkpoint/stores/memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE/D;;;;;GAKG;AACH,qBAAa,qBAAsB,YAAW,eAAe;IAC3D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAiC;IAEvD,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IAIxD,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9D,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/C;;;OAGG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;OAGG;IACH,KAAK,IAAI,IAAI;IAIb;;;OAGG;IACH,IAAI,IAAI,MAAM,EAAE;CAGjB"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory checkpoint store for testing and development.
|
|
3
|
+
* Checkpoints are lost when the process restarts.
|
|
4
|
+
*
|
|
5
|
+
* For production use, implement a persistent store (e.g., RedisCheckpointStore).
|
|
6
|
+
*/
|
|
7
|
+
export class MemoryCheckpointStore {
|
|
8
|
+
checkpoints = new Map();
|
|
9
|
+
async get(envelopeId) {
|
|
10
|
+
return this.checkpoints.get(envelopeId);
|
|
11
|
+
}
|
|
12
|
+
async set(envelopeId, checkpoint) {
|
|
13
|
+
this.checkpoints.set(envelopeId, checkpoint);
|
|
14
|
+
}
|
|
15
|
+
async delete(envelopeId) {
|
|
16
|
+
this.checkpoints.delete(envelopeId);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Gets the number of stored checkpoints.
|
|
20
|
+
* Useful for testing.
|
|
21
|
+
*/
|
|
22
|
+
get size() {
|
|
23
|
+
return this.checkpoints.size;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Clears all stored checkpoints.
|
|
27
|
+
* Useful for testing.
|
|
28
|
+
*/
|
|
29
|
+
clear() {
|
|
30
|
+
this.checkpoints.clear();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Gets all stored checkpoint IDs.
|
|
34
|
+
* Useful for testing.
|
|
35
|
+
*/
|
|
36
|
+
keys() {
|
|
37
|
+
return [...this.checkpoints.keys()];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Checkpoint, CheckpointStore } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* No-operation checkpoint store that doesn't persist anything.
|
|
4
|
+
* Used when no checkpoint store is configured.
|
|
5
|
+
*
|
|
6
|
+
* With this store, resumable subscribers will work but checkpoints
|
|
7
|
+
* won't persist across retries - all io() operations will re-execute.
|
|
8
|
+
*/
|
|
9
|
+
export declare class NoOpCheckpointStore implements CheckpointStore {
|
|
10
|
+
get(_envelopeId: string): Promise<Checkpoint | undefined>;
|
|
11
|
+
set(_envelopeId: string, _checkpoint: Checkpoint): Promise<void>;
|
|
12
|
+
delete(_envelopeId: string): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=noop.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noop.d.ts","sourceRoot":"","sources":["../../../src/checkpoint/stores/noop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE/D;;;;;;GAMG;AACH,qBAAa,mBAAoB,YAAW,eAAe;IACnD,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IAIzD,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhE,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGjD"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* No-operation checkpoint store that doesn't persist anything.
|
|
3
|
+
* Used when no checkpoint store is configured.
|
|
4
|
+
*
|
|
5
|
+
* With this store, resumable subscribers will work but checkpoints
|
|
6
|
+
* won't persist across retries - all io() operations will re-execute.
|
|
7
|
+
*/
|
|
8
|
+
export class NoOpCheckpointStore {
|
|
9
|
+
async get(_envelopeId) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
async set(_envelopeId, _checkpoint) {
|
|
13
|
+
// No-op
|
|
14
|
+
}
|
|
15
|
+
async delete(_envelopeId) {
|
|
16
|
+
// No-op
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stores.test.d.ts","sourceRoot":"","sources":["../../../src/checkpoint/stores/stores.test.ts"],"names":[],"mappings":""}
|