@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
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
|
|
506
|
-
const
|
|
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:
|
|
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
|
|
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 {
|
|
21
|
+
* import { inputTypeRegistry, outputTypeRegistry } from "@uploadista/core/flow";
|
|
18
22
|
*
|
|
19
23
|
* // Check registered types
|
|
20
|
-
* const inputTypes =
|
|
21
|
-
* console.log(inputTypes.map(t => t.id)); // ["
|
|
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 {
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
154
|
-
export {
|
|
158
|
+
// Export the registries for convenience
|
|
159
|
+
export { inputTypeRegistry } from "../input-type-registry";
|
|
160
|
+
export { outputTypeRegistry } from "../output-type-registry";
|