@tranquilload/adapters 0.1.0
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 +62 -0
- package/dist/from-file.cjs +12 -0
- package/dist/from-file.cjs.map +1 -0
- package/dist/from-file.d.cts +8 -0
- package/dist/from-file.d.cts.map +1 -0
- package/dist/from-file.d.mts +8 -0
- package/dist/from-file.d.mts.map +1 -0
- package/dist/from-file.mjs +11 -0
- package/dist/from-file.mjs.map +1 -0
- package/dist/from-node-readable.cjs +10 -0
- package/dist/from-node-readable.cjs.map +1 -0
- package/dist/from-node-readable.d.cts +7 -0
- package/dist/from-node-readable.d.cts.map +1 -0
- package/dist/from-node-readable.d.mts +7 -0
- package/dist/from-node-readable.d.mts.map +1 -0
- package/dist/from-node-readable.mjs +9 -0
- package/dist/from-node-readable.mjs.map +1 -0
- package/dist/network-multiplier.cjs +26 -0
- package/dist/network-multiplier.cjs.map +1 -0
- package/dist/network-multiplier.d.cts +25 -0
- package/dist/network-multiplier.d.cts.map +1 -0
- package/dist/network-multiplier.d.mts +25 -0
- package/dist/network-multiplier.d.mts.map +1 -0
- package/dist/network-multiplier.mjs +25 -0
- package/dist/network-multiplier.mjs.map +1 -0
- package/dist/optimal-part-size.cjs +14 -0
- package/dist/optimal-part-size.cjs.map +1 -0
- package/dist/optimal-part-size.d.cts +23 -0
- package/dist/optimal-part-size.d.cts.map +1 -0
- package/dist/optimal-part-size.d.mts +23 -0
- package/dist/optimal-part-size.d.mts.map +1 -0
- package/dist/optimal-part-size.mjs +13 -0
- package/dist/optimal-part-size.mjs.map +1 -0
- package/dist/s3-multipart-upload.cjs +60 -0
- package/dist/s3-multipart-upload.cjs.map +1 -0
- package/dist/s3-multipart-upload.d.cts +41 -0
- package/dist/s3-multipart-upload.d.cts.map +1 -0
- package/dist/s3-multipart-upload.d.mts +41 -0
- package/dist/s3-multipart-upload.d.mts.map +1 -0
- package/dist/s3-multipart-upload.mjs +58 -0
- package/dist/s3-multipart-upload.mjs.map +1 -0
- package/dist/simple-http-upload.cjs +26 -0
- package/dist/simple-http-upload.cjs.map +1 -0
- package/dist/simple-http-upload.d.cts +13 -0
- package/dist/simple-http-upload.d.cts.map +1 -0
- package/dist/simple-http-upload.d.mts +13 -0
- package/dist/simple-http-upload.d.mts.map +1 -0
- package/dist/simple-http-upload.mjs +25 -0
- package/dist/simple-http-upload.mjs.map +1 -0
- package/package.json +73 -0
- package/src/protocols/s3-multipart-upload.test.ts +127 -0
- package/src/protocols/s3-multipart-upload.ts +92 -0
- package/src/protocols/simple-http-upload.test.ts +75 -0
- package/src/protocols/simple-http-upload.ts +38 -0
- package/src/resilience/network-multiplier.test.ts +57 -0
- package/src/resilience/network-multiplier.ts +52 -0
- package/src/resilience/optimal-part-size.test.ts +57 -0
- package/src/resilience/optimal-part-size.ts +34 -0
- package/src/sources/from-file.test.ts +37 -0
- package/src/sources/from-file.ts +6 -0
- package/src/sources/from-node-readable.test.ts +43 -0
- package/src/sources/from-node-readable.ts +5 -0
- package/tsconfig.json +8 -0
- package/tsdown.config.ts +16 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { computeOptimalPartSize } from './optimal-part-size.js'
|
|
3
|
+
|
|
4
|
+
const MB = 1024 * 1024
|
|
5
|
+
|
|
6
|
+
describe('computeOptimalPartSize', () => {
|
|
7
|
+
it('returns minPartSize when totalBytes is undefined', () => {
|
|
8
|
+
expect(
|
|
9
|
+
computeOptimalPartSize({
|
|
10
|
+
totalBytes: undefined,
|
|
11
|
+
targetPartCount: 10,
|
|
12
|
+
minPartSize: 5 * MB,
|
|
13
|
+
})
|
|
14
|
+
).toBe(5 * MB)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('returns optimal size when raw > minPartSize (AC1: 100MB / 10 = 10MB > 5MB)', () => {
|
|
18
|
+
expect(
|
|
19
|
+
computeOptimalPartSize({
|
|
20
|
+
totalBytes: 100 * MB,
|
|
21
|
+
targetPartCount: 10,
|
|
22
|
+
minPartSize: 5 * MB,
|
|
23
|
+
})
|
|
24
|
+
).toBe(10 * MB)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('returns minPartSize when file is too small (AC2: totalBytes/targetPartCount < minPartSize)', () => {
|
|
28
|
+
expect(
|
|
29
|
+
computeOptimalPartSize({
|
|
30
|
+
totalBytes: 8 * MB,
|
|
31
|
+
targetPartCount: 10,
|
|
32
|
+
minPartSize: 5 * MB,
|
|
33
|
+
})
|
|
34
|
+
).toBe(5 * MB)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('clamps to maxPartSize when raw part size exceeds max', () => {
|
|
38
|
+
expect(
|
|
39
|
+
computeOptimalPartSize({
|
|
40
|
+
totalBytes: 100 * MB,
|
|
41
|
+
targetPartCount: 2,
|
|
42
|
+
minPartSize: 5 * MB,
|
|
43
|
+
maxPartSize: 20 * MB,
|
|
44
|
+
})
|
|
45
|
+
).toBe(20 * MB)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('returns exact value when totalBytes / targetPartCount === minPartSize', () => {
|
|
49
|
+
expect(
|
|
50
|
+
computeOptimalPartSize({
|
|
51
|
+
totalBytes: 50 * MB,
|
|
52
|
+
targetPartCount: 10,
|
|
53
|
+
minPartSize: 5 * MB,
|
|
54
|
+
})
|
|
55
|
+
).toBe(5 * MB)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optimal part size calculator for multipart uploads.
|
|
3
|
+
*
|
|
4
|
+
* Pure function — computes the ideal chunk size given file size,
|
|
5
|
+
* target part count, and protocol constraints (min/max part size).
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface OptimalPartSizeOptions {
|
|
11
|
+
/** Total file size in bytes. Pass `undefined` if unknown (streaming). */
|
|
12
|
+
totalBytes?: number
|
|
13
|
+
/** Desired number of parts. */
|
|
14
|
+
targetPartCount: number
|
|
15
|
+
/** Protocol minimum part size (e.g. 5 * 1024 * 1024 for S3). Always returned as floor. */
|
|
16
|
+
minPartSize: number
|
|
17
|
+
/** Optional maximum part size. Clamps result if provided. */
|
|
18
|
+
maxPartSize?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function computeOptimalPartSize(options: OptimalPartSizeOptions): number {
|
|
22
|
+
const { totalBytes, targetPartCount, minPartSize, maxPartSize } = options
|
|
23
|
+
|
|
24
|
+
if (totalBytes === undefined) return minPartSize
|
|
25
|
+
|
|
26
|
+
const raw = Math.ceil(totalBytes / targetPartCount)
|
|
27
|
+
let result = Math.max(raw, minPartSize)
|
|
28
|
+
|
|
29
|
+
if (maxPartSize !== undefined) {
|
|
30
|
+
result = Math.min(result, maxPartSize)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return result
|
|
34
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { fromFile } from './from-file.js'
|
|
3
|
+
|
|
4
|
+
describe('fromFile', () => {
|
|
5
|
+
it('returns totalBytes equal to file.size', () => {
|
|
6
|
+
const bytes = new Uint8Array([1, 2, 3, 4, 5])
|
|
7
|
+
const file = new File([bytes], 'test.bin', { type: 'application/octet-stream' })
|
|
8
|
+
|
|
9
|
+
const result = fromFile(file)
|
|
10
|
+
|
|
11
|
+
expect(result.totalBytes).toBe(5)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('stream yields all file bytes', async () => {
|
|
15
|
+
const bytes = new Uint8Array([10, 20, 30, 40])
|
|
16
|
+
const file = new File([bytes], 'test.bin')
|
|
17
|
+
|
|
18
|
+
const { stream } = fromFile(file)
|
|
19
|
+
|
|
20
|
+
const reader = stream.getReader()
|
|
21
|
+
const chunks: Uint8Array[] = []
|
|
22
|
+
while (true) {
|
|
23
|
+
const { done, value } = await reader.read()
|
|
24
|
+
if (done) break
|
|
25
|
+
chunks.push(value)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const all = new Uint8Array(chunks.reduce((acc, c) => acc + c.length, 0))
|
|
29
|
+
let offset = 0
|
|
30
|
+
for (const chunk of chunks) {
|
|
31
|
+
all.set(chunk, offset)
|
|
32
|
+
offset += chunk.length
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
expect(Array.from(all)).toEqual([10, 20, 30, 40])
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { Readable } from "node:stream"
|
|
3
|
+
import { fromNodeReadable } from "./from-node-readable.js"
|
|
4
|
+
|
|
5
|
+
describe("fromNodeReadable", () => {
|
|
6
|
+
it("streams all bytes from a Node Readable", async () => {
|
|
7
|
+
const bytes = new Uint8Array([1, 2, 3, 4, 5])
|
|
8
|
+
const readable = Readable.from([Buffer.from(bytes)])
|
|
9
|
+
|
|
10
|
+
const webStream = fromNodeReadable(readable)
|
|
11
|
+
|
|
12
|
+
const reader = webStream.getReader()
|
|
13
|
+
const chunks: Uint8Array[] = []
|
|
14
|
+
while (true) {
|
|
15
|
+
const { done, value } = await reader.read()
|
|
16
|
+
if (done) break
|
|
17
|
+
chunks.push(value)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const totalLength = chunks.reduce((n, c) => n + c.length, 0)
|
|
21
|
+
const all = new Uint8Array(totalLength)
|
|
22
|
+
let offset = 0
|
|
23
|
+
for (const chunk of chunks) {
|
|
24
|
+
all.set(chunk, offset)
|
|
25
|
+
offset += chunk.length
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
expect(Array.from(all)).toEqual([1, 2, 3, 4, 5])
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("propagates Readable errors to the ReadableStream", async () => {
|
|
32
|
+
const readable = new Readable({
|
|
33
|
+
read() {
|
|
34
|
+
this.emit("error", new Error("read failure"))
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const webStream = fromNodeReadable(readable)
|
|
39
|
+
const reader = webStream.getReader()
|
|
40
|
+
|
|
41
|
+
await expect(reader.read()).rejects.toThrow("read failure")
|
|
42
|
+
})
|
|
43
|
+
})
|
package/tsconfig.json
ADDED
package/tsdown.config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from 'tsdown'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: {
|
|
5
|
+
'from-file': 'src/sources/from-file.ts',
|
|
6
|
+
'from-node-readable': 'src/sources/from-node-readable.ts',
|
|
7
|
+
's3-multipart-upload': 'src/protocols/s3-multipart-upload.ts',
|
|
8
|
+
'simple-http-upload': 'src/protocols/simple-http-upload.ts',
|
|
9
|
+
'network-multiplier': 'src/resilience/network-multiplier.ts',
|
|
10
|
+
'optimal-part-size': 'src/resilience/optimal-part-size.ts',
|
|
11
|
+
},
|
|
12
|
+
format: ['esm', 'cjs'],
|
|
13
|
+
dts: true,
|
|
14
|
+
clean: true,
|
|
15
|
+
sourcemap: true,
|
|
16
|
+
})
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
resolve: {
|
|
6
|
+
alias: {
|
|
7
|
+
'@tranquilload/core': path.resolve(__dirname, '../tranquilload-core/src'),
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
test: {
|
|
11
|
+
include: ['src/**/*.test.ts'],
|
|
12
|
+
},
|
|
13
|
+
})
|