@uploadista/core 0.0.17 → 0.0.18-beta.10
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/README.md +102 -0
- package/dist/{checksum-DaCqP8Qa.mjs → checksum-COoD-F1l.mjs} +2 -2
- package/dist/{checksum-DaCqP8Qa.mjs.map → checksum-COoD-F1l.mjs.map} +1 -1
- package/dist/{checksum-BIlVW8bD.cjs → checksum-YLW4hVY7.cjs} +1 -1
- package/dist/errors/index.cjs +1 -1
- package/dist/errors/index.d.cts +1 -1
- package/dist/errors/index.d.mts +1 -1
- package/dist/errors/index.mjs +1 -1
- package/dist/flow/index.cjs +1 -1
- package/dist/flow/index.d.cts +5 -5
- package/dist/flow/index.d.mts +5 -5
- package/dist/flow/index.mjs +1 -1
- package/dist/flow-BLGpxdEm.mjs +2 -0
- package/dist/flow-BLGpxdEm.mjs.map +1 -0
- package/dist/flow-DaBzRGmY.cjs +1 -0
- package/dist/{index-BGi1r_fi.d.mts → index-9gyMMEIB.d.cts} +2 -2
- package/dist/{index-BGi1r_fi.d.mts.map → index-9gyMMEIB.d.cts.map} +1 -1
- package/dist/{index-B_SvQ0MU.d.cts → index-B9V5SSxl.d.mts} +2 -2
- package/dist/{index-B_SvQ0MU.d.cts.map → index-B9V5SSxl.d.mts.map} +1 -1
- package/dist/{index-DIWuZlxd.d.mts → index-BFSHumky.d.mts} +2 -2
- package/dist/{index-DIWuZlxd.d.mts.map → index-BFSHumky.d.mts.map} +1 -1
- package/dist/{index-BQ5luyME.d.cts → index-D7i4bgl3.d.mts} +2747 -828
- package/dist/index-D7i4bgl3.d.mts.map +1 -0
- package/dist/{index-qIN6ULCb.d.cts → index-DFbu_-zn.d.cts} +2 -2
- package/dist/{index-qIN6ULCb.d.cts.map → index-DFbu_-zn.d.cts.map} +1 -1
- package/dist/{index-BtnCNLsH.d.mts → index-fF-j_WhY.d.cts} +2747 -828
- package/dist/index-fF-j_WhY.d.cts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +1 -1
- package/dist/{stream-limiter-D2Y8Z_Kv.mjs → stream-limiter-B9nsn2gb.mjs} +2 -2
- package/dist/{stream-limiter-D2Y8Z_Kv.mjs.map → stream-limiter-B9nsn2gb.mjs.map} +1 -1
- package/dist/{stream-limiter-By0fxkAh.cjs → stream-limiter-DyWOdil4.cjs} +1 -1
- package/dist/streams/index.cjs +1 -1
- package/dist/streams/index.d.cts +2 -2
- package/dist/streams/index.d.mts +2 -2
- package/dist/streams/index.mjs +1 -1
- package/dist/testing/index.cjs +1 -1
- package/dist/testing/index.d.cts +4 -4
- package/dist/testing/index.d.mts +4 -4
- package/dist/testing/index.mjs +1 -1
- package/dist/types/index.cjs +1 -1
- package/dist/types/index.d.cts +5 -5
- package/dist/types/index.d.mts +5 -5
- package/dist/types/index.mjs +1 -1
- package/dist/types-CH0BgiJN.mjs +2 -0
- package/dist/types-CH0BgiJN.mjs.map +1 -0
- package/dist/types-DUYVoR13.cjs +1 -0
- package/dist/upload/index.cjs +1 -1
- package/dist/upload/index.d.cts +4 -4
- package/dist/upload/index.d.mts +4 -4
- package/dist/upload/index.mjs +1 -1
- package/dist/{upload-bBgM3QFI.cjs → upload-CFT-dWPB.cjs} +1 -1
- package/dist/{upload-Bq9h95w6.mjs → upload-ggK-0ZBM.mjs} +2 -2
- package/dist/{upload-Bq9h95w6.mjs.map → upload-ggK-0ZBM.mjs.map} +1 -1
- package/dist/{uploadista-error-DCRIscEv.cjs → uploadista-error-BxBLmQtX.cjs} +4 -1
- package/dist/{uploadista-error-Bb-qIIKM.d.cts → uploadista-error-CYCmAtkZ.d.cts} +2 -2
- package/dist/uploadista-error-CYCmAtkZ.d.cts.map +1 -0
- package/dist/{uploadista-error-djFxVTLh.mjs → uploadista-error-CkSxSyNo.mjs} +4 -1
- package/dist/uploadista-error-CkSxSyNo.mjs.map +1 -0
- package/dist/{uploadista-error-D7Gubrr1.d.mts → uploadista-error-DR0XimpE.d.mts} +2 -2
- package/dist/uploadista-error-DR0XimpE.d.mts.map +1 -0
- package/dist/utils/index.cjs +1 -1
- package/dist/utils/index.d.cts +2 -2
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +1 -1
- package/dist/{utils-MQUZyB9S.mjs → utils-B-ZhQ6b0.mjs} +2 -2
- package/dist/{utils-MQUZyB9S.mjs.map → utils-B-ZhQ6b0.mjs.map} +1 -1
- package/dist/{utils-DxLVhlLd.cjs → utils-Dhq3vPqp.cjs} +1 -1
- package/docs/CIRCUIT_BREAKER.md +381 -0
- package/docs/DEAD-LETTER-QUEUE.md +374 -0
- package/package.json +11 -6
- package/src/errors/uploadista-error.ts +16 -1
- package/src/flow/README.md +102 -0
- package/src/flow/circuit-breaker-store.ts +382 -0
- package/src/flow/circuit-breaker.ts +99 -0
- package/src/flow/dead-letter-queue.ts +573 -0
- package/src/flow/distributed-circuit-breaker.ts +437 -0
- package/src/flow/event.ts +105 -1
- package/src/flow/flow-server.ts +70 -0
- package/src/flow/flow.ts +141 -3
- package/src/flow/index.ts +14 -2
- package/src/flow/input-type-registry.ts +229 -0
- package/src/flow/node-types/index.ts +26 -20
- package/src/flow/node.ts +48 -26
- package/src/flow/nodes/input-node.ts +4 -2
- package/src/flow/nodes/transform-node.ts +64 -6
- package/src/flow/output-type-registry.ts +231 -0
- package/src/flow/type-guards.ts +38 -22
- package/src/flow/typed-flow.ts +26 -0
- package/src/flow/types/dead-letter-item.ts +258 -0
- package/src/flow/types/flow-types.ts +320 -2
- package/src/flow/types/retry-policy.ts +260 -0
- package/src/flow/utils/file-naming.ts +308 -0
- package/src/types/circuit-breaker-store.ts +222 -0
- package/src/types/health-check.ts +204 -0
- package/src/types/index.ts +2 -0
- package/src/types/kv-store.ts +82 -2
- package/tests/flow/dead-letter-item.test.ts +283 -0
- package/tests/flow/dead-letter-queue.test.ts +613 -0
- package/tests/flow/file-naming.test.ts +390 -0
- package/tests/flow/retry-policy.test.ts +284 -0
- package/tests/flow/type-registry.test.ts +1 -1
- package/tests/flow/type-system.test.ts +17 -14
- package/dist/flow-BiUCrFTv.cjs +0 -1
- package/dist/flow-vXXjtBBv.mjs +0 -2
- package/dist/flow-vXXjtBBv.mjs.map +0 -1
- package/dist/index-BQ5luyME.d.cts.map +0 -1
- package/dist/index-BtnCNLsH.d.mts.map +0 -1
- package/dist/types-B5I4BioZ.cjs +0 -1
- package/dist/types-f6w5J3UD.mjs +0 -2
- package/dist/types-f6w5J3UD.mjs.map +0 -1
- package/dist/uploadista-error-Bb-qIIKM.d.cts.map +0 -1
- package/dist/uploadista-error-D7Gubrr1.d.mts.map +0 -1
- package/dist/uploadista-error-djFxVTLh.mjs.map +0 -1
- package/src/flow/type-registry.ts +0 -379
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# Dead Letter Queue (DLQ) Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Dead Letter Queue (DLQ) provides automatic capture and retry capabilities for failed flow jobs. When a flow execution fails, the DLQ preserves the complete failure context including inputs, partial results, and error details for debugging, automatic retry, or manual intervention.
|
|
6
|
+
|
|
7
|
+
## Key Features
|
|
8
|
+
|
|
9
|
+
- **Automatic Failure Capture**: Failed flow jobs are automatically captured with full execution context
|
|
10
|
+
- **Configurable Retry Policies**: Support for immediate, fixed delay, and exponential backoff strategies
|
|
11
|
+
- **Error Filtering**: Configure which errors should be retried vs. non-retryable
|
|
12
|
+
- **Admin API**: RESTful endpoints for DLQ management (list, retry, resolve, cleanup)
|
|
13
|
+
- **Observability**: Event-based metrics and tracing for monitoring
|
|
14
|
+
- **TTL-based Cleanup**: Automatic expiration of old DLQ items
|
|
15
|
+
|
|
16
|
+
## Architecture
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
20
|
+
│ Flow Execution │
|
|
21
|
+
└──────────────────────────────────┬──────────────────────────────────────────┘
|
|
22
|
+
│
|
|
23
|
+
┌────▼────┐
|
|
24
|
+
│ Fail? │
|
|
25
|
+
└────┬────┘
|
|
26
|
+
Yes │
|
|
27
|
+
┌──────────────▼──────────────────────┐
|
|
28
|
+
│ DeadLetterQueueService │
|
|
29
|
+
│ ┌────────────────────────────────┐ │
|
|
30
|
+
│ │ DeadLetterItem │ │
|
|
31
|
+
│ │ - jobId, flowId, storageId │ │
|
|
32
|
+
│ │ - error details, inputs │ │
|
|
33
|
+
│ │ - nodeResults, retryHistory │ │
|
|
34
|
+
│ └────────────────────────────────┘ │
|
|
35
|
+
└──────────────────┬──────────────────┘
|
|
36
|
+
│
|
|
37
|
+
┌──────────────────▼──────────────────┐
|
|
38
|
+
│ DeadLetterQueueKVStore │
|
|
39
|
+
│ (Persistent Storage) │
|
|
40
|
+
└─────────────────────────────────────┘
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
### 1. Enable DLQ in Flow Configuration
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { createFlowWithSchema, type FlowDeadLetterQueueConfig } from "@uploadista/core";
|
|
49
|
+
|
|
50
|
+
const flowConfig = {
|
|
51
|
+
flowId: "image-pipeline",
|
|
52
|
+
name: "Image Processing Pipeline",
|
|
53
|
+
nodes: [...],
|
|
54
|
+
edges: [...],
|
|
55
|
+
inputSchema: imageInputSchema,
|
|
56
|
+
outputSchema: imageOutputSchema,
|
|
57
|
+
// Enable DLQ with custom retry policy
|
|
58
|
+
deadLetterQueue: {
|
|
59
|
+
enabled: true,
|
|
60
|
+
retryPolicy: {
|
|
61
|
+
enabled: true,
|
|
62
|
+
maxRetries: 5,
|
|
63
|
+
backoff: {
|
|
64
|
+
type: "exponential",
|
|
65
|
+
initialDelayMs: 1000,
|
|
66
|
+
maxDelayMs: 300000, // 5 minutes
|
|
67
|
+
multiplier: 2,
|
|
68
|
+
jitter: true
|
|
69
|
+
},
|
|
70
|
+
nonRetryableErrors: ["VALIDATION_ERROR", "AUTH_ERROR"],
|
|
71
|
+
ttlMs: 604800000 // 7 days
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 2. Provide DLQ Service Layer
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import {
|
|
81
|
+
DeadLetterQueueService,
|
|
82
|
+
deadLetterQueueService,
|
|
83
|
+
deadLetterQueueKvStore,
|
|
84
|
+
BaseKvStoreService
|
|
85
|
+
} from "@uploadista/core";
|
|
86
|
+
|
|
87
|
+
// Provide the DLQ service in your Effect layer stack
|
|
88
|
+
const program = myFlowProgram.pipe(
|
|
89
|
+
Effect.provide(deadLetterQueueService),
|
|
90
|
+
Effect.provide(deadLetterQueueKvStore),
|
|
91
|
+
Effect.provide(baseKvStoreLayer)
|
|
92
|
+
);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 3. Access DLQ in Admin Handlers
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { DeadLetterQueueService } from "@uploadista/core";
|
|
99
|
+
|
|
100
|
+
const adminHandler = Effect.gen(function* () {
|
|
101
|
+
const dlq = yield* DeadLetterQueueService;
|
|
102
|
+
|
|
103
|
+
// Get DLQ statistics
|
|
104
|
+
const stats = yield* dlq.getStats();
|
|
105
|
+
console.log(`Total DLQ items: ${stats.totalItems}`);
|
|
106
|
+
|
|
107
|
+
// List pending items
|
|
108
|
+
const { items, total } = yield* dlq.list({ status: "pending" });
|
|
109
|
+
|
|
110
|
+
// Manual retry
|
|
111
|
+
const item = yield* dlq.get(itemId);
|
|
112
|
+
yield* dlq.markRetrying(item.id);
|
|
113
|
+
// ... re-execute flow with item.inputs ...
|
|
114
|
+
yield* dlq.markResolved(item.id);
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Retry Policies
|
|
119
|
+
|
|
120
|
+
### Backoff Strategies
|
|
121
|
+
|
|
122
|
+
#### Immediate
|
|
123
|
+
Retry immediately without delay. Use for transient errors that may succeed on immediate retry.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const immediatePolicy = {
|
|
127
|
+
enabled: true,
|
|
128
|
+
maxRetries: 3,
|
|
129
|
+
backoff: { type: "immediate" }
|
|
130
|
+
};
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### Fixed Delay
|
|
134
|
+
Wait a fixed duration between retries.
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const fixedPolicy = {
|
|
138
|
+
enabled: true,
|
|
139
|
+
maxRetries: 5,
|
|
140
|
+
backoff: {
|
|
141
|
+
type: "fixed",
|
|
142
|
+
delayMs: 5000 // 5 seconds between retries
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### Exponential Backoff
|
|
148
|
+
Progressively longer delays with optional jitter.
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const exponentialPolicy = {
|
|
152
|
+
enabled: true,
|
|
153
|
+
maxRetries: 5,
|
|
154
|
+
backoff: {
|
|
155
|
+
type: "exponential",
|
|
156
|
+
initialDelayMs: 1000, // Start with 1 second
|
|
157
|
+
maxDelayMs: 300000, // Cap at 5 minutes
|
|
158
|
+
multiplier: 2, // Double each time
|
|
159
|
+
jitter: true // Add randomness to prevent thundering herd
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
// Delays: ~1s, ~2s, ~4s, ~8s, ~16s, ... capped at 5min
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Error Filtering
|
|
166
|
+
|
|
167
|
+
Control which errors trigger retries:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const filteredPolicy = {
|
|
171
|
+
enabled: true,
|
|
172
|
+
maxRetries: 3,
|
|
173
|
+
backoff: { type: "exponential", ... },
|
|
174
|
+
// Only retry these errors
|
|
175
|
+
retryableErrors: ["NETWORK_ERROR", "TIMEOUT_ERROR"],
|
|
176
|
+
// Never retry these (takes precedence)
|
|
177
|
+
nonRetryableErrors: ["VALIDATION_ERROR", "AUTH_ERROR", "PERMISSION_DENIED"]
|
|
178
|
+
};
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## DLQ Item Lifecycle
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
┌─────────┐ Add ┌─────────┐ Retry ┌──────────┐
|
|
185
|
+
│ Flow │ ─────────▶ │ pending │ ────────▶ │ retrying │
|
|
186
|
+
│ Failure │ └────┬────┘ └────┬─────┘
|
|
187
|
+
└─────────┘ │ │
|
|
188
|
+
│ ┌─────┴─────┐
|
|
189
|
+
│ Success Failure
|
|
190
|
+
│ │ │
|
|
191
|
+
Max Retries ┌───▼────┐ ┌───▼────┐
|
|
192
|
+
Reached │resolved│ │pending │
|
|
193
|
+
│ └────────┘ └────────┘
|
|
194
|
+
┌───▼─────┐ │
|
|
195
|
+
│exhausted│◀────────────────────┘
|
|
196
|
+
└─────────┘ Max retries
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Status Meanings
|
|
200
|
+
|
|
201
|
+
- **pending**: Awaiting retry (scheduled or manual)
|
|
202
|
+
- **retrying**: Currently being retried
|
|
203
|
+
- **exhausted**: Max retries reached, requires manual intervention
|
|
204
|
+
- **resolved**: Successfully retried or manually resolved
|
|
205
|
+
|
|
206
|
+
## Admin API Endpoints
|
|
207
|
+
|
|
208
|
+
The DLQ provides RESTful admin endpoints for management:
|
|
209
|
+
|
|
210
|
+
### List DLQ Items
|
|
211
|
+
```
|
|
212
|
+
GET /api/admin/dlq
|
|
213
|
+
Query params: status, flowId, clientId, limit, offset
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Get Single Item
|
|
217
|
+
```
|
|
218
|
+
GET /api/admin/dlq/:itemId
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Retry Single Item
|
|
222
|
+
```
|
|
223
|
+
POST /api/admin/dlq/:itemId/retry
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Retry All Items
|
|
227
|
+
```
|
|
228
|
+
POST /api/admin/dlq/retry-all
|
|
229
|
+
Body: { status?: "pending", flowId?: string }
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Delete Item
|
|
233
|
+
```
|
|
234
|
+
DELETE /api/admin/dlq/:itemId
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Mark as Resolved
|
|
238
|
+
```
|
|
239
|
+
POST /api/admin/dlq/:itemId/resolve
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Cleanup Old Items
|
|
243
|
+
```
|
|
244
|
+
POST /api/admin/dlq/cleanup
|
|
245
|
+
Body: { olderThan?: Date, status?: "exhausted" | "resolved" }
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Get Statistics
|
|
249
|
+
```
|
|
250
|
+
GET /api/admin/dlq/stats
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Observability Events
|
|
254
|
+
|
|
255
|
+
The DLQ emits events for monitoring and alerting:
|
|
256
|
+
|
|
257
|
+
| Event | Description |
|
|
258
|
+
|-------|-------------|
|
|
259
|
+
| `dlq-item-added` | Job added to DLQ |
|
|
260
|
+
| `dlq-retry-start` | Retry attempt started |
|
|
261
|
+
| `dlq-retry-success` | Retry succeeded |
|
|
262
|
+
| `dlq-retry-failed` | Retry failed |
|
|
263
|
+
| `dlq-item-exhausted` | Max retries reached |
|
|
264
|
+
| `dlq-item-resolved` | Item marked resolved |
|
|
265
|
+
|
|
266
|
+
### Example Event Handler
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
const eventHandler = (event: FlowEvent) => {
|
|
270
|
+
switch (event.eventType) {
|
|
271
|
+
case EventType.DlqItemAdded:
|
|
272
|
+
metrics.increment("dlq.items.added");
|
|
273
|
+
alerting.notify(`Job ${event.jobId} added to DLQ: ${event.errorMessage}`);
|
|
274
|
+
break;
|
|
275
|
+
case EventType.DlqItemExhausted:
|
|
276
|
+
metrics.increment("dlq.items.exhausted");
|
|
277
|
+
alerting.critical(`Job ${event.jobId} exhausted all retries`);
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Best Practices
|
|
284
|
+
|
|
285
|
+
### 1. Configure Appropriate Retry Limits
|
|
286
|
+
- Use fewer retries (2-3) for validation errors
|
|
287
|
+
- Use more retries (5-10) for external service calls
|
|
288
|
+
- Consider the total retry duration vs. business requirements
|
|
289
|
+
|
|
290
|
+
### 2. Filter Non-Retryable Errors
|
|
291
|
+
Always configure `nonRetryableErrors` to skip permanent failures:
|
|
292
|
+
- `VALIDATION_ERROR` - Invalid input data
|
|
293
|
+
- `AUTH_ERROR` - Authentication failures
|
|
294
|
+
- `PERMISSION_DENIED` - Authorization failures
|
|
295
|
+
- `NOT_FOUND` - Missing resources
|
|
296
|
+
|
|
297
|
+
### 3. Set Appropriate TTL
|
|
298
|
+
- Short TTL (1-2 days) for time-sensitive flows
|
|
299
|
+
- Longer TTL (7-30 days) for debugging needs
|
|
300
|
+
- Consider storage costs for high-volume systems
|
|
301
|
+
|
|
302
|
+
### 4. Monitor DLQ Growth
|
|
303
|
+
Set up alerts for:
|
|
304
|
+
- DLQ size exceeding threshold
|
|
305
|
+
- High rate of exhausted items
|
|
306
|
+
- Specific error codes appearing frequently
|
|
307
|
+
|
|
308
|
+
### 5. Regular Cleanup
|
|
309
|
+
Schedule periodic cleanup of resolved and exhausted items:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
// Daily cleanup of items older than 7 days
|
|
313
|
+
const dailyCleanup = Effect.gen(function* () {
|
|
314
|
+
const dlq = yield* DeadLetterQueueService;
|
|
315
|
+
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
|
316
|
+
const result = yield* dlq.cleanup({ olderThan: weekAgo });
|
|
317
|
+
console.log(`Cleaned up ${result.deleted} DLQ items`);
|
|
318
|
+
});
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## TypeScript Types
|
|
322
|
+
|
|
323
|
+
### DeadLetterItem
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
interface DeadLetterItem {
|
|
327
|
+
id: string;
|
|
328
|
+
jobId: string;
|
|
329
|
+
flowId: string;
|
|
330
|
+
storageId: string;
|
|
331
|
+
clientId: string | null;
|
|
332
|
+
error: DeadLetterError;
|
|
333
|
+
inputs: Record<string, unknown>;
|
|
334
|
+
nodeResults: Record<string, unknown>;
|
|
335
|
+
failedAtNodeId?: string;
|
|
336
|
+
retryCount: number;
|
|
337
|
+
maxRetries: number;
|
|
338
|
+
nextRetryAt?: Date;
|
|
339
|
+
retryHistory: DeadLetterRetryAttempt[];
|
|
340
|
+
createdAt: Date;
|
|
341
|
+
updatedAt: Date;
|
|
342
|
+
expiresAt?: Date;
|
|
343
|
+
status: DeadLetterItemStatus;
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### RetryPolicy
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
interface RetryPolicy {
|
|
351
|
+
enabled: boolean;
|
|
352
|
+
maxRetries: number;
|
|
353
|
+
backoff: BackoffStrategy;
|
|
354
|
+
retryableErrors?: string[];
|
|
355
|
+
nonRetryableErrors?: string[];
|
|
356
|
+
ttlMs?: number;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
type BackoffStrategy =
|
|
360
|
+
| { type: "immediate" }
|
|
361
|
+
| { type: "fixed"; delayMs: number }
|
|
362
|
+
| { type: "exponential"; initialDelayMs: number; maxDelayMs: number; multiplier: number; jitter: boolean };
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## Migration Guide
|
|
366
|
+
|
|
367
|
+
### From v0.x (No DLQ) to v1.x (With DLQ)
|
|
368
|
+
|
|
369
|
+
1. Add DLQ KV store to your base store configuration
|
|
370
|
+
2. Provide the `deadLetterQueueService` layer
|
|
371
|
+
3. Optionally configure flow-level retry policies
|
|
372
|
+
4. Implement admin UI or CLI for DLQ management
|
|
373
|
+
|
|
374
|
+
The DLQ integration is fully optional and backward compatible. Flows without explicit DLQ configuration will work as before.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uploadista/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18-beta.10",
|
|
4
4
|
"description": "Core package of Uploadista",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Uploadista",
|
|
@@ -62,16 +62,21 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"
|
|
66
|
-
|
|
65
|
+
"micromustache": "^8.0.3"
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"effect": "^3.0.0",
|
|
69
|
+
"zod": "^4.0.0"
|
|
67
70
|
},
|
|
68
71
|
"devDependencies": {
|
|
69
72
|
"@effect/vitest": "0.27.0",
|
|
70
73
|
"@types/node": "24.10.1",
|
|
74
|
+
"effect": "3.19.8",
|
|
71
75
|
"tsd": "0.33.0",
|
|
72
|
-
"tsdown": "0.16.
|
|
73
|
-
"vitest": "4.0.
|
|
74
|
-
"
|
|
76
|
+
"tsdown": "0.16.8",
|
|
77
|
+
"vitest": "4.0.14",
|
|
78
|
+
"zod": "4.1.13",
|
|
79
|
+
"@uploadista/typescript-config": "0.0.18-beta.10"
|
|
75
80
|
},
|
|
76
81
|
"publishConfig": {
|
|
77
82
|
"access": "public",
|
|
@@ -48,6 +48,8 @@ export type UploadistaErrorCode =
|
|
|
48
48
|
| "FFMPEG_NOT_INSTALLED"
|
|
49
49
|
| "INVALID_NODE_TYPE"
|
|
50
50
|
| "TYPE_CATEGORY_MISMATCH"
|
|
51
|
+
| "INVALID_INPUT_TYPE"
|
|
52
|
+
| "INVALID_OUTPUT_TYPE"
|
|
51
53
|
| "OUTPUT_NOT_FOUND"
|
|
52
54
|
| "MULTIPLE_OUTPUTS_FOUND"
|
|
53
55
|
| "VIRUS_SCAN_FAILED"
|
|
@@ -60,7 +62,8 @@ export type UploadistaErrorCode =
|
|
|
60
62
|
| "OCR_FAILED"
|
|
61
63
|
| "PDF_ENCRYPTED"
|
|
62
64
|
| "PDF_CORRUPTED"
|
|
63
|
-
| "PAGE_RANGE_INVALID"
|
|
65
|
+
| "PAGE_RANGE_INVALID"
|
|
66
|
+
| "CIRCUIT_BREAKER_OPEN";
|
|
64
67
|
|
|
65
68
|
/**
|
|
66
69
|
* Catalog of all predefined errors in the Uploadista system.
|
|
@@ -236,6 +239,14 @@ export const ERROR_CATALOG: Readonly<
|
|
|
236
239
|
status: 500,
|
|
237
240
|
body: "Node type category does not match the node configuration\n",
|
|
238
241
|
},
|
|
242
|
+
INVALID_INPUT_TYPE: {
|
|
243
|
+
status: 500,
|
|
244
|
+
body: "The input type is not registered\n",
|
|
245
|
+
},
|
|
246
|
+
INVALID_OUTPUT_TYPE: {
|
|
247
|
+
status: 500,
|
|
248
|
+
body: "The output type is not registered\n",
|
|
249
|
+
},
|
|
239
250
|
OUTPUT_NOT_FOUND: {
|
|
240
251
|
status: 404,
|
|
241
252
|
body: "No output of the specified type was found\n",
|
|
@@ -288,6 +299,10 @@ export const ERROR_CATALOG: Readonly<
|
|
|
288
299
|
status: 400,
|
|
289
300
|
body: "The specified page range is invalid\n",
|
|
290
301
|
},
|
|
302
|
+
CIRCUIT_BREAKER_OPEN: {
|
|
303
|
+
status: 503,
|
|
304
|
+
body: "Circuit breaker is open - service temporarily unavailable\n",
|
|
305
|
+
},
|
|
291
306
|
} as const;
|
|
292
307
|
|
|
293
308
|
/**
|
package/src/flow/README.md
CHANGED
|
@@ -437,6 +437,108 @@ const conditionalNode: FlowNode<MyData, MyData> = {
|
|
|
437
437
|
};
|
|
438
438
|
```
|
|
439
439
|
|
|
440
|
+
### 6. File Naming for Transform Nodes
|
|
441
|
+
|
|
442
|
+
Transform nodes (nodes that produce new files) support automatic file naming to avoid confusion in multi-output flows. When processing multiple files through a pipeline, automatic naming helps distinguish between original and processed versions.
|
|
443
|
+
|
|
444
|
+
#### Naming Modes
|
|
445
|
+
|
|
446
|
+
The flow engine supports three naming modes:
|
|
447
|
+
|
|
448
|
+
- **None**: Keep the original filename unchanged
|
|
449
|
+
- **Auto** (default): Automatically add a suffix based on the operation (e.g., `photo.jpg` → `photo-800x600.jpg`)
|
|
450
|
+
- **Custom**: Use a template pattern or custom function
|
|
451
|
+
|
|
452
|
+
#### Auto Naming
|
|
453
|
+
|
|
454
|
+
When enabled, each transform node type adds a relevant suffix:
|
|
455
|
+
|
|
456
|
+
| Node | Auto Suffix | Example |
|
|
457
|
+
|------|-------------|---------|
|
|
458
|
+
| resize | `${width}x${height}` | `photo-800x600.jpg` |
|
|
459
|
+
| optimize | `${format}` | `photo-webp.webp` |
|
|
460
|
+
| transform-image | `transformed` | `photo-transformed.jpg` |
|
|
461
|
+
| remove-background | `nobg` | `photo-nobg.png` |
|
|
462
|
+
| resize-video | `${width}x${height}` | `video-720p.mp4` |
|
|
463
|
+
| transcode | `${format}` | `video-mp4.mp4` |
|
|
464
|
+
| trim | `trimmed` | `video-trimmed.mp4` |
|
|
465
|
+
| thumbnail | `thumb` | `video-thumb.jpg` |
|
|
466
|
+
| split-pdf | `page-${pageNumber}` | `doc-page-1.pdf` |
|
|
467
|
+
| merge-pdf | `merged` | `docs-merged.pdf` |
|
|
468
|
+
|
|
469
|
+
#### Usage Example
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
import { createResizeNode } from "@uploadista/flow-image-nodes";
|
|
473
|
+
|
|
474
|
+
// Default: Auto naming enabled
|
|
475
|
+
const resizeNode = yield* createResizeNode("resize", {
|
|
476
|
+
width: 800,
|
|
477
|
+
height: 600,
|
|
478
|
+
}, {
|
|
479
|
+
naming: { mode: "auto" }, // Output: "photo-800x600.jpg"
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// Custom naming with template
|
|
483
|
+
const resizeNodeCustom = yield* createResizeNode("resize", {
|
|
484
|
+
width: 800,
|
|
485
|
+
height: 600,
|
|
486
|
+
}, {
|
|
487
|
+
naming: {
|
|
488
|
+
mode: "custom",
|
|
489
|
+
pattern: "{{baseName}}-{{nodeType}}-{{width}}w.{{extension}}",
|
|
490
|
+
}, // Output: "photo-resize-800w.jpg"
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// Custom naming with function
|
|
494
|
+
const resizeNodeFn = yield* createResizeNode("resize", {
|
|
495
|
+
width: 800,
|
|
496
|
+
height: 600,
|
|
497
|
+
}, {
|
|
498
|
+
naming: {
|
|
499
|
+
mode: "custom",
|
|
500
|
+
rename: (file, ctx) => `processed-${ctx.baseName}.${ctx.extension}`,
|
|
501
|
+
}, // Output: "processed-photo.jpg"
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Disable naming
|
|
505
|
+
const resizeNodeNone = yield* createResizeNode("resize", {
|
|
506
|
+
width: 800,
|
|
507
|
+
height: 600,
|
|
508
|
+
}, {
|
|
509
|
+
naming: { mode: "none" }, // Output: "photo.jpg" (unchanged)
|
|
510
|
+
});
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
#### Available Template Variables
|
|
514
|
+
|
|
515
|
+
| Variable | Description | Example |
|
|
516
|
+
|----------|-------------|---------|
|
|
517
|
+
| `{{baseName}}` | Filename without extension | `photo` |
|
|
518
|
+
| `{{extension}}` | File extension | `jpg` |
|
|
519
|
+
| `{{fileName}}` | Full filename | `photo.jpg` |
|
|
520
|
+
| `{{nodeType}}` | Type of processing node | `resize` |
|
|
521
|
+
| `{{nodeId}}` | Node identifier | `resize-1` |
|
|
522
|
+
| `{{flowId}}` | Flow identifier | `flow-abc` |
|
|
523
|
+
| `{{jobId}}` | Job identifier | `job-123` |
|
|
524
|
+
| `{{timestamp}}` | Processing timestamp | `2024-01-15T10:30:00Z` |
|
|
525
|
+
| `{{width}}` | Output width (when applicable) | `800` |
|
|
526
|
+
| `{{height}}` | Output height (when applicable) | `600` |
|
|
527
|
+
| `{{format}}` | Output format (when applicable) | `webp` |
|
|
528
|
+
| `{{quality}}` | Quality setting (when applicable) | `80` |
|
|
529
|
+
| `{{pageNumber}}` | Page number (for PDF split) | `1` |
|
|
530
|
+
|
|
531
|
+
#### Metadata-Only Nodes
|
|
532
|
+
|
|
533
|
+
Nodes that only extract metadata (don't transform file bytes) don't support file naming:
|
|
534
|
+
- `describe-image-node` - AI image description
|
|
535
|
+
- `describe-document-node` - PDF metadata extraction
|
|
536
|
+
- `describe-video-node` - Video metadata extraction
|
|
537
|
+
- `extract-text-node` - PDF text extraction
|
|
538
|
+
- `ocr-node` - OCR text extraction
|
|
539
|
+
- `convert-to-markdown-node` - Markdown extraction to metadata
|
|
540
|
+
- `scan-virus-node` - Virus scanning
|
|
541
|
+
|
|
440
542
|
## Best Practices
|
|
441
543
|
|
|
442
544
|
1. **Use descriptive node names**: Make your nodes easy to understand and debug
|