@uploadista/core 0.0.18 → 0.0.20-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/{checksum-p3NmuAky.cjs → checksum-DVPe3Db4.cjs} +1 -1
  2. package/dist/errors/index.cjs +1 -1
  3. package/dist/errors/index.d.cts +0 -1
  4. package/dist/flow/index.cjs +1 -1
  5. package/dist/flow/index.d.cts +2 -6
  6. package/dist/flow/index.d.mts +2 -2
  7. package/dist/flow/index.mjs +1 -1
  8. package/dist/flow-CAlAQtBK.cjs +1 -0
  9. package/dist/flow-DWNJ-NOU.mjs +2 -0
  10. package/dist/flow-DWNJ-NOU.mjs.map +1 -0
  11. package/dist/index-9gyMMEIB.d.cts.map +1 -1
  12. package/dist/{index-TokXRAZ5.d.mts → index-B3_9v6Z8.d.mts} +494 -36
  13. package/dist/index-B3_9v6Z8.d.mts.map +1 -0
  14. package/dist/{index-BOic6-Cg.d.cts → index-br6o9tCI.d.cts} +494 -36
  15. package/dist/index-br6o9tCI.d.cts.map +1 -0
  16. package/dist/index.cjs +1 -1
  17. package/dist/index.d.cts +2 -3
  18. package/dist/index.d.mts +2 -2
  19. package/dist/index.mjs +1 -1
  20. package/dist/{stream-limiter-Cem7Zvaw.cjs → stream-limiter-BvkaZXcz.cjs} +1 -1
  21. package/dist/streams/index.cjs +1 -1
  22. package/dist/streams/index.d.cts +0 -1
  23. package/dist/testing/index.cjs +2 -2
  24. package/dist/testing/index.d.cts +1 -5
  25. package/dist/testing/index.d.cts.map +1 -1
  26. package/dist/testing/index.d.mts +1 -1
  27. package/dist/testing/index.d.mts.map +1 -1
  28. package/dist/testing/index.mjs +3 -3
  29. package/dist/testing/index.mjs.map +1 -1
  30. package/dist/types/index.cjs +1 -1
  31. package/dist/types/index.d.cts +2 -6
  32. package/dist/types/index.d.mts +2 -2
  33. package/dist/types/index.mjs +1 -1
  34. package/dist/types-Cws60JHC.cjs +1 -0
  35. package/dist/types-DKGQJIEr.mjs +2 -0
  36. package/dist/types-DKGQJIEr.mjs.map +1 -0
  37. package/dist/upload/index.cjs +1 -1
  38. package/dist/upload/index.d.cts +1 -5
  39. package/dist/upload/index.d.mts +1 -1
  40. package/dist/upload/index.mjs +1 -1
  41. package/dist/{upload-5l3utoc7.cjs → upload-BHDuuJ80.cjs} +1 -1
  42. package/dist/{upload-B2RDFkTe.mjs → upload-tLC7uR9U.mjs} +2 -2
  43. package/dist/upload-tLC7uR9U.mjs.map +1 -0
  44. package/dist/{uploadista-error-BfpQ4mOO.cjs → uploadista-error-BgQU45we.cjs} +1 -1
  45. package/dist/utils/index.cjs +1 -1
  46. package/dist/utils/index.d.cts +0 -1
  47. package/dist/{utils-QJOPnlmt.cjs → utils-UUJt8ILJ.cjs} +1 -1
  48. package/package.json +3 -3
  49. package/src/flow/index.ts +10 -0
  50. package/src/flow/nodes/transform-node.ts +321 -29
  51. package/src/flow/plugins/image-plugin.ts +101 -1
  52. package/src/flow/plugins/video-plugin.ts +124 -1
  53. package/src/testing/mock-upload-server.ts +81 -2
  54. package/src/types/data-store.ts +157 -0
  55. package/src/types/input-file.ts +47 -21
  56. package/src/upload/upload-server.ts +234 -1
  57. package/dist/flow-DKCp_0Y1.mjs +0 -2
  58. package/dist/flow-DKCp_0Y1.mjs.map +0 -1
  59. package/dist/flow-NHkTGTxu.cjs +0 -1
  60. package/dist/index-BOic6-Cg.d.cts.map +0 -1
  61. package/dist/index-TokXRAZ5.d.mts.map +0 -1
  62. package/dist/types-CHbyV8e6.mjs +0 -2
  63. package/dist/types-CHbyV8e6.mjs.map +0 -1
  64. package/dist/types-D3_rWxD0.cjs +0 -1
  65. package/dist/upload-B2RDFkTe.mjs.map +0 -1
@@ -1,7 +1,7 @@
1
- import { Effect } from "effect";
1
+ import { Effect, Stream } from "effect";
2
2
  import type { UploadistaError } from "../../errors";
3
- import type { UploadFile } from "../../types";
4
- import { uploadFileSchema } from "../../types";
3
+ import type { StreamingConfig, UploadFile } from "../../types";
4
+ import { DEFAULT_STREAMING_CONFIG, uploadFileSchema } from "../../types";
5
5
  import { UploadServer } from "../../upload";
6
6
  import { createFlowNode, NodeType } from "../node";
7
7
  import { completeNodeExecution, type FileNamingConfig } from "../types";
@@ -12,6 +12,38 @@ import {
12
12
  } from "../utils/file-naming";
13
13
  import { resolveUploadMetadata } from "../utils/resolve-upload-metadata";
14
14
 
15
+ /**
16
+ * Transform mode for controlling how file data is processed.
17
+ *
18
+ * - `buffered`: Always load entire file into memory before transforming (default, backward compatible)
19
+ * - `streaming`: Process file as a stream of chunks for memory efficiency
20
+ * - `auto`: Automatically select mode based on file size and DataStore capabilities
21
+ */
22
+ export type TransformMode = "buffered" | "streaming" | "auto";
23
+
24
+ /**
25
+ * Result type for streaming transforms.
26
+ * Can return just the transformed stream, or include metadata changes.
27
+ */
28
+ export type StreamingTransformResult =
29
+ | Stream.Stream<Uint8Array, UploadistaError>
30
+ | {
31
+ stream: Stream.Stream<Uint8Array, UploadistaError>;
32
+ type?: string;
33
+ fileName?: string;
34
+ /** Estimated output size in bytes (for progress tracking) */
35
+ estimatedSize?: number;
36
+ };
37
+
38
+ /**
39
+ * Function type for streaming transforms.
40
+ * Receives an input stream and file metadata, returns a transformed stream.
41
+ */
42
+ export type StreamingTransformFn = (
43
+ stream: Stream.Stream<Uint8Array, UploadistaError>,
44
+ file: UploadFile,
45
+ ) => Effect.Effect<StreamingTransformResult, UploadistaError>;
46
+
15
47
  /**
16
48
  * Configuration object for creating a transform node.
17
49
  */
@@ -60,14 +92,47 @@ export interface TransformNodeConfig {
60
92
  * Overrides flow-level circuit breaker defaults for this node.
61
93
  */
62
94
  circuitBreaker?: FlowCircuitBreakerConfig;
63
- /** Function that transforms file bytes */
64
- transform: (
95
+ /**
96
+ * Transform mode controlling how file data is processed.
97
+ * - `buffered`: Always load entire file into memory
98
+ * - `streaming`: Process file as a stream of chunks
99
+ * - `auto`: Select mode based on file size and DataStore capabilities (default)
100
+ *
101
+ * @default "auto"
102
+ */
103
+ mode?: TransformMode;
104
+ /**
105
+ * Configuration for streaming mode (file size threshold, chunk size).
106
+ * Only used when mode is "streaming" or "auto".
107
+ */
108
+ streamingConfig?: StreamingConfig;
109
+ /**
110
+ * Function that transforms file bytes (buffered mode).
111
+ * Required unless streamingTransform is provided and mode is "streaming".
112
+ */
113
+ transform?: (
65
114
  bytes: Uint8Array,
66
115
  file: UploadFile,
67
116
  ) => Effect.Effect<
68
117
  Uint8Array | { bytes: Uint8Array; type?: string; fileName?: string },
69
118
  UploadistaError
70
119
  >;
120
+ /**
121
+ * Function that transforms file as a stream (streaming mode).
122
+ * For memory-efficient processing of large files.
123
+ * Used when mode is "streaming" or when "auto" selects streaming.
124
+ */
125
+ streamingTransform?: StreamingTransformFn;
126
+ }
127
+
128
+ /**
129
+ * Helper to check if a StreamingTransformResult is a stream or an object with metadata.
130
+ */
131
+ function isStreamResult(
132
+ result: StreamingTransformResult,
133
+ ): result is Stream.Stream<Uint8Array, UploadistaError> {
134
+ // Check if it has the 'stream' property (object form) vs is a Stream directly
135
+ return !("stream" in result);
71
136
  }
72
137
 
73
138
  /**
@@ -79,12 +144,17 @@ export interface TransformNodeConfig {
79
144
  * This simplifies nodes that just need to transform file bytes without
80
145
  * worrying about upload server interactions.
81
146
  *
147
+ * Supports both buffered and streaming modes:
148
+ * - **Buffered mode**: Loads entire file into memory, transforms, uploads
149
+ * - **Streaming mode**: Processes file as chunks for memory efficiency with large files
150
+ * - **Auto mode** (default): Selects mode based on file size and DataStore capabilities
151
+ *
82
152
  * @param config - Configuration object for the transform node
83
153
  * @returns An Effect that creates a flow node configured for file transformation
84
154
  *
85
155
  * @example
86
156
  * ```typescript
87
- * // Create an image resize transform node
157
+ * // Create a transform node with auto mode (default) - uses streaming for large files
88
158
  * const resizeNode = yield* createTransformNode({
89
159
  * id: "resize-image",
90
160
  * name: "Resize Image",
@@ -92,31 +162,31 @@ export interface TransformNodeConfig {
92
162
  * transform: (bytes, file) => {
93
163
  * // Your transformation logic here
94
164
  * return Effect.succeed(transformedBytes);
165
+ * },
166
+ * streamingTransform: (stream, file) => {
167
+ * const transformed = Stream.map(stream, (chunk) => processChunk(chunk));
168
+ * return Effect.succeed(transformed);
95
169
  * }
96
170
  * });
97
171
  *
98
- * // Create a transform node with keepOutput enabled
99
- * const processedNode = yield* createTransformNode({
100
- * id: "process-image",
101
- * name: "Process Image",
102
- * description: "Processes images and preserves output",
103
- * keepOutput: true, // Output will be included in flow results
104
- * transform: (bytes, file) => {
105
- * return Effect.succeed(transformedBytes);
106
- * }
172
+ * // Force buffered mode for specific use cases
173
+ * const bufferedNode = yield* createTransformNode({
174
+ * id: "optimize-small",
175
+ * name: "Optimize Small Files",
176
+ * description: "Optimizes small files with buffered mode",
177
+ * mode: "buffered",
178
+ * transform: (bytes, file) => Effect.succeed(transformBytes(bytes)),
107
179
  * });
108
180
  *
109
- * // Create a transform node that changes file metadata
110
- * const metadataTransformNode = yield* createTransformNode({
111
- * id: "add-metadata",
112
- * name: "Add Metadata",
113
- * description: "Adds custom metadata to files",
114
- * transform: (bytes, file) => {
115
- * return Effect.succeed({
116
- * bytes,
117
- * type: "application/custom",
118
- * fileName: `processed-${file.fileName}`
119
- * });
181
+ * // Force streaming mode for memory efficiency
182
+ * const streamingNode = yield* createTransformNode({
183
+ * id: "optimize-large",
184
+ * name: "Optimize Large Files",
185
+ * description: "Optimizes large files with streaming",
186
+ * mode: "streaming",
187
+ * streamingTransform: (stream, file) => {
188
+ * const transformed = Stream.map(stream, (chunk) => processChunk(chunk));
189
+ * return Effect.succeed(transformed);
120
190
  * }
121
191
  * });
122
192
  * ```
@@ -132,8 +202,34 @@ export function createTransformNode({
132
202
  nodeTypeId,
133
203
  namingVars,
134
204
  circuitBreaker,
205
+ mode = "auto",
206
+ streamingConfig,
135
207
  transform,
208
+ streamingTransform,
136
209
  }: TransformNodeConfig) {
210
+ // Validate configuration
211
+ if (mode === "streaming" && !streamingTransform) {
212
+ throw new Error(
213
+ `Transform node "${id}": mode is "streaming" but no streamingTransform function provided`,
214
+ );
215
+ }
216
+ if (mode === "buffered" && !transform) {
217
+ throw new Error(
218
+ `Transform node "${id}": mode is "buffered" but no transform function provided`,
219
+ );
220
+ }
221
+ if (mode === "auto" && !transform && !streamingTransform) {
222
+ throw new Error(
223
+ `Transform node "${id}": mode is "auto" but neither transform nor streamingTransform provided`,
224
+ );
225
+ }
226
+
227
+ // Merge streaming config with defaults
228
+ const effectiveStreamingConfig = {
229
+ ...DEFAULT_STREAMING_CONFIG,
230
+ ...streamingConfig,
231
+ };
232
+
137
233
  return Effect.gen(function* () {
138
234
  const uploadServer = yield* UploadServer;
139
235
 
@@ -155,6 +251,205 @@ export function createTransformNode({
155
251
  nodeId: id,
156
252
  jobId,
157
253
  };
254
+
255
+ // Determine which mode to use
256
+ const shouldUseStreaming = yield* Effect.gen(function* () {
257
+ if (mode === "buffered") return false;
258
+ if (mode === "streaming") return true;
259
+
260
+ // Auto mode: check file size and capabilities
261
+ const fileSize = file.size ?? 0;
262
+ const threshold = effectiveStreamingConfig.fileSizeThreshold;
263
+
264
+ // If file is smaller than threshold, use buffered
265
+ if (fileSize > 0 && fileSize < threshold) {
266
+ yield* Effect.logDebug(
267
+ `File ${file.id} (${fileSize} bytes) below threshold (${threshold}), using buffered mode`,
268
+ );
269
+ return false;
270
+ }
271
+
272
+ // Check if we have the required functions
273
+ if (!streamingTransform) {
274
+ yield* Effect.logDebug(
275
+ `No streamingTransform function, using buffered mode`,
276
+ );
277
+ return false;
278
+ }
279
+
280
+ // Check DataStore capabilities via UploadServer
281
+ const capabilities = yield* uploadServer.getCapabilities(
282
+ storageId,
283
+ clientId,
284
+ );
285
+ if (!capabilities.supportsStreamingRead) {
286
+ yield* Effect.logDebug(
287
+ `DataStore doesn't support streaming read, using buffered mode`,
288
+ );
289
+ return false;
290
+ }
291
+
292
+ yield* Effect.logDebug(
293
+ `File ${file.id} qualifies for streaming mode`,
294
+ );
295
+ return true;
296
+ });
297
+
298
+ const { type, fileName, metadata, metadataJson } =
299
+ resolveUploadMetadata(file.metadata);
300
+
301
+ if (shouldUseStreaming && streamingTransform) {
302
+ // STREAMING PATH - True end-to-end streaming
303
+ yield* Effect.logDebug(`Using streaming transform for ${file.id}`);
304
+
305
+ // Get input stream
306
+ const inputStream = yield* uploadServer.readStream(
307
+ file.id,
308
+ clientId,
309
+ effectiveStreamingConfig,
310
+ );
311
+
312
+ // Transform the stream
313
+ const transformResult = yield* streamingTransform(
314
+ inputStream,
315
+ file,
316
+ );
317
+
318
+ // Extract stream and metadata from result
319
+ const outputStream = isStreamResult(transformResult)
320
+ ? transformResult
321
+ : transformResult.stream;
322
+ const outputType = isStreamResult(transformResult)
323
+ ? undefined
324
+ : transformResult.type;
325
+ const estimatedSize = isStreamResult(transformResult)
326
+ ? undefined
327
+ : transformResult.estimatedSize;
328
+
329
+ // Get fileName from transform result or apply naming config
330
+ let outputFileName = isStreamResult(transformResult)
331
+ ? undefined
332
+ : transformResult.fileName;
333
+
334
+ if (!outputFileName && naming) {
335
+ const namingContext = buildNamingContext(
336
+ file,
337
+ { flowId, jobId, nodeId: id, nodeType: namingNodeType },
338
+ namingVars,
339
+ );
340
+ outputFileName = applyFileNaming(file, namingContext, naming);
341
+ }
342
+
343
+ // Check if DataStore supports streaming writes
344
+ const capabilities = yield* uploadServer.getCapabilities(
345
+ storageId,
346
+ clientId,
347
+ );
348
+
349
+ let result: UploadFile;
350
+
351
+ if (capabilities.supportsStreamingWrite) {
352
+ // True end-to-end streaming: pipe transform output directly to storage
353
+ yield* Effect.logDebug(
354
+ `Using streaming write for ${file.id} - no intermediate buffering`,
355
+ );
356
+
357
+ result = yield* uploadServer.uploadStream(
358
+ {
359
+ storageId,
360
+ uploadLengthDeferred: true,
361
+ sizeHint: estimatedSize,
362
+ type: outputType ?? type,
363
+ fileName: outputFileName ?? fileName,
364
+ lastModified: 0,
365
+ metadata: metadataJson,
366
+ flow,
367
+ },
368
+ clientId,
369
+ outputStream,
370
+ );
371
+ } else {
372
+ // Fallback: buffer the output before uploading
373
+ // This path is for DataStores that don't support streaming writes
374
+ yield* Effect.logDebug(
375
+ `Falling back to buffered upload for ${file.id} (streaming write not supported)`,
376
+ );
377
+
378
+ const outputChunks: Uint8Array[] = [];
379
+ yield* Stream.runForEach(outputStream, (chunk) =>
380
+ Effect.sync(() => {
381
+ outputChunks.push(chunk);
382
+ }),
383
+ );
384
+
385
+ // Concatenate chunks into a single Uint8Array
386
+ const totalLength = outputChunks.reduce(
387
+ (sum, chunk) => sum + chunk.byteLength,
388
+ 0,
389
+ );
390
+ const outputBytes = new Uint8Array(totalLength);
391
+ let offset = 0;
392
+ for (const chunk of outputChunks) {
393
+ outputBytes.set(chunk, offset);
394
+ offset += chunk.byteLength;
395
+ }
396
+
397
+ // Create a ReadableStream for upload
398
+ const bufferedUploadStream = new ReadableStream({
399
+ start(controller) {
400
+ controller.enqueue(outputBytes);
401
+ controller.close();
402
+ },
403
+ });
404
+
405
+ result = yield* uploadServer.upload(
406
+ {
407
+ storageId,
408
+ size: outputBytes.byteLength,
409
+ type: outputType ?? type,
410
+ fileName: outputFileName ?? fileName,
411
+ lastModified: 0,
412
+ metadata: metadataJson,
413
+ flow,
414
+ },
415
+ clientId,
416
+ bufferedUploadStream,
417
+ );
418
+ }
419
+
420
+ // Merge updated metadata
421
+ const updatedMetadata = metadata
422
+ ? {
423
+ ...metadata,
424
+ ...(outputType && {
425
+ mimeType: outputType,
426
+ type: outputType,
427
+ "content-type": outputType,
428
+ }),
429
+ ...(outputFileName && {
430
+ fileName: outputFileName,
431
+ originalName: outputFileName,
432
+ name: outputFileName,
433
+ extension:
434
+ outputFileName.split(".").pop() || metadata.extension,
435
+ }),
436
+ }
437
+ : result.metadata;
438
+
439
+ return completeNodeExecution(
440
+ updatedMetadata
441
+ ? { ...result, metadata: updatedMetadata }
442
+ : result,
443
+ );
444
+ }
445
+
446
+ // BUFFERED PATH (default, backward compatible)
447
+ if (!transform) {
448
+ throw new Error(
449
+ `Transform node "${id}": buffered mode selected but no transform function provided`,
450
+ );
451
+ }
452
+
158
453
  // Read input bytes from upload server
159
454
  const inputBytes = yield* uploadServer.read(file.id, clientId);
160
455
 
@@ -201,9 +496,6 @@ export function createTransformNode({
201
496
  },
202
497
  });
203
498
 
204
- const { type, fileName, metadata, metadataJson } =
205
- resolveUploadMetadata(file.metadata);
206
-
207
499
  // Upload the transformed bytes back to the upload server
208
500
  // Use output metadata if provided, otherwise fall back to original
209
501
  const result = yield* uploadServer.upload(
@@ -1,4 +1,4 @@
1
- import { Context, type Effect, type Layer } from "effect";
1
+ import { Context, type Effect, type Layer, type Stream } from "effect";
2
2
  import type { UploadistaError } from "../../errors";
3
3
  import type { OptimizeParams } from "./types/optimize-node";
4
4
  import type { ResizeParams } from "./types/resize-node";
@@ -73,6 +73,106 @@ export type ImagePluginShape = {
73
73
  input: Uint8Array,
74
74
  transformation: Transformation,
75
75
  ) => Effect.Effect<Uint8Array, UploadistaError>;
76
+
77
+ /**
78
+ * Optimizes an image using streaming for memory-efficient processing of large files.
79
+ *
80
+ * This method processes image data as a stream, which is beneficial for large images
81
+ * where loading the entire file into memory would be problematic.
82
+ *
83
+ * Note: Image processing inherently requires decoding the full image, so memory
84
+ * savings are primarily from avoiding double-buffering. The streaming interface
85
+ * allows better pipeline integration with DataStore streaming reads.
86
+ *
87
+ * @param input - The input image as an Effect Stream of Uint8Array chunks
88
+ * @param options - Optimization parameters including quality and format
89
+ * @returns An Effect that resolves to a Stream of the optimized image bytes
90
+ * @throws {UploadistaError} When image optimization fails
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * const program = Effect.gen(function* () {
95
+ * const imagePlugin = yield* ImagePlugin;
96
+ * const inputStream = yield* dataStore.readStream(fileId);
97
+ * const outputStream = yield* imagePlugin.optimizeStream(inputStream, {
98
+ * quality: 80,
99
+ * format: "webp"
100
+ * });
101
+ * return outputStream;
102
+ * });
103
+ * ```
104
+ */
105
+ optimizeStream?: (
106
+ input: Stream.Stream<Uint8Array, UploadistaError>,
107
+ options: OptimizeParams,
108
+ ) => Effect.Effect<Stream.Stream<Uint8Array, UploadistaError>, UploadistaError>;
109
+
110
+ /**
111
+ * Resizes an image using streaming for memory-efficient processing of large files.
112
+ *
113
+ * This method processes image data as a stream. Like other image operations,
114
+ * the full image must be decoded before processing, but the streaming interface
115
+ * avoids double-buffering when combined with streaming DataStore reads and writes.
116
+ *
117
+ * @param input - The input image as an Effect Stream of Uint8Array chunks
118
+ * @param options - Resize parameters including width, height, and fit mode
119
+ * @returns An Effect that resolves to a Stream of the resized image bytes
120
+ * @throws {UploadistaError} When image resizing fails
121
+ *
122
+ * @example
123
+ * ```typescript
124
+ * const program = Effect.gen(function* () {
125
+ * const imagePlugin = yield* ImagePlugin;
126
+ * const inputStream = yield* dataStore.readStream(fileId);
127
+ * const outputStream = yield* imagePlugin.resizeStream(inputStream, {
128
+ * width: 800,
129
+ * height: 600,
130
+ * fit: "cover"
131
+ * });
132
+ * return outputStream;
133
+ * });
134
+ * ```
135
+ */
136
+ resizeStream?: (
137
+ input: Stream.Stream<Uint8Array, UploadistaError>,
138
+ options: ResizeParams,
139
+ ) => Effect.Effect<Stream.Stream<Uint8Array, UploadistaError>, UploadistaError>;
140
+
141
+ /**
142
+ * Applies a single transformation using streaming for memory-efficient processing.
143
+ *
144
+ * This method processes image data as a stream. The streaming interface
145
+ * allows better pipeline integration with DataStore streaming reads and writes,
146
+ * reducing peak memory usage for large files.
147
+ *
148
+ * @param input - The input image as an Effect Stream of Uint8Array chunks
149
+ * @param transformation - The transformation to apply
150
+ * @returns An Effect that resolves to a Stream of the transformed image bytes
151
+ * @throws {UploadistaError} When transformation fails
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * const program = Effect.gen(function* () {
156
+ * const imagePlugin = yield* ImagePlugin;
157
+ * const inputStream = yield* dataStore.readStream(fileId);
158
+ * const outputStream = yield* imagePlugin.transformStream(inputStream, {
159
+ * type: 'blur',
160
+ * sigma: 5.0
161
+ * });
162
+ * return outputStream;
163
+ * });
164
+ * ```
165
+ */
166
+ transformStream?: (
167
+ input: Stream.Stream<Uint8Array, UploadistaError>,
168
+ transformation: Transformation,
169
+ ) => Effect.Effect<Stream.Stream<Uint8Array, UploadistaError>, UploadistaError>;
170
+
171
+ /**
172
+ * Indicates whether this plugin supports streaming operations.
173
+ * Returns true if streaming methods (optimizeStream, resizeStream, transformStream) are available.
174
+ */
175
+ supportsStreaming?: boolean;
76
176
  };
77
177
 
78
178
  /**
@@ -1,4 +1,4 @@
1
- import { Context, type Effect, type Layer } from "effect";
1
+ import { Context, type Effect, type Layer, type Stream } from "effect";
2
2
  import type { UploadistaError } from "../../errors";
3
3
  import type { DescribeVideoMetadata } from "./types/describe-video-node";
4
4
  import type { ExtractFrameVideoParams } from "./types/extract-frame-video-node";
@@ -6,6 +6,26 @@ import type { ResizeVideoParams } from "./types/resize-video-node";
6
6
  import type { TranscodeVideoParams } from "./types/transcode-video-node";
7
7
  import type { TrimVideoParams } from "./types/trim-video-node";
8
8
 
9
+ /**
10
+ * Input type for streaming video operations.
11
+ * Accepts either buffered input (Uint8Array) or streaming input (Effect Stream).
12
+ * Streaming input is only supported for specific formats like MPEG-TS.
13
+ */
14
+ export type VideoStreamInput =
15
+ | Uint8Array
16
+ | Stream.Stream<Uint8Array, UploadistaError>;
17
+
18
+ /**
19
+ * Options for streaming video operations.
20
+ */
21
+ export type VideoStreamOptions = {
22
+ /**
23
+ * Hint for input format to help determine if streaming input is possible.
24
+ * MPEG-TS format supports true streaming input; other formats require buffering.
25
+ */
26
+ inputFormat?: string;
27
+ };
28
+
9
29
  /**
10
30
  * Shape definition for the Video Plugin interface.
11
31
  * Defines the contract that all video processing implementations must follow.
@@ -73,6 +93,109 @@ export type VideoPluginShape = {
73
93
  describe: (
74
94
  input: Uint8Array,
75
95
  ) => Effect.Effect<DescribeVideoMetadata, UploadistaError>;
96
+
97
+ /**
98
+ * Transcodes a video using streaming for memory-efficient processing of large files.
99
+ *
100
+ * This method outputs the transcoded video as a stream, reducing peak memory usage.
101
+ * For input, it accepts either a buffered Uint8Array or a Stream. Streaming input
102
+ * is only supported for MPEG-TS format; other formats will be buffered internally.
103
+ *
104
+ * @param input - The input video as Uint8Array or Stream (MPEG-TS only for streaming)
105
+ * @param options - Transcode parameters including format, codec, and bitrates
106
+ * @param streamOptions - Optional streaming configuration including input format hint
107
+ * @returns An Effect that resolves to a Stream of the transcoded video bytes
108
+ * @throws {UploadistaError} When video transcoding fails
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * const program = Effect.gen(function* () {
113
+ * const videoPlugin = yield* VideoPlugin;
114
+ * const inputStream = yield* dataStore.readStream(fileId);
115
+ * const outputStream = yield* videoPlugin.transcodeStream(inputStream, {
116
+ * format: "mp4",
117
+ * codec: "h264"
118
+ * }, { inputFormat: "video/mp2t" });
119
+ * return outputStream;
120
+ * });
121
+ * ```
122
+ */
123
+ transcodeStream?: (
124
+ input: VideoStreamInput,
125
+ options: TranscodeVideoParams,
126
+ streamOptions?: VideoStreamOptions,
127
+ ) => Effect.Effect<Stream.Stream<Uint8Array, UploadistaError>, UploadistaError>;
128
+
129
+ /**
130
+ * Resizes a video using streaming for memory-efficient processing of large files.
131
+ *
132
+ * This method outputs the resized video as a stream, reducing peak memory usage.
133
+ * For input, it accepts either a buffered Uint8Array or a Stream. Streaming input
134
+ * is only supported for MPEG-TS format; other formats will be buffered internally.
135
+ *
136
+ * @param input - The input video as Uint8Array or Stream (MPEG-TS only for streaming)
137
+ * @param options - Resize parameters including width, height, and aspect ratio
138
+ * @param streamOptions - Optional streaming configuration including input format hint
139
+ * @returns An Effect that resolves to a Stream of the resized video bytes
140
+ * @throws {UploadistaError} When video resizing fails
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * const program = Effect.gen(function* () {
145
+ * const videoPlugin = yield* VideoPlugin;
146
+ * const inputStream = yield* dataStore.readStream(fileId);
147
+ * const outputStream = yield* videoPlugin.resizeStream(inputStream, {
148
+ * width: 1280,
149
+ * height: 720,
150
+ * aspectRatio: "keep"
151
+ * });
152
+ * return outputStream;
153
+ * });
154
+ * ```
155
+ */
156
+ resizeStream?: (
157
+ input: VideoStreamInput,
158
+ options: ResizeVideoParams,
159
+ streamOptions?: VideoStreamOptions,
160
+ ) => Effect.Effect<Stream.Stream<Uint8Array, UploadistaError>, UploadistaError>;
161
+
162
+ /**
163
+ * Trims a video using streaming for memory-efficient processing of large files.
164
+ *
165
+ * This method outputs the trimmed video as a stream, reducing peak memory usage.
166
+ * For input, it accepts either a buffered Uint8Array or a Stream. Streaming input
167
+ * is only supported for MPEG-TS format; other formats will be buffered internally.
168
+ *
169
+ * @param input - The input video as Uint8Array or Stream (MPEG-TS only for streaming)
170
+ * @param options - Trim parameters including start time and end time/duration
171
+ * @param streamOptions - Optional streaming configuration including input format hint
172
+ * @returns An Effect that resolves to a Stream of the trimmed video bytes
173
+ * @throws {UploadistaError} When video trimming fails
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * const program = Effect.gen(function* () {
178
+ * const videoPlugin = yield* VideoPlugin;
179
+ * const inputStream = yield* dataStore.readStream(fileId);
180
+ * const outputStream = yield* videoPlugin.trimStream(inputStream, {
181
+ * startTime: 10,
182
+ * endTime: 30
183
+ * });
184
+ * return outputStream;
185
+ * });
186
+ * ```
187
+ */
188
+ trimStream?: (
189
+ input: VideoStreamInput,
190
+ options: TrimVideoParams,
191
+ streamOptions?: VideoStreamOptions,
192
+ ) => Effect.Effect<Stream.Stream<Uint8Array, UploadistaError>, UploadistaError>;
193
+
194
+ /**
195
+ * Indicates whether this plugin supports streaming operations.
196
+ * Returns true if streaming methods are available and functional.
197
+ */
198
+ supportsStreaming?: boolean;
76
199
  };
77
200
 
78
201
  /**