@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.
Files changed (117) hide show
  1. package/README.md +102 -0
  2. package/dist/{checksum-DaCqP8Qa.mjs → checksum-COoD-F1l.mjs} +2 -2
  3. package/dist/{checksum-DaCqP8Qa.mjs.map → checksum-COoD-F1l.mjs.map} +1 -1
  4. package/dist/{checksum-BIlVW8bD.cjs → checksum-YLW4hVY7.cjs} +1 -1
  5. package/dist/errors/index.cjs +1 -1
  6. package/dist/errors/index.d.cts +1 -1
  7. package/dist/errors/index.d.mts +1 -1
  8. package/dist/errors/index.mjs +1 -1
  9. package/dist/flow/index.cjs +1 -1
  10. package/dist/flow/index.d.cts +5 -5
  11. package/dist/flow/index.d.mts +5 -5
  12. package/dist/flow/index.mjs +1 -1
  13. package/dist/flow-BLGpxdEm.mjs +2 -0
  14. package/dist/flow-BLGpxdEm.mjs.map +1 -0
  15. package/dist/flow-DaBzRGmY.cjs +1 -0
  16. package/dist/{index-BGi1r_fi.d.mts → index-9gyMMEIB.d.cts} +2 -2
  17. package/dist/{index-BGi1r_fi.d.mts.map → index-9gyMMEIB.d.cts.map} +1 -1
  18. package/dist/{index-B_SvQ0MU.d.cts → index-B9V5SSxl.d.mts} +2 -2
  19. package/dist/{index-B_SvQ0MU.d.cts.map → index-B9V5SSxl.d.mts.map} +1 -1
  20. package/dist/{index-DIWuZlxd.d.mts → index-BFSHumky.d.mts} +2 -2
  21. package/dist/{index-DIWuZlxd.d.mts.map → index-BFSHumky.d.mts.map} +1 -1
  22. package/dist/{index-BQ5luyME.d.cts → index-D7i4bgl3.d.mts} +2747 -828
  23. package/dist/index-D7i4bgl3.d.mts.map +1 -0
  24. package/dist/{index-qIN6ULCb.d.cts → index-DFbu_-zn.d.cts} +2 -2
  25. package/dist/{index-qIN6ULCb.d.cts.map → index-DFbu_-zn.d.cts.map} +1 -1
  26. package/dist/{index-BtnCNLsH.d.mts → index-fF-j_WhY.d.cts} +2747 -828
  27. package/dist/index-fF-j_WhY.d.cts.map +1 -0
  28. package/dist/index.cjs +1 -1
  29. package/dist/index.d.cts +5 -5
  30. package/dist/index.d.mts +5 -5
  31. package/dist/index.mjs +1 -1
  32. package/dist/{stream-limiter-D2Y8Z_Kv.mjs → stream-limiter-B9nsn2gb.mjs} +2 -2
  33. package/dist/{stream-limiter-D2Y8Z_Kv.mjs.map → stream-limiter-B9nsn2gb.mjs.map} +1 -1
  34. package/dist/{stream-limiter-By0fxkAh.cjs → stream-limiter-DyWOdil4.cjs} +1 -1
  35. package/dist/streams/index.cjs +1 -1
  36. package/dist/streams/index.d.cts +2 -2
  37. package/dist/streams/index.d.mts +2 -2
  38. package/dist/streams/index.mjs +1 -1
  39. package/dist/testing/index.cjs +1 -1
  40. package/dist/testing/index.d.cts +4 -4
  41. package/dist/testing/index.d.mts +4 -4
  42. package/dist/testing/index.mjs +1 -1
  43. package/dist/types/index.cjs +1 -1
  44. package/dist/types/index.d.cts +5 -5
  45. package/dist/types/index.d.mts +5 -5
  46. package/dist/types/index.mjs +1 -1
  47. package/dist/types-CH0BgiJN.mjs +2 -0
  48. package/dist/types-CH0BgiJN.mjs.map +1 -0
  49. package/dist/types-DUYVoR13.cjs +1 -0
  50. package/dist/upload/index.cjs +1 -1
  51. package/dist/upload/index.d.cts +4 -4
  52. package/dist/upload/index.d.mts +4 -4
  53. package/dist/upload/index.mjs +1 -1
  54. package/dist/{upload-bBgM3QFI.cjs → upload-CFT-dWPB.cjs} +1 -1
  55. package/dist/{upload-Bq9h95w6.mjs → upload-ggK-0ZBM.mjs} +2 -2
  56. package/dist/{upload-Bq9h95w6.mjs.map → upload-ggK-0ZBM.mjs.map} +1 -1
  57. package/dist/{uploadista-error-DCRIscEv.cjs → uploadista-error-BxBLmQtX.cjs} +4 -1
  58. package/dist/{uploadista-error-Bb-qIIKM.d.cts → uploadista-error-CYCmAtkZ.d.cts} +2 -2
  59. package/dist/uploadista-error-CYCmAtkZ.d.cts.map +1 -0
  60. package/dist/{uploadista-error-djFxVTLh.mjs → uploadista-error-CkSxSyNo.mjs} +4 -1
  61. package/dist/uploadista-error-CkSxSyNo.mjs.map +1 -0
  62. package/dist/{uploadista-error-D7Gubrr1.d.mts → uploadista-error-DR0XimpE.d.mts} +2 -2
  63. package/dist/uploadista-error-DR0XimpE.d.mts.map +1 -0
  64. package/dist/utils/index.cjs +1 -1
  65. package/dist/utils/index.d.cts +2 -2
  66. package/dist/utils/index.d.mts +2 -2
  67. package/dist/utils/index.mjs +1 -1
  68. package/dist/{utils-MQUZyB9S.mjs → utils-B-ZhQ6b0.mjs} +2 -2
  69. package/dist/{utils-MQUZyB9S.mjs.map → utils-B-ZhQ6b0.mjs.map} +1 -1
  70. package/dist/{utils-DxLVhlLd.cjs → utils-Dhq3vPqp.cjs} +1 -1
  71. package/docs/CIRCUIT_BREAKER.md +381 -0
  72. package/docs/DEAD-LETTER-QUEUE.md +374 -0
  73. package/package.json +11 -6
  74. package/src/errors/uploadista-error.ts +16 -1
  75. package/src/flow/README.md +102 -0
  76. package/src/flow/circuit-breaker-store.ts +382 -0
  77. package/src/flow/circuit-breaker.ts +99 -0
  78. package/src/flow/dead-letter-queue.ts +573 -0
  79. package/src/flow/distributed-circuit-breaker.ts +437 -0
  80. package/src/flow/event.ts +105 -1
  81. package/src/flow/flow-server.ts +70 -0
  82. package/src/flow/flow.ts +141 -3
  83. package/src/flow/index.ts +14 -2
  84. package/src/flow/input-type-registry.ts +229 -0
  85. package/src/flow/node-types/index.ts +26 -20
  86. package/src/flow/node.ts +48 -26
  87. package/src/flow/nodes/input-node.ts +4 -2
  88. package/src/flow/nodes/transform-node.ts +64 -6
  89. package/src/flow/output-type-registry.ts +231 -0
  90. package/src/flow/type-guards.ts +38 -22
  91. package/src/flow/typed-flow.ts +26 -0
  92. package/src/flow/types/dead-letter-item.ts +258 -0
  93. package/src/flow/types/flow-types.ts +320 -2
  94. package/src/flow/types/retry-policy.ts +260 -0
  95. package/src/flow/utils/file-naming.ts +308 -0
  96. package/src/types/circuit-breaker-store.ts +222 -0
  97. package/src/types/health-check.ts +204 -0
  98. package/src/types/index.ts +2 -0
  99. package/src/types/kv-store.ts +82 -2
  100. package/tests/flow/dead-letter-item.test.ts +283 -0
  101. package/tests/flow/dead-letter-queue.test.ts +613 -0
  102. package/tests/flow/file-naming.test.ts +390 -0
  103. package/tests/flow/retry-policy.test.ts +284 -0
  104. package/tests/flow/type-registry.test.ts +1 -1
  105. package/tests/flow/type-system.test.ts +17 -14
  106. package/dist/flow-BiUCrFTv.cjs +0 -1
  107. package/dist/flow-vXXjtBBv.mjs +0 -2
  108. package/dist/flow-vXXjtBBv.mjs.map +0 -1
  109. package/dist/index-BQ5luyME.d.cts.map +0 -1
  110. package/dist/index-BtnCNLsH.d.mts.map +0 -1
  111. package/dist/types-B5I4BioZ.cjs +0 -1
  112. package/dist/types-f6w5J3UD.mjs +0 -2
  113. package/dist/types-f6w5J3UD.mjs.map +0 -1
  114. package/dist/uploadista-error-Bb-qIIKM.d.cts.map +0 -1
  115. package/dist/uploadista-error-D7Gubrr1.d.mts.map +0 -1
  116. package/dist/uploadista-error-djFxVTLh.mjs.map +0 -1
  117. 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.17",
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
- "effect": "3.19.6",
66
- "zod": "4.1.13"
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.6",
73
- "vitest": "4.0.13",
74
- "@uploadista/typescript-config": "0.0.17"
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
  /**
@@ -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