@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.
- package/dist/{checksum-p3NmuAky.cjs → checksum-DVPe3Db4.cjs} +1 -1
- package/dist/errors/index.cjs +1 -1
- package/dist/errors/index.d.cts +0 -1
- package/dist/flow/index.cjs +1 -1
- package/dist/flow/index.d.cts +2 -6
- package/dist/flow/index.d.mts +2 -2
- package/dist/flow/index.mjs +1 -1
- package/dist/flow-CAlAQtBK.cjs +1 -0
- package/dist/flow-DWNJ-NOU.mjs +2 -0
- package/dist/flow-DWNJ-NOU.mjs.map +1 -0
- package/dist/index-9gyMMEIB.d.cts.map +1 -1
- package/dist/{index-TokXRAZ5.d.mts → index-B3_9v6Z8.d.mts} +494 -36
- package/dist/index-B3_9v6Z8.d.mts.map +1 -0
- package/dist/{index-BOic6-Cg.d.cts → index-br6o9tCI.d.cts} +494 -36
- package/dist/index-br6o9tCI.d.cts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +2 -3
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/{stream-limiter-Cem7Zvaw.cjs → stream-limiter-BvkaZXcz.cjs} +1 -1
- package/dist/streams/index.cjs +1 -1
- package/dist/streams/index.d.cts +0 -1
- package/dist/testing/index.cjs +2 -2
- package/dist/testing/index.d.cts +1 -5
- package/dist/testing/index.d.cts.map +1 -1
- package/dist/testing/index.d.mts +1 -1
- package/dist/testing/index.d.mts.map +1 -1
- package/dist/testing/index.mjs +3 -3
- package/dist/testing/index.mjs.map +1 -1
- package/dist/types/index.cjs +1 -1
- package/dist/types/index.d.cts +2 -6
- package/dist/types/index.d.mts +2 -2
- package/dist/types/index.mjs +1 -1
- package/dist/types-Cws60JHC.cjs +1 -0
- package/dist/types-DKGQJIEr.mjs +2 -0
- package/dist/types-DKGQJIEr.mjs.map +1 -0
- package/dist/upload/index.cjs +1 -1
- package/dist/upload/index.d.cts +1 -5
- package/dist/upload/index.d.mts +1 -1
- package/dist/upload/index.mjs +1 -1
- package/dist/{upload-5l3utoc7.cjs → upload-BHDuuJ80.cjs} +1 -1
- package/dist/{upload-B2RDFkTe.mjs → upload-tLC7uR9U.mjs} +2 -2
- package/dist/upload-tLC7uR9U.mjs.map +1 -0
- package/dist/{uploadista-error-BfpQ4mOO.cjs → uploadista-error-BgQU45we.cjs} +1 -1
- package/dist/utils/index.cjs +1 -1
- package/dist/utils/index.d.cts +0 -1
- package/dist/{utils-QJOPnlmt.cjs → utils-UUJt8ILJ.cjs} +1 -1
- package/package.json +3 -3
- package/src/flow/index.ts +10 -0
- package/src/flow/nodes/transform-node.ts +321 -29
- package/src/flow/plugins/image-plugin.ts +101 -1
- package/src/flow/plugins/video-plugin.ts +124 -1
- package/src/testing/mock-upload-server.ts +81 -2
- package/src/types/data-store.ts +157 -0
- package/src/types/input-file.ts +47 -21
- package/src/upload/upload-server.ts +234 -1
- package/dist/flow-DKCp_0Y1.mjs +0 -2
- package/dist/flow-DKCp_0Y1.mjs.map +0 -1
- package/dist/flow-NHkTGTxu.cjs +0 -1
- package/dist/index-BOic6-Cg.d.cts.map +0 -1
- package/dist/index-TokXRAZ5.d.mts.map +0 -1
- package/dist/types-CHbyV8e6.mjs +0 -2
- package/dist/types-CHbyV8e6.mjs.map +0 -1
- package/dist/types-D3_rWxD0.cjs +0 -1
- 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
|
-
/**
|
|
64
|
-
|
|
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
|
|
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
|
-
* //
|
|
99
|
-
* const
|
|
100
|
-
* id: "
|
|
101
|
-
* name: "
|
|
102
|
-
* description: "
|
|
103
|
-
*
|
|
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
|
-
* //
|
|
110
|
-
* const
|
|
111
|
-
* id: "
|
|
112
|
-
* name: "
|
|
113
|
-
* description: "
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
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
|
/**
|