@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
package/src/flow/flow.ts CHANGED
@@ -16,14 +16,18 @@ import { Effect, Stream } from "effect";
16
16
  import { z } from "zod";
17
17
 
18
18
  import { UploadistaError } from "../errors";
19
+ import { CircuitBreakerStoreService } from "../types/circuit-breaker-store";
19
20
  import { UploadFileDataStores } from "../types/data-store";
20
21
  import type { UploadFile } from "../types/upload-file";
22
+ import type { CircuitBreakerConfig } from "./circuit-breaker";
23
+ import { DistributedCircuitBreakerRegistry } from "./distributed-circuit-breaker";
21
24
  import type { FlowEdge } from "./edge";
22
25
  import { EventType } from "./event";
23
26
  import { getNodeData } from "./node";
24
27
  import { ParallelScheduler } from "./parallel-scheduler";
25
28
  import { isUploadFile } from "./type-guards";
26
29
  import type {
30
+ FlowCircuitBreakerConfig,
27
31
  FlowConfig,
28
32
  FlowNode,
29
33
  FlowNodeData,
@@ -325,10 +329,44 @@ export function createFlowWithSchema<
325
329
  inputSchema,
326
330
  outputSchema,
327
331
  typeChecker,
332
+ circuitBreaker: circuitBreakerConfig,
328
333
  } = config;
329
334
  const nodes = resolvedNodes;
330
335
  const typeValidator = new FlowTypeValidator(typeChecker);
331
336
 
337
+ /**
338
+ * Gets the circuit breaker config for a specific node.
339
+ * Priority: node config > flow nodeTypeOverrides > flow defaults
340
+ */
341
+ const getCircuitBreakerConfigForNode = (
342
+ node: FlowNode<any, any, UploadistaError>,
343
+ ): CircuitBreakerConfig | undefined => {
344
+ // Get node-level config from the resolved node
345
+ const nodeConfig = node.circuitBreaker as
346
+ | FlowCircuitBreakerConfig
347
+ | undefined;
348
+
349
+ // Get flow-level config for this node type (using nodeTypeId for stable identification)
350
+ const flowNodeTypeConfig = node.nodeTypeId
351
+ ? circuitBreakerConfig?.nodeTypeOverrides?.[node.nodeTypeId]
352
+ : undefined;
353
+
354
+ // Get flow defaults
355
+ const flowDefaults = circuitBreakerConfig?.defaults;
356
+
357
+ // If nothing is configured, return undefined (circuit breaker disabled)
358
+ if (!nodeConfig && !flowNodeTypeConfig && !flowDefaults) {
359
+ return undefined;
360
+ }
361
+
362
+ // Merge configs with priority: node > nodeTypeOverrides > defaults
363
+ return {
364
+ ...flowDefaults,
365
+ ...flowNodeTypeConfig,
366
+ ...nodeConfig,
367
+ } as CircuitBreakerConfig;
368
+ };
369
+
332
370
  // Build adjacency list for topological sorting
333
371
  const buildGraph = () => {
334
372
  const graph: Record<string, string[]> = {};
@@ -502,13 +540,13 @@ export function createFlowWithSchema<
502
540
  outputNodes.forEach((node: any) => {
503
541
  const result = nodeResults.get(node.id);
504
542
  if (result !== undefined) {
505
- // Get the nodeType from the nodeTypes map
506
- const nodeTypeId = nodeTypesMap.get(node.id);
543
+ // Get the outputTypeId from the node types map (set from node execution results)
544
+ const outputTypeId = nodeTypesMap.get(node.id);
507
545
 
508
546
  // Create TypedOutput with metadata
509
547
  typedOutputs.push({
510
548
  nodeId: node.id,
511
- nodeType: nodeTypeId,
549
+ nodeType: outputTypeId,
512
550
  data: result,
513
551
  timestamp: new Date().toISOString(),
514
552
  });
@@ -581,6 +619,7 @@ export function createFlowWithSchema<
581
619
  nodeMap: Map<string, FlowNode<any, any, UploadistaError>>,
582
620
  jobId: string,
583
621
  clientId: string | null,
622
+ circuitBreakerRegistry: DistributedCircuitBreakerRegistry | null,
584
623
  ): Effect.Effect<
585
624
  {
586
625
  nodeId: string;
@@ -634,6 +673,85 @@ export function createFlowWithSchema<
634
673
  const baseDelay = node.retry?.retryDelay ?? 1000;
635
674
  const useExponentialBackoff = node.retry?.exponentialBackoff ?? true;
636
675
 
676
+ // Get circuit breaker configuration for this node
677
+ const cbConfig = getCircuitBreakerConfigForNode(node);
678
+ const circuitBreaker =
679
+ cbConfig?.enabled && node.nodeTypeId && circuitBreakerRegistry
680
+ ? circuitBreakerRegistry.getOrCreate(node.nodeTypeId, cbConfig)
681
+ : null;
682
+
683
+ // Check circuit breaker before attempting execution
684
+ if (circuitBreaker) {
685
+ const {
686
+ allowed,
687
+ state: cbState,
688
+ failureCount: cbFailureCount,
689
+ } = yield* circuitBreaker.allowRequest();
690
+
691
+ if (!allowed) {
692
+ const fallback = circuitBreaker.getFallback();
693
+
694
+ yield* Effect.logWarning(
695
+ `Circuit breaker OPEN for node type "${node.nodeTypeId}" - applying fallback`,
696
+ );
697
+
698
+ // Handle fallback based on configuration
699
+ if (fallback.type === "skip") {
700
+ // Skip the node but continue flow execution
701
+ if (onEvent) {
702
+ yield* onEvent({
703
+ jobId,
704
+ flowId,
705
+ nodeId,
706
+ eventType: EventType.NodeEnd,
707
+ nodeName: node.name,
708
+ });
709
+ }
710
+
711
+ // For skip fallback, we need to pass through some value
712
+ // Get the first input as pass-through data
713
+ const passThruInput = nodeInputs[nodeId];
714
+ return {
715
+ nodeId,
716
+ result: passThruInput,
717
+ success: true,
718
+ waiting: false,
719
+ };
720
+ }
721
+
722
+ if (fallback.type === "default") {
723
+ // Return configured default value
724
+ if (onEvent) {
725
+ yield* onEvent({
726
+ jobId,
727
+ flowId,
728
+ nodeId,
729
+ eventType: EventType.NodeEnd,
730
+ nodeName: node.name,
731
+ result: fallback.value,
732
+ });
733
+ }
734
+ return {
735
+ nodeId,
736
+ result: fallback.value,
737
+ success: true,
738
+ waiting: false,
739
+ };
740
+ }
741
+
742
+ // Default: fail immediately
743
+ return yield* UploadistaError.fromCode("CIRCUIT_BREAKER_OPEN", {
744
+ body: `Circuit breaker is open for node type "${node.name}"`,
745
+ details: {
746
+ nodeType: node.name,
747
+ nodeId,
748
+ state: cbState,
749
+ failureCount: cbFailureCount,
750
+ },
751
+ }).toEffect();
752
+ }
753
+ }
754
+
637
755
  let retryCount = 0;
638
756
  let lastError: UploadistaError | null = null;
639
757
 
@@ -778,6 +896,11 @@ export function createFlowWithSchema<
778
896
  }
779
897
  }
780
898
 
899
+ // Record success with circuit breaker
900
+ if (circuitBreaker) {
901
+ yield* circuitBreaker.recordSuccess();
902
+ }
903
+
781
904
  // Emit NodeEnd event with result
782
905
  if (onEvent) {
783
906
  yield* onEvent({
@@ -804,6 +927,11 @@ export function createFlowWithSchema<
804
927
  ? error
805
928
  : UploadistaError.fromCode("FLOW_NODE_ERROR", { cause: error });
806
929
 
930
+ // Record failure with circuit breaker (on each retry attempt)
931
+ if (circuitBreaker) {
932
+ yield* circuitBreaker.recordFailure(lastError.body);
933
+ }
934
+
807
935
  // Check if we should retry
808
936
  if (retryCount < maxRetries) {
809
937
  retryCount++;
@@ -890,6 +1018,14 @@ export function createFlowWithSchema<
890
1018
  UploadFileDataStores
891
1019
  > => {
892
1020
  return Effect.gen(function* () {
1021
+ // Get circuit breaker store from context (optional - if not provided, circuit breakers are disabled)
1022
+ const circuitBreakerStore = yield* Effect.serviceOption(
1023
+ CircuitBreakerStoreService,
1024
+ );
1025
+ const circuitBreakerRegistry = circuitBreakerStore._tag === "Some"
1026
+ ? new DistributedCircuitBreakerRegistry(circuitBreakerStore.value)
1027
+ : null;
1028
+
893
1029
  // Emit FlowStart event only if starting fresh
894
1030
  if (!resumeFrom && onEvent) {
895
1031
  yield* onEvent({
@@ -1005,6 +1141,7 @@ export function createFlowWithSchema<
1005
1141
  nodeMap,
1006
1142
  jobId,
1007
1143
  clientId,
1144
+ circuitBreakerRegistry,
1008
1145
  );
1009
1146
 
1010
1147
  return { nodeId, nodeResult };
@@ -1082,6 +1219,7 @@ export function createFlowWithSchema<
1082
1219
  nodeMap,
1083
1220
  jobId,
1084
1221
  clientId,
1222
+ circuitBreakerRegistry,
1085
1223
  );
1086
1224
 
1087
1225
  if (nodeResult.waiting) {
package/src/flow/index.ts CHANGED
@@ -1,11 +1,17 @@
1
+ // Circuit breaker
2
+ export * from "./circuit-breaker";
3
+ export * from "./circuit-breaker-store";
4
+ export * from "./distributed-circuit-breaker";
5
+
1
6
  // Edge types
2
7
  export type { FlowEdge } from "./edge";
3
8
  export * from "./edge";
4
9
 
5
10
  export * from "./event";
6
11
  export type { Flow, FlowData } from "./flow";
7
- // Type registry
8
- export * from "./type-registry";
12
+ // Type registries (separate registries for input and output types)
13
+ export * from "./input-type-registry";
14
+ export * from "./output-type-registry";
9
15
  // Built-in node types (auto-registers on import)
10
16
  import "./node-types";
11
17
 
@@ -38,5 +44,11 @@ export * from "./types/flow-file";
38
44
  export * from "./types/flow-job";
39
45
  export * from "./types/flow-types";
40
46
  export * from "./types/run-args";
47
+ // Dead Letter Queue types and service
48
+ export * from "./types/dead-letter-item";
49
+ export * from "./types/retry-policy";
50
+ export * from "./dead-letter-queue";
41
51
  export * from "./types/type-utils";
42
52
  export * from "./utils/resolve-upload-metadata";
53
+ // File naming utilities
54
+ export * from "./utils/file-naming";
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Input type registry for flow entry point nodes.
3
+ *
4
+ * This module provides a registry for input node type definitions. Input types
5
+ * describe how data enters the flow system from external sources (e.g., streaming
6
+ * uploads, URL fetches, webhook triggers).
7
+ *
8
+ * Input types are distinct from output types - they describe the external interface
9
+ * that clients use to interact with input nodes, not the data shape that flows
10
+ * through the system.
11
+ *
12
+ * @module flow/input-type-registry
13
+ * @see {@link outputTypeRegistry} for output types
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { inputTypeRegistry } from "@uploadista/core/flow";
18
+ * import { z } from "zod";
19
+ *
20
+ * // Register a custom input type
21
+ * inputTypeRegistry.register({
22
+ * id: "webhook-input-v1",
23
+ * schema: z.object({
24
+ * payload: z.unknown(),
25
+ * headers: z.record(z.string()),
26
+ * }),
27
+ * version: "1.0.0",
28
+ * description: "Webhook-triggered file input",
29
+ * });
30
+ * ```
31
+ */
32
+
33
+ import type { z } from "zod";
34
+ import { UploadistaError } from "../errors";
35
+
36
+ /**
37
+ * Defines a registered input type with its schema and metadata.
38
+ *
39
+ * Input type definitions describe how external clients interact with input nodes.
40
+ * Unlike output types, input types define the external interface (e.g., init/finalize
41
+ * operations for streaming uploads).
42
+ *
43
+ * @template TSchema - The Zod schema type for this input's data
44
+ *
45
+ * @property id - Unique identifier (e.g., "streaming-input-v1")
46
+ * @property schema - Zod schema for validating input data from clients
47
+ * @property version - Semantic version (e.g., "1.0.0") for tracking type evolution
48
+ * @property description - Human-readable explanation of what this input type does
49
+ */
50
+ export interface InputTypeDefinition<TSchema = unknown> {
51
+ id: string;
52
+ schema: z.ZodSchema<TSchema>;
53
+ version: string;
54
+ description: string;
55
+ }
56
+
57
+ /**
58
+ * Result type for input validation operations.
59
+ *
60
+ * @template T - The expected type on successful validation
61
+ */
62
+ export type InputValidationResult<T> =
63
+ | { success: true; data: T }
64
+ | { success: false; error: UploadistaError };
65
+
66
+ /**
67
+ * Registry for input node type definitions.
68
+ *
69
+ * The InputTypeRegistry maintains a global registry of input types with their schemas
70
+ * and metadata. Input types describe how data enters the flow from external sources.
71
+ *
72
+ * @remarks
73
+ * - Use the exported `inputTypeRegistry` singleton instance
74
+ * - Types cannot be unregistered or modified after registration
75
+ * - Duplicate type IDs are rejected
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * // Register a new input type
80
+ * inputTypeRegistry.register({
81
+ * id: "form-input-v1",
82
+ * schema: formInputSchema,
83
+ * version: "1.0.0",
84
+ * description: "Form-based file input",
85
+ * });
86
+ *
87
+ * // Check if type exists
88
+ * if (inputTypeRegistry.has("streaming-input-v1")) {
89
+ * const def = inputTypeRegistry.get("streaming-input-v1");
90
+ * }
91
+ * ```
92
+ */
93
+ export class InputTypeRegistry {
94
+ private readonly types: Map<string, InputTypeDefinition<unknown>>;
95
+
96
+ constructor() {
97
+ this.types = new Map();
98
+ }
99
+
100
+ /**
101
+ * Register a new input type in the registry.
102
+ *
103
+ * @template T - The TypeScript type inferred from the Zod schema
104
+ * @param definition - The complete type definition including schema and metadata
105
+ * @throws {UploadistaError} If a type with the same ID is already registered
106
+ */
107
+ register<T>(definition: InputTypeDefinition<T>): void {
108
+ if (this.types.has(definition.id)) {
109
+ throw UploadistaError.fromCode("VALIDATION_ERROR", {
110
+ body: `Input type "${definition.id}" is already registered. Types cannot be modified or re-registered.`,
111
+ details: { typeId: definition.id },
112
+ });
113
+ }
114
+
115
+ this.types.set(definition.id, definition as InputTypeDefinition<unknown>);
116
+ }
117
+
118
+ /**
119
+ * Retrieve a registered type definition by its ID.
120
+ *
121
+ * @param id - The unique type identifier (e.g., "streaming-input-v1")
122
+ * @returns The type definition if found, undefined otherwise
123
+ */
124
+ get(id: string): InputTypeDefinition<unknown> | undefined {
125
+ return this.types.get(id);
126
+ }
127
+
128
+ /**
129
+ * List all registered input types.
130
+ *
131
+ * @returns Array of all input type definitions
132
+ */
133
+ list(): InputTypeDefinition<unknown>[] {
134
+ return Array.from(this.types.values());
135
+ }
136
+
137
+ /**
138
+ * Validate data against a registered type's schema.
139
+ *
140
+ * @template T - The expected TypeScript type after validation
141
+ * @param typeId - The ID of the registered type to validate against
142
+ * @param data - The data to validate
143
+ * @returns A result object with either the validated data or an error
144
+ */
145
+ validate<T>(typeId: string, data: unknown): InputValidationResult<T> {
146
+ const typeDef = this.types.get(typeId);
147
+
148
+ if (!typeDef) {
149
+ return {
150
+ success: false,
151
+ error: UploadistaError.fromCode("VALIDATION_ERROR", {
152
+ body: `Input type "${typeId}" is not registered`,
153
+ details: { typeId },
154
+ }),
155
+ };
156
+ }
157
+
158
+ try {
159
+ const parsed = typeDef.schema.parse(data);
160
+ return { success: true, data: parsed as T };
161
+ } catch (error) {
162
+ return {
163
+ success: false,
164
+ error: UploadistaError.fromCode("VALIDATION_ERROR", {
165
+ body: `Data validation failed for input type "${typeId}"`,
166
+ cause: error,
167
+ details: { typeId, validationErrors: error },
168
+ }),
169
+ };
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Check if a type is registered.
175
+ *
176
+ * @param id - The unique type identifier to check
177
+ * @returns True if the type is registered, false otherwise
178
+ */
179
+ has(id: string): boolean {
180
+ return this.types.has(id);
181
+ }
182
+
183
+ /**
184
+ * Get the total number of registered types.
185
+ *
186
+ * @returns The count of registered types
187
+ */
188
+ size(): number {
189
+ return this.types.size;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Global singleton instance of the input type registry.
195
+ *
196
+ * Use this instance to register and access input node type definitions.
197
+ * Input types describe how data enters the flow from external sources.
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * import { inputTypeRegistry } from "@uploadista/core/flow";
202
+ *
203
+ * // Register a type
204
+ * inputTypeRegistry.register({
205
+ * id: "my-input-v1",
206
+ * schema: myInputSchema,
207
+ * version: "1.0.0",
208
+ * description: "My custom input type",
209
+ * });
210
+ *
211
+ * // Validate data
212
+ * const result = inputTypeRegistry.validate("my-input-v1", data);
213
+ * ```
214
+ */
215
+ export const inputTypeRegistry = new InputTypeRegistry();
216
+
217
+ /**
218
+ * Validates flow input data against a registered input type.
219
+ *
220
+ * @param typeId - The registered type ID (e.g., "streaming-input-v1")
221
+ * @param data - The input data to validate
222
+ * @returns A validation result with either the typed data or an error
223
+ */
224
+ export function validateFlowInput<T = unknown>(
225
+ typeId: string,
226
+ data: unknown,
227
+ ): InputValidationResult<T> {
228
+ return inputTypeRegistry.validate<T>(typeId, data);
229
+ }
@@ -4,6 +4,10 @@
4
4
  * This module automatically registers the standard input and output node types
5
5
  * when imported. These types enable type-safe result consumption in clients.
6
6
  *
7
+ * Input types are registered in `inputTypeRegistry` and describe how data enters
8
+ * the flow from external sources. Output types are registered in `outputTypeRegistry`
9
+ * and describe the data shapes produced by nodes.
10
+ *
7
11
  * @module flow/node-types
8
12
  *
9
13
  * @remarks
@@ -14,18 +18,22 @@
14
18
  * ```typescript
15
19
  * // Types are automatically registered on import
16
20
  * import "@uploadista/core/flow";
17
- * import { flowTypeRegistry } from "@uploadista/core/flow";
21
+ * import { inputTypeRegistry, outputTypeRegistry } from "@uploadista/core/flow";
18
22
  *
19
23
  * // Check registered types
20
- * const inputTypes = flowTypeRegistry.listByCategory("input");
21
- * console.log(inputTypes.map(t => t.id)); // ["storage-output-v1"]
24
+ * const inputTypes = inputTypeRegistry.list();
25
+ * console.log(inputTypes.map(t => t.id)); // ["streaming-input-v1"]
26
+ *
27
+ * const outputTypes = outputTypeRegistry.list();
28
+ * console.log(outputTypes.map(t => t.id)); // ["storage-output-v1", "ocr-output-v1", ...]
22
29
  * ```
23
30
  */
24
31
 
25
32
  import { z } from "zod";
26
33
  import { uploadFileSchema } from "../../types/upload-file";
34
+ import { inputTypeRegistry } from "../input-type-registry";
27
35
  import { inputDataSchema } from "../nodes/input-node";
28
- import { flowTypeRegistry } from "../type-registry";
36
+ import { outputTypeRegistry } from "../output-type-registry";
29
37
 
30
38
  /**
31
39
  * Type ID constants for built-in node types.
@@ -35,11 +43,12 @@ import { flowTypeRegistry } from "../type-registry";
35
43
  *
36
44
  * @example
37
45
  * ```typescript
38
- * import { STREAMING_INPUT_TYPE_ID } from "@uploadista/core/flow";
46
+ * import { STREAMING_INPUT_TYPE_ID, STORAGE_OUTPUT_TYPE_ID } from "@uploadista/core/flow";
39
47
  *
40
48
  * const inputNode = createFlowNode({
41
49
  * // ... other config
42
- * nodeTypeId: STREAMING_INPUT_TYPE_ID
50
+ * inputTypeId: STREAMING_INPUT_TYPE_ID,
51
+ * outputTypeId: STORAGE_OUTPUT_TYPE_ID,
43
52
  * });
44
53
  * ```
45
54
  */
@@ -88,7 +97,7 @@ export type ImageDescriptionOutput = z.infer<
88
97
  >;
89
98
 
90
99
  /**
91
- * Register streaming input node type.
100
+ * Register streaming input node type in inputTypeRegistry.
92
101
  *
93
102
  * This is the standard input type for flows that accept file uploads via
94
103
  * streaming chunks or direct URL fetches. It supports three operations:
@@ -96,9 +105,8 @@ export type ImageDescriptionOutput = z.infer<
96
105
  * - finalize: Complete the upload after all chunks are uploaded
97
106
  * - url: Fetch a file directly from a URL
98
107
  */
99
- flowTypeRegistry.register({
108
+ inputTypeRegistry.register({
100
109
  id: STREAMING_INPUT_TYPE_ID,
101
- category: "input",
102
110
  schema: inputDataSchema,
103
111
  version: "1.0.0",
104
112
  description:
@@ -106,14 +114,13 @@ flowTypeRegistry.register({
106
114
  });
107
115
 
108
116
  /**
109
- * Register storage output node type.
117
+ * Register storage output node type in outputTypeRegistry.
110
118
  *
111
119
  * This is the standard output type for flows that save files to storage backends
112
120
  * (S3, Azure, GCS, etc.). It produces UploadFile objects with final storage URLs.
113
121
  */
114
- flowTypeRegistry.register({
122
+ outputTypeRegistry.register({
115
123
  id: STORAGE_OUTPUT_TYPE_ID,
116
- category: "output",
117
124
  schema: uploadFileSchema,
118
125
  version: "1.0.0",
119
126
  description:
@@ -121,14 +128,13 @@ flowTypeRegistry.register({
121
128
  });
122
129
 
123
130
  /**
124
- * Register OCR output node type.
131
+ * Register OCR output node type in outputTypeRegistry.
125
132
  *
126
133
  * This output type is for document text extraction nodes that use AI/OCR to
127
134
  * extract structured text from images or PDFs.
128
135
  */
129
- flowTypeRegistry.register({
136
+ outputTypeRegistry.register({
130
137
  id: OCR_OUTPUT_TYPE_ID,
131
- category: "output",
132
138
  schema: ocrOutputSchema,
133
139
  version: "1.0.0",
134
140
  description:
@@ -136,19 +142,19 @@ flowTypeRegistry.register({
136
142
  });
137
143
 
138
144
  /**
139
- * Register image description output node type.
145
+ * Register image description output node type in outputTypeRegistry.
140
146
  *
141
147
  * This output type is for AI-powered image analysis nodes that generate
142
148
  * textual descriptions of image content.
143
149
  */
144
- flowTypeRegistry.register({
150
+ outputTypeRegistry.register({
145
151
  id: IMAGE_DESCRIPTION_OUTPUT_TYPE_ID,
146
- category: "output",
147
152
  schema: imageDescriptionOutputSchema,
148
153
  version: "1.0.0",
149
154
  description:
150
155
  "Image description output node that generates AI-powered descriptions of images",
151
156
  });
152
157
 
153
- // Export the registry for convenience
154
- export { flowTypeRegistry } from "../type-registry";
158
+ // Export the registries for convenience
159
+ export { inputTypeRegistry } from "../input-type-registry";
160
+ export { outputTypeRegistry } from "../output-type-registry";