@uploadista/core 0.0.2
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/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-check.log +231 -0
- package/.turbo/turbo-format.log +5 -0
- package/LICENSE +21 -0
- package/README.md +1120 -0
- package/dist/chunk-CUT6urMc.cjs +1 -0
- package/dist/debounce-C2SeqcxD.js +2 -0
- package/dist/debounce-C2SeqcxD.js.map +1 -0
- package/dist/debounce-LZK7yS7Z.cjs +1 -0
- package/dist/errors/index.cjs +1 -0
- package/dist/errors/index.d.cts +3 -0
- package/dist/errors/index.d.ts +3 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +2 -0
- package/dist/errors/uploadista-error.d.ts +209 -0
- package/dist/errors/uploadista-error.d.ts.map +1 -0
- package/dist/errors/uploadista-error.js +322 -0
- package/dist/errors-8i_aMxOE.js +1 -0
- package/dist/errors-CRm1FHHT.cjs +0 -0
- package/dist/flow/edge.d.ts +47 -0
- package/dist/flow/edge.d.ts.map +1 -0
- package/dist/flow/edge.js +40 -0
- package/dist/flow/event.d.ts +206 -0
- package/dist/flow/event.d.ts.map +1 -0
- package/dist/flow/event.js +53 -0
- package/dist/flow/flow-server.d.ts +223 -0
- package/dist/flow/flow-server.d.ts.map +1 -0
- package/dist/flow/flow-server.js +614 -0
- package/dist/flow/flow.d.ts +238 -0
- package/dist/flow/flow.d.ts.map +1 -0
- package/dist/flow/flow.js +629 -0
- package/dist/flow/index.cjs +1 -0
- package/dist/flow/index.d.cts +6 -0
- package/dist/flow/index.d.ts +24 -0
- package/dist/flow/index.d.ts.map +1 -0
- package/dist/flow/index.js +24 -0
- package/dist/flow/node.d.ts +136 -0
- package/dist/flow/node.d.ts.map +1 -0
- package/dist/flow/node.js +153 -0
- package/dist/flow/nodes/index.d.ts +8 -0
- package/dist/flow/nodes/index.d.ts.map +1 -0
- package/dist/flow/nodes/index.js +7 -0
- package/dist/flow/nodes/input-node.d.ts +78 -0
- package/dist/flow/nodes/input-node.d.ts.map +1 -0
- package/dist/flow/nodes/input-node.js +233 -0
- package/dist/flow/nodes/storage-node.d.ts +67 -0
- package/dist/flow/nodes/storage-node.d.ts.map +1 -0
- package/dist/flow/nodes/storage-node.js +94 -0
- package/dist/flow/nodes/streaming-input-node.d.ts +69 -0
- package/dist/flow/nodes/streaming-input-node.d.ts.map +1 -0
- package/dist/flow/nodes/streaming-input-node.js +156 -0
- package/dist/flow/nodes/transform-node.d.ts +85 -0
- package/dist/flow/nodes/transform-node.d.ts.map +1 -0
- package/dist/flow/nodes/transform-node.js +107 -0
- package/dist/flow/parallel-scheduler.d.ts +175 -0
- package/dist/flow/parallel-scheduler.d.ts.map +1 -0
- package/dist/flow/parallel-scheduler.js +193 -0
- package/dist/flow/plugins/credential-provider.d.ts +47 -0
- package/dist/flow/plugins/credential-provider.d.ts.map +1 -0
- package/dist/flow/plugins/credential-provider.js +24 -0
- package/dist/flow/plugins/image-ai-plugin.d.ts +61 -0
- package/dist/flow/plugins/image-ai-plugin.d.ts.map +1 -0
- package/dist/flow/plugins/image-ai-plugin.js +21 -0
- package/dist/flow/plugins/image-plugin.d.ts +52 -0
- package/dist/flow/plugins/image-plugin.d.ts.map +1 -0
- package/dist/flow/plugins/image-plugin.js +22 -0
- package/dist/flow/plugins/types/describe-image-node.d.ts +16 -0
- package/dist/flow/plugins/types/describe-image-node.d.ts.map +1 -0
- package/dist/flow/plugins/types/describe-image-node.js +9 -0
- package/dist/flow/plugins/types/index.d.ts +9 -0
- package/dist/flow/plugins/types/index.d.ts.map +1 -0
- package/dist/flow/plugins/types/index.js +8 -0
- package/dist/flow/plugins/types/optimize-node.d.ts +20 -0
- package/dist/flow/plugins/types/optimize-node.d.ts.map +1 -0
- package/dist/flow/plugins/types/optimize-node.js +11 -0
- package/dist/flow/plugins/types/remove-background-node.d.ts +16 -0
- package/dist/flow/plugins/types/remove-background-node.d.ts.map +1 -0
- package/dist/flow/plugins/types/remove-background-node.js +9 -0
- package/dist/flow/plugins/types/resize-node.d.ts +21 -0
- package/dist/flow/plugins/types/resize-node.d.ts.map +1 -0
- package/dist/flow/plugins/types/resize-node.js +16 -0
- package/dist/flow/plugins/zip-plugin.d.ts +62 -0
- package/dist/flow/plugins/zip-plugin.d.ts.map +1 -0
- package/dist/flow/plugins/zip-plugin.js +21 -0
- package/dist/flow/typed-flow.d.ts +90 -0
- package/dist/flow/typed-flow.d.ts.map +1 -0
- package/dist/flow/typed-flow.js +59 -0
- package/dist/flow/types/flow-file.d.ts +45 -0
- package/dist/flow/types/flow-file.d.ts.map +1 -0
- package/dist/flow/types/flow-file.js +27 -0
- package/dist/flow/types/flow-job.d.ts +118 -0
- package/dist/flow/types/flow-job.d.ts.map +1 -0
- package/dist/flow/types/flow-job.js +11 -0
- package/dist/flow/types/flow-types.d.ts +321 -0
- package/dist/flow/types/flow-types.d.ts.map +1 -0
- package/dist/flow/types/flow-types.js +52 -0
- package/dist/flow/types/index.d.ts +4 -0
- package/dist/flow/types/index.d.ts.map +1 -0
- package/dist/flow/types/index.js +3 -0
- package/dist/flow/types/run-args.d.ts +38 -0
- package/dist/flow/types/run-args.d.ts.map +1 -0
- package/dist/flow/types/run-args.js +30 -0
- package/dist/flow/types/type-validator.d.ts +26 -0
- package/dist/flow/types/type-validator.d.ts.map +1 -0
- package/dist/flow/types/type-validator.js +134 -0
- package/dist/flow/utils/resolve-upload-metadata.d.ts +11 -0
- package/dist/flow/utils/resolve-upload-metadata.d.ts.map +1 -0
- package/dist/flow/utils/resolve-upload-metadata.js +28 -0
- package/dist/flow-2zXnEiWL.cjs +1 -0
- package/dist/flow-CRaKy7Vj.js +2 -0
- package/dist/flow-CRaKy7Vj.js.map +1 -0
- package/dist/generate-id-Dm-Vboxq.d.ts +34 -0
- package/dist/generate-id-Dm-Vboxq.d.ts.map +1 -0
- package/dist/generate-id-LjJRLD6N.d.cts +34 -0
- package/dist/generate-id-LjJRLD6N.d.cts.map +1 -0
- package/dist/generate-id-xHp_Z7Cl.cjs +1 -0
- package/dist/generate-id-yohS1ZDk.js +2 -0
- package/dist/generate-id-yohS1ZDk.js.map +1 -0
- package/dist/index-BO8GZlbD.d.cts +1040 -0
- package/dist/index-BO8GZlbD.d.cts.map +1 -0
- package/dist/index-BoGG5KAY.d.ts +1 -0
- package/dist/index-BtBZHVmz.d.cts +1 -0
- package/dist/index-D-CoVpkZ.d.ts +1004 -0
- package/dist/index-D-CoVpkZ.d.ts.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/logger/logger.cjs +1 -0
- package/dist/logger/logger.d.cts +8 -0
- package/dist/logger/logger.d.cts.map +1 -0
- package/dist/logger/logger.d.ts +5 -0
- package/dist/logger/logger.d.ts.map +1 -0
- package/dist/logger/logger.js +10 -0
- package/dist/logger/logger.js.map +1 -0
- package/dist/semaphore-0ZwjVpyF.js +2 -0
- package/dist/semaphore-0ZwjVpyF.js.map +1 -0
- package/dist/semaphore-BHprIjFI.d.cts +37 -0
- package/dist/semaphore-BHprIjFI.d.cts.map +1 -0
- package/dist/semaphore-DThupBkc.d.ts +37 -0
- package/dist/semaphore-DThupBkc.d.ts.map +1 -0
- package/dist/semaphore-DVrONiAV.cjs +1 -0
- package/dist/stream-limiter-CoWKv39w.js +2 -0
- package/dist/stream-limiter-CoWKv39w.js.map +1 -0
- package/dist/stream-limiter-JgOwmkMa.cjs +1 -0
- package/dist/streams/multi-stream.cjs +1 -0
- package/dist/streams/multi-stream.d.cts +91 -0
- package/dist/streams/multi-stream.d.cts.map +1 -0
- package/dist/streams/multi-stream.d.ts +86 -0
- package/dist/streams/multi-stream.d.ts.map +1 -0
- package/dist/streams/multi-stream.js +149 -0
- package/dist/streams/multi-stream.js.map +1 -0
- package/dist/streams/stream-limiter.cjs +1 -0
- package/dist/streams/stream-limiter.d.cts +36 -0
- package/dist/streams/stream-limiter.d.cts.map +1 -0
- package/dist/streams/stream-limiter.d.ts +27 -0
- package/dist/streams/stream-limiter.d.ts.map +1 -0
- package/dist/streams/stream-limiter.js +49 -0
- package/dist/streams/stream-splitter.cjs +1 -0
- package/dist/streams/stream-splitter.d.cts +68 -0
- package/dist/streams/stream-splitter.d.cts.map +1 -0
- package/dist/streams/stream-splitter.d.ts +51 -0
- package/dist/streams/stream-splitter.d.ts.map +1 -0
- package/dist/streams/stream-splitter.js +175 -0
- package/dist/streams/stream-splitter.js.map +1 -0
- package/dist/types/data-store-registry.d.ts +13 -0
- package/dist/types/data-store-registry.d.ts.map +1 -0
- package/dist/types/data-store-registry.js +4 -0
- package/dist/types/data-store.d.ts +316 -0
- package/dist/types/data-store.d.ts.map +1 -0
- package/dist/types/data-store.js +157 -0
- package/dist/types/event-broadcaster.d.ts +28 -0
- package/dist/types/event-broadcaster.d.ts.map +1 -0
- package/dist/types/event-broadcaster.js +6 -0
- package/dist/types/event-emitter.d.ts +378 -0
- package/dist/types/event-emitter.d.ts.map +1 -0
- package/dist/types/event-emitter.js +223 -0
- package/dist/types/index.cjs +1 -0
- package/dist/types/index.d.cts +6 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +9 -0
- package/dist/types/input-file.d.ts +104 -0
- package/dist/types/input-file.d.ts.map +1 -0
- package/dist/types/input-file.js +27 -0
- package/dist/types/kv-store.d.ts +281 -0
- package/dist/types/kv-store.d.ts.map +1 -0
- package/dist/types/kv-store.js +234 -0
- package/dist/types/middleware.d.ts +17 -0
- package/dist/types/middleware.d.ts.map +1 -0
- package/dist/types/middleware.js +21 -0
- package/dist/types/upload-event.d.ts +105 -0
- package/dist/types/upload-event.d.ts.map +1 -0
- package/dist/types/upload-event.js +71 -0
- package/dist/types/upload-file.d.ts +136 -0
- package/dist/types/upload-file.d.ts.map +1 -0
- package/dist/types/upload-file.js +34 -0
- package/dist/types/websocket.d.ts +144 -0
- package/dist/types/websocket.d.ts.map +1 -0
- package/dist/types/websocket.js +40 -0
- package/dist/types-BT-cvi7T.cjs +1 -0
- package/dist/types-DhU2j-XF.js +2 -0
- package/dist/types-DhU2j-XF.js.map +1 -0
- package/dist/upload/convert-to-stream.d.ts +38 -0
- package/dist/upload/convert-to-stream.d.ts.map +1 -0
- package/dist/upload/convert-to-stream.js +43 -0
- package/dist/upload/convert-upload-to-flow-file.d.ts +14 -0
- package/dist/upload/convert-upload-to-flow-file.d.ts.map +1 -0
- package/dist/upload/convert-upload-to-flow-file.js +21 -0
- package/dist/upload/create-upload.d.ts +68 -0
- package/dist/upload/create-upload.d.ts.map +1 -0
- package/dist/upload/create-upload.js +157 -0
- package/dist/upload/index.cjs +1 -0
- package/dist/upload/index.d.cts +6 -0
- package/dist/upload/index.d.ts +4 -0
- package/dist/upload/index.d.ts.map +1 -0
- package/dist/upload/index.js +3 -0
- package/dist/upload/mime.d.ts +24 -0
- package/dist/upload/mime.d.ts.map +1 -0
- package/dist/upload/mime.js +351 -0
- package/dist/upload/upload-chunk.d.ts +58 -0
- package/dist/upload/upload-chunk.d.ts.map +1 -0
- package/dist/upload/upload-chunk.js +277 -0
- package/dist/upload/upload-server.d.ts +221 -0
- package/dist/upload/upload-server.d.ts.map +1 -0
- package/dist/upload/upload-server.js +181 -0
- package/dist/upload/upload-strategy-negotiator.d.ts +148 -0
- package/dist/upload/upload-strategy-negotiator.d.ts.map +1 -0
- package/dist/upload/upload-strategy-negotiator.js +217 -0
- package/dist/upload/upload-url.d.ts +68 -0
- package/dist/upload/upload-url.d.ts.map +1 -0
- package/dist/upload/upload-url.js +142 -0
- package/dist/upload/write-to-store.d.ts +77 -0
- package/dist/upload/write-to-store.d.ts.map +1 -0
- package/dist/upload/write-to-store.js +147 -0
- package/dist/upload-DLuICjpP.cjs +1 -0
- package/dist/upload-DaXO34dE.js +2 -0
- package/dist/upload-DaXO34dE.js.map +1 -0
- package/dist/uploadista-error-BB-Wdiz9.cjs +22 -0
- package/dist/uploadista-error-BVsVxqvz.js +23 -0
- package/dist/uploadista-error-BVsVxqvz.js.map +1 -0
- package/dist/uploadista-error-CwxYs4EB.d.ts +52 -0
- package/dist/uploadista-error-CwxYs4EB.d.ts.map +1 -0
- package/dist/uploadista-error-kKlhLRhY.d.cts +52 -0
- package/dist/uploadista-error-kKlhLRhY.d.cts.map +1 -0
- package/dist/utils/checksum.d.ts +22 -0
- package/dist/utils/checksum.d.ts.map +1 -0
- package/dist/utils/checksum.js +49 -0
- package/dist/utils/debounce.cjs +1 -0
- package/dist/utils/debounce.d.cts +38 -0
- package/dist/utils/debounce.d.cts.map +1 -0
- package/dist/utils/debounce.d.ts +36 -0
- package/dist/utils/debounce.d.ts.map +1 -0
- package/dist/utils/debounce.js +73 -0
- package/dist/utils/generate-id.cjs +1 -0
- package/dist/utils/generate-id.d.cts +2 -0
- package/dist/utils/generate-id.d.ts +32 -0
- package/dist/utils/generate-id.d.ts.map +1 -0
- package/dist/utils/generate-id.js +23 -0
- package/dist/utils/md5.cjs +1 -0
- package/dist/utils/md5.d.cts +73 -0
- package/dist/utils/md5.d.cts.map +1 -0
- package/dist/utils/md5.d.ts +71 -0
- package/dist/utils/md5.d.ts.map +1 -0
- package/dist/utils/md5.js +417 -0
- package/dist/utils/md5.js.map +1 -0
- package/dist/utils/once.cjs +1 -0
- package/dist/utils/once.d.cts +25 -0
- package/dist/utils/once.d.cts.map +1 -0
- package/dist/utils/once.d.ts +21 -0
- package/dist/utils/once.d.ts.map +1 -0
- package/dist/utils/once.js +54 -0
- package/dist/utils/once.js.map +1 -0
- package/dist/utils/semaphore.cjs +1 -0
- package/dist/utils/semaphore.d.cts +3 -0
- package/dist/utils/semaphore.d.ts +78 -0
- package/dist/utils/semaphore.d.ts.map +1 -0
- package/dist/utils/semaphore.js +134 -0
- package/dist/utils/throttle.cjs +1 -0
- package/dist/utils/throttle.d.cts +24 -0
- package/dist/utils/throttle.d.cts.map +1 -0
- package/dist/utils/throttle.d.ts +18 -0
- package/dist/utils/throttle.d.ts.map +1 -0
- package/dist/utils/throttle.js +20 -0
- package/dist/utils/throttle.js.map +1 -0
- package/docs/PARALLEL_EXECUTION.md +206 -0
- package/docs/PARALLEL_EXECUTION_QUICKSTART.md +142 -0
- package/docs/PARALLEL_EXECUTION_REFACTOR.md +184 -0
- package/package.json +80 -0
- package/src/errors/__tests__/uploadista-error.test.ts +251 -0
- package/src/errors/index.ts +2 -0
- package/src/errors/uploadista-error.ts +394 -0
- package/src/flow/README.md +352 -0
- package/src/flow/edge.test.ts +146 -0
- package/src/flow/edge.ts +60 -0
- package/src/flow/event.ts +229 -0
- package/src/flow/flow-server.ts +1089 -0
- package/src/flow/flow.ts +1050 -0
- package/src/flow/index.ts +28 -0
- package/src/flow/node.ts +249 -0
- package/src/flow/nodes/index.ts +8 -0
- package/src/flow/nodes/input-node.ts +296 -0
- package/src/flow/nodes/storage-node.ts +128 -0
- package/src/flow/nodes/transform-node.ts +154 -0
- package/src/flow/parallel-scheduler.ts +259 -0
- package/src/flow/plugins/credential-provider.ts +48 -0
- package/src/flow/plugins/image-ai-plugin.ts +66 -0
- package/src/flow/plugins/image-plugin.ts +60 -0
- package/src/flow/plugins/types/describe-image-node.ts +16 -0
- package/src/flow/plugins/types/index.ts +9 -0
- package/src/flow/plugins/types/optimize-node.ts +18 -0
- package/src/flow/plugins/types/remove-background-node.ts +18 -0
- package/src/flow/plugins/types/resize-node.ts +26 -0
- package/src/flow/plugins/zip-plugin.ts +69 -0
- package/src/flow/typed-flow.ts +279 -0
- package/src/flow/types/flow-file.ts +51 -0
- package/src/flow/types/flow-job.ts +138 -0
- package/src/flow/types/flow-types.ts +353 -0
- package/src/flow/types/index.ts +6 -0
- package/src/flow/types/run-args.ts +40 -0
- package/src/flow/types/type-validator.ts +204 -0
- package/src/flow/utils/resolve-upload-metadata.ts +48 -0
- package/src/index.ts +5 -0
- package/src/logger/logger.ts +14 -0
- package/src/streams/stream-limiter.test.ts +150 -0
- package/src/streams/stream-limiter.ts +75 -0
- package/src/types/data-store.ts +427 -0
- package/src/types/event-broadcaster.ts +39 -0
- package/src/types/event-emitter.ts +349 -0
- package/src/types/index.ts +9 -0
- package/src/types/input-file.ts +107 -0
- package/src/types/kv-store.ts +375 -0
- package/src/types/middleware.ts +54 -0
- package/src/types/upload-event.ts +75 -0
- package/src/types/upload-file.ts +139 -0
- package/src/types/websocket.ts +65 -0
- package/src/upload/convert-to-stream.ts +48 -0
- package/src/upload/create-upload.ts +214 -0
- package/src/upload/index.ts +3 -0
- package/src/upload/mime.ts +436 -0
- package/src/upload/upload-chunk.ts +364 -0
- package/src/upload/upload-server.ts +390 -0
- package/src/upload/upload-strategy-negotiator.ts +316 -0
- package/src/upload/upload-url.ts +173 -0
- package/src/upload/write-to-store.ts +211 -0
- package/src/utils/checksum.ts +61 -0
- package/src/utils/debounce.test.ts +126 -0
- package/src/utils/debounce.ts +89 -0
- package/src/utils/generate-id.ts +35 -0
- package/src/utils/md5.ts +475 -0
- package/src/utils/once.test.ts +83 -0
- package/src/utils/once.ts +63 -0
- package/src/utils/throttle.test.ts +101 -0
- package/src/utils/throttle.ts +29 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.ts +25 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Effect, Metric } from "effect";
|
|
2
|
+
import { UploadistaError } from "../errors";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fetches a file from a remote URL.
|
|
6
|
+
*
|
|
7
|
+
* This function handles HTTP requests to remote URLs for file uploads,
|
|
8
|
+
* including proper error handling, metrics tracking, and observability.
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - HTTP request with proper error handling
|
|
12
|
+
* - Effect tracing for performance monitoring
|
|
13
|
+
* - Metrics tracking for URL-based uploads
|
|
14
|
+
* - Structured logging for debugging
|
|
15
|
+
* - Response validation and error reporting
|
|
16
|
+
*
|
|
17
|
+
* @param url - The remote URL to fetch the file from
|
|
18
|
+
* @returns Effect that yields the Response object
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* // Fetch a file from URL
|
|
23
|
+
* const fetchEffect = fetchFile("https://example.com/image.jpg");
|
|
24
|
+
*
|
|
25
|
+
* // Run with error handling
|
|
26
|
+
* const response = await Effect.runPromise(
|
|
27
|
+
* fetchEffect.pipe(
|
|
28
|
+
* Effect.catchAll((error) =>
|
|
29
|
+
* Effect.logError("Failed to fetch file").pipe(
|
|
30
|
+
* Effect.andThen(Effect.fail(error))
|
|
31
|
+
* )
|
|
32
|
+
* )
|
|
33
|
+
* )
|
|
34
|
+
* );
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export const fetchFile = (url: string) => {
|
|
38
|
+
return Effect.tryPromise({
|
|
39
|
+
try: async () => {
|
|
40
|
+
return await fetch(url);
|
|
41
|
+
},
|
|
42
|
+
catch: (error) => {
|
|
43
|
+
return UploadistaError.fromCode("UNKNOWN_ERROR", {
|
|
44
|
+
cause: error,
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
}).pipe(
|
|
48
|
+
// Add tracing span for URL fetch
|
|
49
|
+
Effect.withSpan("upload-fetch-url", {
|
|
50
|
+
attributes: {
|
|
51
|
+
"upload.url": url,
|
|
52
|
+
"upload.operation": "fetch",
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
// Track URL fetch metrics
|
|
56
|
+
Effect.tap((response) =>
|
|
57
|
+
Effect.gen(function* () {
|
|
58
|
+
// Increment URL upload counter
|
|
59
|
+
yield* Metric.increment(
|
|
60
|
+
Metric.counter("upload_from_url_total", {
|
|
61
|
+
description: "Total number of URL-based uploads",
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Track success/failure
|
|
66
|
+
if (response.ok) {
|
|
67
|
+
yield* Metric.increment(
|
|
68
|
+
Metric.counter("upload_from_url_success_total", {
|
|
69
|
+
description: "Total number of successful URL-based uploads",
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
),
|
|
75
|
+
// Add structured logging
|
|
76
|
+
Effect.tap((response) =>
|
|
77
|
+
Effect.logInfo("URL fetch completed").pipe(
|
|
78
|
+
Effect.annotateLogs({
|
|
79
|
+
"upload.url": url,
|
|
80
|
+
"response.status": response.status.toString(),
|
|
81
|
+
"response.ok": response.ok.toString(),
|
|
82
|
+
"response.content_length":
|
|
83
|
+
response.headers.get("content-length") ?? "unknown",
|
|
84
|
+
})
|
|
85
|
+
)
|
|
86
|
+
),
|
|
87
|
+
// Handle errors with logging and metrics
|
|
88
|
+
Effect.tapError((error) =>
|
|
89
|
+
Effect.gen(function* () {
|
|
90
|
+
// Track failed URL upload
|
|
91
|
+
yield* Metric.increment(
|
|
92
|
+
Metric.counter("upload_from_url_failed_total", {
|
|
93
|
+
description: "Total number of failed URL-based uploads",
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Log error
|
|
98
|
+
yield* Effect.logError("URL fetch failed").pipe(
|
|
99
|
+
Effect.annotateLogs({
|
|
100
|
+
"upload.url": url,
|
|
101
|
+
error: String(error),
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
})
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Converts a Response object to an ArrayBuffer.
|
|
111
|
+
*
|
|
112
|
+
* This function safely converts HTTP response data to binary format
|
|
113
|
+
* for processing and storage, with proper error handling and observability.
|
|
114
|
+
*
|
|
115
|
+
* Features:
|
|
116
|
+
* - Safe conversion from Response to ArrayBuffer
|
|
117
|
+
* - Effect tracing for performance monitoring
|
|
118
|
+
* - Structured logging for debugging
|
|
119
|
+
* - Error handling with proper UploadistaError types
|
|
120
|
+
*
|
|
121
|
+
* @param response - The HTTP Response object to convert
|
|
122
|
+
* @returns Effect that yields the ArrayBuffer data
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* // Convert response to buffer
|
|
127
|
+
* const bufferEffect = arrayBuffer(response);
|
|
128
|
+
*
|
|
129
|
+
* // Use in upload pipeline
|
|
130
|
+
* const buffer = await Effect.runPromise(
|
|
131
|
+
* bufferEffect.pipe(
|
|
132
|
+
* Effect.tap((buffer) =>
|
|
133
|
+
* Effect.logInfo(`Buffer size: ${buffer.byteLength} bytes`)
|
|
134
|
+
* )
|
|
135
|
+
* )
|
|
136
|
+
* );
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
export const arrayBuffer = (response: Response) => {
|
|
140
|
+
return Effect.tryPromise({
|
|
141
|
+
try: async () => {
|
|
142
|
+
return await response.arrayBuffer();
|
|
143
|
+
},
|
|
144
|
+
catch: (error) => {
|
|
145
|
+
return UploadistaError.fromCode("UNKNOWN_ERROR", {
|
|
146
|
+
cause: error,
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
}).pipe(
|
|
150
|
+
// Add tracing span for buffer conversion
|
|
151
|
+
Effect.withSpan("upload-convert-to-buffer", {
|
|
152
|
+
attributes: {
|
|
153
|
+
"upload.operation": "arrayBuffer",
|
|
154
|
+
},
|
|
155
|
+
}),
|
|
156
|
+
// Add structured logging
|
|
157
|
+
Effect.tap((buffer) =>
|
|
158
|
+
Effect.logDebug("Response converted to array buffer").pipe(
|
|
159
|
+
Effect.annotateLogs({
|
|
160
|
+
"buffer.size": buffer.byteLength.toString(),
|
|
161
|
+
})
|
|
162
|
+
)
|
|
163
|
+
),
|
|
164
|
+
// Handle errors with logging
|
|
165
|
+
Effect.tapError((error) =>
|
|
166
|
+
Effect.logError("Failed to convert response to array buffer").pipe(
|
|
167
|
+
Effect.annotateLogs({
|
|
168
|
+
error: String(error),
|
|
169
|
+
})
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { Effect, Ref } from "effect";
|
|
2
|
+
import { UploadistaError } from "../errors";
|
|
3
|
+
import { StreamLimiterEffect } from "../streams/stream-limiter";
|
|
4
|
+
import type { DataStore, UploadEvent, UploadFile } from "../types";
|
|
5
|
+
import { type EventEmitter, UploadEventType } from "../types";
|
|
6
|
+
import { convertToStream } from "./convert-to-stream";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Configuration options for writing data to a data store.
|
|
10
|
+
*
|
|
11
|
+
* @property data - The stream of data to write
|
|
12
|
+
* @property upload - Upload file metadata
|
|
13
|
+
* @property dataStore - Target data store for writing
|
|
14
|
+
* @property maxFileSize - Maximum allowed file size in bytes
|
|
15
|
+
* @property controller - AbortController for cancellation
|
|
16
|
+
* @property eventEmitter - Event emitter for progress tracking
|
|
17
|
+
* @property uploadProgressInterval - Progress emission interval in milliseconds (default: 200)
|
|
18
|
+
*/
|
|
19
|
+
type WriteToStoreOptions = {
|
|
20
|
+
data: ReadableStream<Uint8Array>;
|
|
21
|
+
upload: UploadFile;
|
|
22
|
+
dataStore: DataStore<UploadFile>;
|
|
23
|
+
maxFileSize: number;
|
|
24
|
+
controller: AbortController;
|
|
25
|
+
eventEmitter: EventEmitter<UploadEvent>;
|
|
26
|
+
uploadProgressInterval?: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Writes data stream to a data store with progress tracking and size limits.
|
|
31
|
+
*
|
|
32
|
+
* This function handles the core data writing logic including:
|
|
33
|
+
* - Stream conversion and processing
|
|
34
|
+
* - File size validation and limiting
|
|
35
|
+
* - Progress tracking with throttled events
|
|
36
|
+
* - Abort signal handling for cancellation
|
|
37
|
+
* - Error handling and cleanup
|
|
38
|
+
*
|
|
39
|
+
* The function includes comprehensive observability with:
|
|
40
|
+
* - Effect tracing spans for performance monitoring
|
|
41
|
+
* - Structured logging for debugging and monitoring
|
|
42
|
+
* - Progress event emission with throttling
|
|
43
|
+
* - Error handling with proper UploadistaError types
|
|
44
|
+
*
|
|
45
|
+
* @param data - The stream of data to write to storage
|
|
46
|
+
* @param upload - Upload file metadata containing ID, offset, etc.
|
|
47
|
+
* @param dataStore - Target data store for writing the data
|
|
48
|
+
* @param maxFileSize - Maximum allowed file size in bytes
|
|
49
|
+
* @param controller - AbortController for handling cancellation
|
|
50
|
+
* @param eventEmitter - Event emitter for progress tracking
|
|
51
|
+
* @param uploadProgressInterval - Progress emission interval in milliseconds (default: 200)
|
|
52
|
+
* @returns Effect that yields the number of bytes written
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* // Write data to store with progress tracking
|
|
57
|
+
* const writeEffect = writeToStore({
|
|
58
|
+
* data: fileStream,
|
|
59
|
+
* upload: uploadMetadata,
|
|
60
|
+
* dataStore: s3DataStore,
|
|
61
|
+
* maxFileSize: 100_000_000, // 100MB
|
|
62
|
+
* controller: abortController,
|
|
63
|
+
* eventEmitter: progressEmitter,
|
|
64
|
+
* uploadProgressInterval: 500 // Emit progress every 500ms
|
|
65
|
+
* });
|
|
66
|
+
*
|
|
67
|
+
* // Run with error handling
|
|
68
|
+
* const bytesWritten = await Effect.runPromise(
|
|
69
|
+
* writeEffect.pipe(
|
|
70
|
+
* Effect.catchAll((error) =>
|
|
71
|
+
* Effect.logError("Failed to write to store").pipe(
|
|
72
|
+
* Effect.andThen(Effect.fail(error))
|
|
73
|
+
* )
|
|
74
|
+
* )
|
|
75
|
+
* )
|
|
76
|
+
* );
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function writeToStore({
|
|
80
|
+
data,
|
|
81
|
+
upload,
|
|
82
|
+
dataStore,
|
|
83
|
+
maxFileSize,
|
|
84
|
+
controller,
|
|
85
|
+
eventEmitter,
|
|
86
|
+
uploadProgressInterval = 200,
|
|
87
|
+
}: WriteToStoreOptions) {
|
|
88
|
+
return Effect.gen(function* () {
|
|
89
|
+
const stream = convertToStream(data);
|
|
90
|
+
// Check if already aborted
|
|
91
|
+
if (controller.signal.aborted) {
|
|
92
|
+
return yield* Effect.fail(UploadistaError.fromCode("ABORTED"));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Create an AbortController to manage the stream pipeline
|
|
96
|
+
const abortController = new AbortController();
|
|
97
|
+
const { signal } = abortController;
|
|
98
|
+
|
|
99
|
+
// Set up abort handling
|
|
100
|
+
const onAbort = () => {
|
|
101
|
+
// stream.cancel();
|
|
102
|
+
abortController.abort();
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
106
|
+
|
|
107
|
+
return yield* Effect.acquireUseRelease(
|
|
108
|
+
Effect.sync(() => ({ signal, onAbort })),
|
|
109
|
+
({ signal: _signal }) =>
|
|
110
|
+
Effect.gen(function* () {
|
|
111
|
+
// Create a ref to track the last progress emission time for throttling
|
|
112
|
+
const lastEmitTime = yield* Ref.make(0);
|
|
113
|
+
|
|
114
|
+
// Create the stream limiter
|
|
115
|
+
const limiter = StreamLimiterEffect.limit({
|
|
116
|
+
maxSize: maxFileSize,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Pipe the data through the limiter
|
|
120
|
+
const limitedStream = limiter(stream);
|
|
121
|
+
|
|
122
|
+
// Write to the data store with progress tracking
|
|
123
|
+
const offset = yield* dataStore.write(
|
|
124
|
+
{
|
|
125
|
+
stream: limitedStream,
|
|
126
|
+
file_id: upload.id,
|
|
127
|
+
offset: upload.offset,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
onProgress: (newOffset: number) => {
|
|
131
|
+
// Simple throttling using timestamp check
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
Ref.get(lastEmitTime)
|
|
134
|
+
.pipe(
|
|
135
|
+
Effect.flatMap((lastTime) => {
|
|
136
|
+
if (now - lastTime >= uploadProgressInterval) {
|
|
137
|
+
return Effect.gen(function* () {
|
|
138
|
+
yield* Ref.set(lastEmitTime, now);
|
|
139
|
+
yield* eventEmitter.emit(upload.id, {
|
|
140
|
+
type: UploadEventType.UPLOAD_PROGRESS,
|
|
141
|
+
data: {
|
|
142
|
+
id: upload.id,
|
|
143
|
+
progress: newOffset,
|
|
144
|
+
total: upload.size ?? 0,
|
|
145
|
+
},
|
|
146
|
+
flow: upload.flow,
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return Effect.void;
|
|
151
|
+
}),
|
|
152
|
+
Effect.runPromise
|
|
153
|
+
)
|
|
154
|
+
.catch(() => {
|
|
155
|
+
// Ignore errors during progress emission
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return offset;
|
|
162
|
+
}).pipe(
|
|
163
|
+
Effect.catchAll((error) => {
|
|
164
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
165
|
+
return Effect.fail(UploadistaError.fromCode("ABORTED"));
|
|
166
|
+
}
|
|
167
|
+
if (error instanceof UploadistaError) {
|
|
168
|
+
return Effect.fail(error);
|
|
169
|
+
}
|
|
170
|
+
return Effect.fail(
|
|
171
|
+
UploadistaError.fromCode("FILE_WRITE_ERROR", { cause: error })
|
|
172
|
+
);
|
|
173
|
+
})
|
|
174
|
+
),
|
|
175
|
+
({ onAbort }) =>
|
|
176
|
+
Effect.sync(() => {
|
|
177
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
}).pipe(
|
|
181
|
+
// Add tracing span for write operation
|
|
182
|
+
Effect.withSpan("upload-write-to-store", {
|
|
183
|
+
attributes: {
|
|
184
|
+
"upload.id": upload.id,
|
|
185
|
+
"upload.offset": upload.offset.toString(),
|
|
186
|
+
"upload.max_file_size": maxFileSize.toString(),
|
|
187
|
+
"upload.file_size": upload.size?.toString() ?? "0",
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
// Add structured logging for write operation
|
|
191
|
+
Effect.tap((offset) =>
|
|
192
|
+
Effect.logDebug("Data written to store").pipe(
|
|
193
|
+
Effect.annotateLogs({
|
|
194
|
+
"upload.id": upload.id,
|
|
195
|
+
"write.offset": offset.toString(),
|
|
196
|
+
"write.bytes_written": (offset - upload.offset).toString(),
|
|
197
|
+
})
|
|
198
|
+
)
|
|
199
|
+
),
|
|
200
|
+
// Handle errors with logging
|
|
201
|
+
Effect.tapError((error) =>
|
|
202
|
+
Effect.logError("Failed to write to store").pipe(
|
|
203
|
+
Effect.annotateLogs({
|
|
204
|
+
"upload.id": upload.id,
|
|
205
|
+
"upload.offset": upload.offset.toString(),
|
|
206
|
+
error: error instanceof UploadistaError ? error.code : String(error),
|
|
207
|
+
})
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { UploadistaError } from "../errors/uploadista-error";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Supported checksum algorithms
|
|
6
|
+
*/
|
|
7
|
+
const SUPPORTED_ALGORITHMS = ["sha256"] as const;
|
|
8
|
+
export type ChecksumAlgorithm = (typeof SUPPORTED_ALGORITHMS)[number];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if a checksum algorithm is supported
|
|
12
|
+
*/
|
|
13
|
+
export function isSupportedAlgorithm(algorithm: string): algorithm is ChecksumAlgorithm {
|
|
14
|
+
return SUPPORTED_ALGORITHMS.includes(algorithm as ChecksumAlgorithm);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Compute checksum of file bytes using the Web Crypto API.
|
|
19
|
+
* This works across all modern platforms: browsers, Node.js 15+, Deno, Bun, and Cloudflare Workers.
|
|
20
|
+
*
|
|
21
|
+
* @param bytes - File content as Uint8Array
|
|
22
|
+
* @param algorithm - Hashing algorithm to use (currently only 'sha256' is supported)
|
|
23
|
+
* @returns Effect that resolves to hex-encoded checksum string
|
|
24
|
+
*/
|
|
25
|
+
export function computeChecksum(
|
|
26
|
+
bytes: Uint8Array,
|
|
27
|
+
algorithm: string,
|
|
28
|
+
): Effect.Effect<string, UploadistaError> {
|
|
29
|
+
return Effect.gen(function* () {
|
|
30
|
+
// Validate algorithm is supported
|
|
31
|
+
if (!isSupportedAlgorithm(algorithm)) {
|
|
32
|
+
return yield* UploadistaError.fromCode("UNSUPPORTED_CHECKSUM_ALGORITHM", {
|
|
33
|
+
body: `Checksum algorithm '${algorithm}' is not supported. Supported algorithms: ${SUPPORTED_ALGORITHMS.join(", ")}`,
|
|
34
|
+
details: { algorithm, supportedAlgorithms: SUPPORTED_ALGORITHMS },
|
|
35
|
+
}).toEffect();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Map algorithm name to Web Crypto API algorithm name
|
|
39
|
+
const webCryptoAlgorithm = algorithm.toUpperCase().replace(/\d+/, "-$&"); // "sha256" -> "SHA-256"
|
|
40
|
+
|
|
41
|
+
// Compute hash using Web Crypto API (available in browsers, Node.js 15+, Deno, Bun, Cloudflare Workers)
|
|
42
|
+
// Pass Uint8Array directly - it's a valid BufferSource
|
|
43
|
+
const hashBuffer = yield* Effect.tryPromise({
|
|
44
|
+
try: () => crypto.subtle.digest(webCryptoAlgorithm, bytes as BufferSource),
|
|
45
|
+
catch: (error) =>
|
|
46
|
+
UploadistaError.fromCode("UNKNOWN_ERROR", {
|
|
47
|
+
body: `Failed to compute checksum: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
48
|
+
cause: error,
|
|
49
|
+
details: { algorithm },
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Convert ArrayBuffer to hex string
|
|
54
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
55
|
+
const hashHex = hashArray
|
|
56
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
57
|
+
.join("");
|
|
58
|
+
|
|
59
|
+
return hashHex;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { debounce } from "./debounce";
|
|
3
|
+
|
|
4
|
+
describe("debounce", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.useFakeTimers();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.useRealTimers();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should delay function execution by the specified time", () => {
|
|
14
|
+
const mockFn = vi.fn();
|
|
15
|
+
const debouncedFn = debounce(mockFn, 100);
|
|
16
|
+
|
|
17
|
+
debouncedFn();
|
|
18
|
+
expect(mockFn).not.toHaveBeenCalled();
|
|
19
|
+
|
|
20
|
+
vi.advanceTimersByTime(99);
|
|
21
|
+
expect(mockFn).not.toHaveBeenCalled();
|
|
22
|
+
|
|
23
|
+
vi.advanceTimersByTime(1);
|
|
24
|
+
expect(mockFn).toHaveBeenCalledOnce();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should reset delay on subsequent calls", () => {
|
|
28
|
+
const mockFn = vi.fn();
|
|
29
|
+
const debouncedFn = debounce(mockFn, 100);
|
|
30
|
+
|
|
31
|
+
debouncedFn();
|
|
32
|
+
vi.advanceTimersByTime(50);
|
|
33
|
+
debouncedFn(); // This should reset the timer
|
|
34
|
+
|
|
35
|
+
vi.advanceTimersByTime(99);
|
|
36
|
+
expect(mockFn).not.toHaveBeenCalled();
|
|
37
|
+
|
|
38
|
+
vi.advanceTimersByTime(1);
|
|
39
|
+
expect(mockFn).toHaveBeenCalledOnce();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should call function immediately with leading: true", () => {
|
|
43
|
+
const mockFn = vi.fn();
|
|
44
|
+
const debouncedFn = debounce(mockFn, 100, { leading: true });
|
|
45
|
+
|
|
46
|
+
debouncedFn();
|
|
47
|
+
expect(mockFn).toHaveBeenCalledOnce();
|
|
48
|
+
|
|
49
|
+
vi.advanceTimersByTime(100);
|
|
50
|
+
expect(mockFn).toHaveBeenCalledOnce(); // Should not be called again
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should not call function on trailing edge with trailing: false", () => {
|
|
54
|
+
const mockFn = vi.fn();
|
|
55
|
+
const debouncedFn = debounce(mockFn, 100, { trailing: false });
|
|
56
|
+
|
|
57
|
+
debouncedFn();
|
|
58
|
+
vi.advanceTimersByTime(100);
|
|
59
|
+
expect(mockFn).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should call function on both leading and trailing edges when both options are true", () => {
|
|
63
|
+
const mockFn = vi.fn();
|
|
64
|
+
const debouncedFn = debounce(mockFn, 100, {
|
|
65
|
+
leading: true,
|
|
66
|
+
trailing: true,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
debouncedFn();
|
|
70
|
+
expect(mockFn).toHaveBeenCalledOnce();
|
|
71
|
+
|
|
72
|
+
debouncedFn(); // Second call within delay
|
|
73
|
+
vi.advanceTimersByTime(100);
|
|
74
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should preserve function arguments", () => {
|
|
78
|
+
const mockFn = vi.fn();
|
|
79
|
+
const debouncedFn = debounce(mockFn, 100);
|
|
80
|
+
|
|
81
|
+
debouncedFn("arg1", "arg2", 123);
|
|
82
|
+
vi.advanceTimersByTime(100);
|
|
83
|
+
|
|
84
|
+
expect(mockFn).toHaveBeenCalledWith("arg1", "arg2", 123);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should preserve function context (this)", () => {
|
|
88
|
+
const obj = {
|
|
89
|
+
value: 42,
|
|
90
|
+
fn: vi.fn(function (this: { value: number }) {
|
|
91
|
+
return this.value;
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const debouncedFn = debounce(obj.fn, 100);
|
|
96
|
+
debouncedFn.call(obj);
|
|
97
|
+
|
|
98
|
+
vi.advanceTimersByTime(100);
|
|
99
|
+
expect(obj.fn).toHaveBeenCalledOnce();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should handle multiple rapid calls correctly", () => {
|
|
103
|
+
const mockFn = vi.fn();
|
|
104
|
+
const debouncedFn = debounce(mockFn, 100);
|
|
105
|
+
|
|
106
|
+
// Make 5 rapid calls
|
|
107
|
+
for (let i = 0; i < 5; i++) {
|
|
108
|
+
debouncedFn(i);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
vi.advanceTimersByTime(100);
|
|
112
|
+
expect(mockFn).toHaveBeenCalledOnce();
|
|
113
|
+
expect(mockFn).toHaveBeenCalledWith(4); // Should be called with the last argument
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should use default options when none provided", () => {
|
|
117
|
+
const mockFn = vi.fn();
|
|
118
|
+
const debouncedFn = debounce(mockFn, 100);
|
|
119
|
+
|
|
120
|
+
debouncedFn();
|
|
121
|
+
expect(mockFn).not.toHaveBeenCalled(); // leading: false by default
|
|
122
|
+
|
|
123
|
+
vi.advanceTimersByTime(100);
|
|
124
|
+
expect(mockFn).toHaveBeenCalledOnce(); // trailing: true by default
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
type DebounceOptions = {
|
|
2
|
+
// Whether the first call to the debounced function is run immediately.
|
|
3
|
+
leading?: boolean;
|
|
4
|
+
|
|
5
|
+
// Whether the last call to the debounced function is run after delay
|
|
6
|
+
// milliseconds have elapsed since the last call.
|
|
7
|
+
trailing?: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const defaultDebounceOptions: DebounceOptions = {
|
|
11
|
+
leading: false,
|
|
12
|
+
trailing: true,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns `fn` wrapped by a function that delays invoking `fn` for `delay`
|
|
17
|
+
* milliseconds since the last call. Set `options.leading` to invoke `fn` on
|
|
18
|
+
* the leading edge of the delay, and/or set `options.trailing` to invoke `fn`
|
|
19
|
+
* on the trailing edge of the delay (true by default).
|
|
20
|
+
*
|
|
21
|
+
* Example for `debounce(fn, 30, {leading: true, trailing: true})`,
|
|
22
|
+
* where `fn` is called twice, with the second call made 20 ms after the first:
|
|
23
|
+
*
|
|
24
|
+
* Time: 0 20 50 (ms)
|
|
25
|
+
* Timeline: |----------------------------------------------------------------|
|
|
26
|
+
* ^ ^ ^
|
|
27
|
+
* | | |
|
|
28
|
+
* | First call. | Second call 20ms after the | End of delay.
|
|
29
|
+
* | (instant leading | first. | (trailing edge
|
|
30
|
+
* | edge call) | | call)
|
|
31
|
+
* |-----------------------------|
|
|
32
|
+
* | 30 ms delay for debounce. |
|
|
33
|
+
*
|
|
34
|
+
*
|
|
35
|
+
* Note that if both `options.leading` and `options.trailing` are true, `fn`
|
|
36
|
+
* will only be invoked on the trailing edge if the debounced function is called
|
|
37
|
+
* more than once during the delay.
|
|
38
|
+
*
|
|
39
|
+
* @param fn - Function to debounce.
|
|
40
|
+
* @param delay - Milliseconds to delay calling `fn` since the last call.
|
|
41
|
+
* @param debounceOptions - See `DebounceOptions` and `defaultDebounceOptions`.
|
|
42
|
+
* @returns A debounced `fn`.
|
|
43
|
+
*/
|
|
44
|
+
export function debounce<T, A extends unknown[]>(
|
|
45
|
+
fn: (this: T, ...args: A) => void,
|
|
46
|
+
delay: number,
|
|
47
|
+
debounceOptions: DebounceOptions = {},
|
|
48
|
+
): (this: T, ...args: A) => void {
|
|
49
|
+
const options = { ...defaultDebounceOptions, ...debounceOptions };
|
|
50
|
+
|
|
51
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
52
|
+
|
|
53
|
+
return function (this: T, ...args: A): void {
|
|
54
|
+
if (options.leading && !timer) {
|
|
55
|
+
// Leading edge.
|
|
56
|
+
// Call fn on the leading edge, when debouncing hasn't started yet.
|
|
57
|
+
console.log("leading");
|
|
58
|
+
fn.apply(this, args);
|
|
59
|
+
|
|
60
|
+
// Debounce the next call.
|
|
61
|
+
timer = setTimeout(() => {
|
|
62
|
+
timer = undefined;
|
|
63
|
+
}, delay);
|
|
64
|
+
} else {
|
|
65
|
+
// Trailing edge.
|
|
66
|
+
// Postpone calling fn until the delay has elapsed since the last call.
|
|
67
|
+
// Each call clears any previously delayed call and resets the delay, so
|
|
68
|
+
// the postponed call will always be the last one.
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
timer = setTimeout(() => {
|
|
71
|
+
if (options.trailing) {
|
|
72
|
+
// Call fn on the trailing edge.
|
|
73
|
+
fn.apply(this, args);
|
|
74
|
+
|
|
75
|
+
if (options.leading) {
|
|
76
|
+
// Debounce next leading call since a trailing call was just made.
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
timer = undefined;
|
|
79
|
+
}, delay);
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
// No trailing call. Since the delay has elapsed since the last call,
|
|
83
|
+
// immediately reset the debouncing delay.
|
|
84
|
+
timer = undefined;
|
|
85
|
+
}
|
|
86
|
+
}, delay);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
|
|
3
|
+
export type GenerateIdShape = {
|
|
4
|
+
generateId: () => Effect.Effect<string>;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// Declaring a tag for a service that generates random id
|
|
8
|
+
export class GenerateId extends Context.Tag("UploadistaGenerateIdService")<
|
|
9
|
+
GenerateId,
|
|
10
|
+
{ readonly generateId: () => Effect.Effect<string> }
|
|
11
|
+
>() {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Effect-based ID generation services
|
|
15
|
+
*/
|
|
16
|
+
export const GenerateIdService = GenerateId.Service;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generates a random UUID using Effect
|
|
20
|
+
* @returns Effect that produces a random UUID string
|
|
21
|
+
*/
|
|
22
|
+
export const GenerateIdRandom = GenerateId.of({
|
|
23
|
+
generateId: () => Effect.succeed(crypto.randomUUID()),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const GenerateIdLive = Layer.succeed(GenerateId, GenerateIdRandom);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generates a timestamp-based ID using Effect
|
|
30
|
+
* @returns Effect that produces a timestamp-based ID
|
|
31
|
+
*/
|
|
32
|
+
export const GenerateIdTimestamp = GenerateId.of({
|
|
33
|
+
generateId: () =>
|
|
34
|
+
Effect.succeed(`${Date.now()}-${Math.random().toString(36).slice(2, 11)}`),
|
|
35
|
+
});
|