@umituz/web-cloudflare 1.4.3 → 1.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -29
- package/package.json +6 -5
- package/src/config/patterns.ts +43 -24
- package/src/domain/entities/analytics.entity.ts +18 -35
- package/src/domain/entities/d1.entity.ts +27 -0
- package/src/domain/entities/image.entity.ts +27 -27
- package/src/domain/entities/kv.entity.ts +20 -17
- package/src/domain/entities/r2.entity.ts +49 -0
- package/src/domain/entities/worker.entity.ts +35 -0
- package/src/domains/analytics/entities/index.ts +47 -0
- package/src/domains/analytics/index.ts +13 -0
- package/src/{infrastructure/services/analytics → domains/analytics/services}/analytics.service.ts +1 -1
- package/src/{infrastructure/services/analytics → domains/analytics/services}/index.ts +1 -0
- package/src/domains/analytics/types/index.ts +5 -0
- package/src/domains/analytics/types/service.interface.ts +12 -0
- package/src/domains/images/entities/index.ts +48 -0
- package/src/domains/images/index.ts +13 -0
- package/src/{infrastructure/services/images → domains/images/services}/images.service.ts +3 -3
- package/src/{infrastructure/services/images → domains/images/services}/index.ts +1 -0
- package/src/domains/images/types/index.ts +5 -0
- package/src/domains/images/types/service.interface.ts +13 -0
- package/src/domains/kv/entities/index.ts +34 -0
- package/src/domains/kv/index.ts +13 -0
- package/src/{infrastructure/services/kv → domains/kv/services}/index.ts +1 -0
- package/src/{infrastructure/services/kv → domains/kv/services}/kv.service.ts +2 -2
- package/src/domains/kv/types/index.ts +5 -0
- package/src/domains/kv/types/service.interface.ts +13 -0
- package/src/domains/workers/entities/index.ts +1 -1
- package/src/domains/workflows/entities/index.ts +60 -0
- package/src/domains/workflows/index.ts +10 -0
- package/src/domains/workflows/services/index.ts +6 -0
- package/src/{infrastructure/services/workflows/index.ts → domains/workflows/services/workflows.service.ts} +1 -1
- package/src/domains/wrangler/entities/index.ts +2 -2
- package/src/domains/wrangler/services/wrangler.service.ts +16 -8
- package/src/domains/wrangler/types/service.interface.ts +2 -2
- package/src/index.ts +4 -4
- package/src/infrastructure/middleware/auth.ts +118 -0
- package/src/infrastructure/middleware/cache.ts +95 -0
- package/src/infrastructure/middleware/cors.ts +95 -0
- package/src/infrastructure/middleware/index.ts +20 -3
- package/src/infrastructure/middleware/rate-limit.ts +105 -0
- package/src/infrastructure/router/index.ts +26 -4
- package/src/infrastructure/utils/helpers.ts +25 -11
- package/src/infrastructure/utils/utils.util.ts +3 -2
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Middleware
|
|
3
|
+
* @description Caching middleware for Cloudflare Workers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface CacheConfig {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
defaultTTL: number;
|
|
9
|
+
paths?: Record<string, number>;
|
|
10
|
+
prefix?: string;
|
|
11
|
+
bypassPaths?: string[];
|
|
12
|
+
respectHeaders?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const cacheStore = new Map<string, { response: Response; expires: number }>();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Cache middleware
|
|
19
|
+
*/
|
|
20
|
+
export async function cache(
|
|
21
|
+
request: Request,
|
|
22
|
+
config: CacheConfig
|
|
23
|
+
): Promise<Response | null> {
|
|
24
|
+
if (!config.enabled) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const url = new URL(request.url);
|
|
29
|
+
const cacheKey = `${config.prefix || 'cache'}:${url.pathname}${url.search}`;
|
|
30
|
+
|
|
31
|
+
// Check if path should bypass cache
|
|
32
|
+
if (config.bypassPaths?.some((path) => url.pathname.startsWith(path))) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check cache
|
|
37
|
+
const cached = cacheStore.get(cacheKey);
|
|
38
|
+
if (cached && cached.expires > Date.now()) {
|
|
39
|
+
return cached.response;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Set cache
|
|
47
|
+
*/
|
|
48
|
+
export function setCache(
|
|
49
|
+
request: Request,
|
|
50
|
+
response: Response,
|
|
51
|
+
config: CacheConfig
|
|
52
|
+
): void {
|
|
53
|
+
if (!config.enabled) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const url = new URL(request.url);
|
|
58
|
+
const cacheKey = `${config.prefix || 'cache'}:${url.pathname}${url.search}`;
|
|
59
|
+
|
|
60
|
+
// Determine TTL
|
|
61
|
+
let ttl = config.defaultTTL;
|
|
62
|
+
for (const [path, pathTTL] of Object.entries(config.paths || {})) {
|
|
63
|
+
if (url.pathname.startsWith(path)) {
|
|
64
|
+
ttl = pathTTL;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Don't cache if TTL is 0
|
|
70
|
+
if (ttl === 0) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Cache the response
|
|
75
|
+
cacheStore.set(cacheKey, {
|
|
76
|
+
response: response.clone(),
|
|
77
|
+
expires: Date.now() + ttl * 1000,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Invalidate cache
|
|
83
|
+
*/
|
|
84
|
+
export function invalidateCache(pattern?: string): void {
|
|
85
|
+
if (!pattern) {
|
|
86
|
+
cacheStore.clear();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const key of cacheStore.keys()) {
|
|
91
|
+
if (key.includes(pattern)) {
|
|
92
|
+
cacheStore.delete(key);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORS Middleware
|
|
3
|
+
* @description Cross-Origin Resource Sharing middleware for Cloudflare Workers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface CORSConfig {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
allowedOrigins: string[];
|
|
9
|
+
allowedMethods: string[];
|
|
10
|
+
allowedHeaders: string[];
|
|
11
|
+
exposedHeaders?: string[];
|
|
12
|
+
allowCredentials?: boolean;
|
|
13
|
+
maxAge?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Add CORS headers to response
|
|
18
|
+
*/
|
|
19
|
+
export function addCorsHeaders(
|
|
20
|
+
request: Request,
|
|
21
|
+
response: Response,
|
|
22
|
+
config: CORSConfig
|
|
23
|
+
): Response {
|
|
24
|
+
if (!config.enabled) {
|
|
25
|
+
return response;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const headers = new Headers(response.headers);
|
|
29
|
+
const origin = request.headers.get('Origin');
|
|
30
|
+
|
|
31
|
+
// Check if origin is allowed
|
|
32
|
+
const allowedOrigin = config.allowedOrigins.includes('*')
|
|
33
|
+
? '*'
|
|
34
|
+
: config.allowedOrigins.includes(origin || '')
|
|
35
|
+
? origin
|
|
36
|
+
: config.allowedOrigins[0];
|
|
37
|
+
|
|
38
|
+
headers.set('Access-Control-Allow-Origin', allowedOrigin);
|
|
39
|
+
headers.set('Access-Control-Allow-Methods', config.allowedMethods.join(', '));
|
|
40
|
+
headers.set('Access-Control-Allow-Headers', config.allowedHeaders.join(', '));
|
|
41
|
+
|
|
42
|
+
if (config.exposedHeaders) {
|
|
43
|
+
headers.set('Access-Control-Expose-Headers', config.exposedHeaders.join(', '));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (config.allowCredentials) {
|
|
47
|
+
headers.set('Access-Control-Allow-Credentials', 'true');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (config.maxAge) {
|
|
51
|
+
headers.set('Access-Control-Max-Age', config.maxAge.toString());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return new Response(response.body, {
|
|
55
|
+
status: response.status,
|
|
56
|
+
statusText: response.statusText,
|
|
57
|
+
headers,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* CORS middleware
|
|
63
|
+
*/
|
|
64
|
+
export async function cors(
|
|
65
|
+
request: Request,
|
|
66
|
+
config: CORSConfig
|
|
67
|
+
): Promise<Response | null> {
|
|
68
|
+
if (!config.enabled) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Handle preflight request
|
|
73
|
+
if (request.method === 'OPTIONS') {
|
|
74
|
+
const headers = new Headers();
|
|
75
|
+
const origin = request.headers.get('Origin');
|
|
76
|
+
|
|
77
|
+
const allowedOrigin = config.allowedOrigins.includes('*')
|
|
78
|
+
? '*'
|
|
79
|
+
: config.allowedOrigins.includes(origin || '')
|
|
80
|
+
? origin
|
|
81
|
+
: config.allowedOrigins[0];
|
|
82
|
+
|
|
83
|
+
headers.set('Access-Control-Allow-Origin', allowedOrigin);
|
|
84
|
+
headers.set('Access-Control-Allow-Methods', config.allowedMethods.join(', '));
|
|
85
|
+
headers.set('Access-Control-Allow-Headers', config.allowedHeaders.join(', '));
|
|
86
|
+
|
|
87
|
+
if (config.maxAge) {
|
|
88
|
+
headers.set('Access-Control-Max-Age', config.maxAge.toString());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return new Response(null, { headers });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
* @description Comprehensive middleware for Cloudflare Workers
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
// ============================================================
|
|
7
|
+
// Environment Types
|
|
8
|
+
// ============================================================
|
|
9
|
+
|
|
10
|
+
export interface CloudflareMiddlewareEnv {
|
|
11
|
+
KV?: KVNamespace;
|
|
12
|
+
R2?: R2Bucket;
|
|
13
|
+
D1?: D1Database;
|
|
14
|
+
DO?: Record<string, DurableObjectNamespace>;
|
|
15
|
+
QUEUE?: Record<string, Queue>;
|
|
16
|
+
AI?: any;
|
|
17
|
+
vars?: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Type alias for backwards compatibility
|
|
21
|
+
export type Env = CloudflareMiddlewareEnv;
|
|
22
|
+
|
|
6
23
|
// Re-export existing middleware
|
|
7
24
|
export { cors, addCorsHeaders } from './cors';
|
|
8
25
|
export { cache, setCache, invalidateCache } from './cache';
|
|
@@ -300,7 +317,7 @@ export interface HealthCheckConfig {
|
|
|
300
317
|
}
|
|
301
318
|
|
|
302
319
|
export async function healthCheck(
|
|
303
|
-
env:
|
|
320
|
+
env: CloudflareMiddlewareEnv,
|
|
304
321
|
config?: HealthCheckConfig
|
|
305
322
|
): Promise<Response> {
|
|
306
323
|
const checks: Record<string, boolean | string> = {
|
|
@@ -357,9 +374,9 @@ export function handleMiddlewareError(
|
|
|
357
374
|
}
|
|
358
375
|
|
|
359
376
|
/**
|
|
360
|
-
* Conditional Middleware
|
|
377
|
+
* Conditional Middleware (Chain)
|
|
361
378
|
*/
|
|
362
|
-
export function
|
|
379
|
+
export function conditionalChainMiddleware(
|
|
363
380
|
condition: (request: Request) => boolean,
|
|
364
381
|
middleware: (request: Request) => Response | null
|
|
365
382
|
): (request: Request) => Response | null {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limit Middleware
|
|
3
|
+
* @description Rate limiting middleware for Cloudflare Workers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface RateLimitConfig {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
maxRequests: number;
|
|
9
|
+
window: number;
|
|
10
|
+
by?: 'ip' | 'user' | 'both';
|
|
11
|
+
customKeys?: string[];
|
|
12
|
+
whitelist?: string[];
|
|
13
|
+
response?: {
|
|
14
|
+
status: number;
|
|
15
|
+
message: string;
|
|
16
|
+
retryAfter?: number;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface RateLimitEntry {
|
|
21
|
+
count: number;
|
|
22
|
+
resetTime: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const rateLimitStore = new Map<string, RateLimitEntry>();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get rate limit key
|
|
29
|
+
*/
|
|
30
|
+
function getRateLimitKey(request: Request, config: RateLimitConfig): string {
|
|
31
|
+
const parts: string[] = [];
|
|
32
|
+
|
|
33
|
+
if (config.by === 'ip' || config.by === 'both') {
|
|
34
|
+
parts.push(request.headers.get('CF-Connecting-IP') || 'unknown');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (config.by === 'user' || config.by === 'both') {
|
|
38
|
+
const auth = request.headers.get('Authorization');
|
|
39
|
+
if (auth) {
|
|
40
|
+
parts.push(auth.substring(0, 20));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (config.customKeys) {
|
|
45
|
+
for (const key of config.customKeys) {
|
|
46
|
+
parts.push(request.headers.get(key) || '');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return parts.join(':') || 'default';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check rate limit
|
|
55
|
+
*/
|
|
56
|
+
export async function checkRateLimit(
|
|
57
|
+
request: Request,
|
|
58
|
+
config: RateLimitConfig
|
|
59
|
+
): Promise<Response | null> {
|
|
60
|
+
if (!config.enabled) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const key = getRateLimitKey(request, config);
|
|
65
|
+
|
|
66
|
+
// Check whitelist
|
|
67
|
+
if (config.whitelist?.includes(key)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
const entry = rateLimitStore.get(key);
|
|
73
|
+
|
|
74
|
+
// Reset if window expired
|
|
75
|
+
if (!entry || now > entry.resetTime) {
|
|
76
|
+
rateLimitStore.set(key, {
|
|
77
|
+
count: 1,
|
|
78
|
+
resetTime: now + config.window * 1000,
|
|
79
|
+
});
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Increment count
|
|
84
|
+
entry.count++;
|
|
85
|
+
|
|
86
|
+
// Check if exceeded
|
|
87
|
+
if (entry.count > config.maxRequests) {
|
|
88
|
+
const retryAfter = Math.ceil((entry.resetTime - now) / 1000);
|
|
89
|
+
return new Response(
|
|
90
|
+
JSON.stringify({
|
|
91
|
+
error: config.response?.message || 'Rate limit exceeded',
|
|
92
|
+
retryAfter,
|
|
93
|
+
}),
|
|
94
|
+
{
|
|
95
|
+
status: config.response?.status || 429,
|
|
96
|
+
headers: {
|
|
97
|
+
'Content-Type': 'application/json',
|
|
98
|
+
'Retry-After': retryAfter.toString(),
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
@@ -5,6 +5,23 @@
|
|
|
5
5
|
|
|
6
6
|
import { json, notFound, badRequest } from '../utils/helpers';
|
|
7
7
|
|
|
8
|
+
// ============================================================
|
|
9
|
+
// Environment Types
|
|
10
|
+
// ============================================================
|
|
11
|
+
|
|
12
|
+
export interface CloudflareEnv {
|
|
13
|
+
KV?: KVNamespace;
|
|
14
|
+
R2?: R2Bucket;
|
|
15
|
+
D1?: D1Database;
|
|
16
|
+
DO?: Record<string, DurableObjectNamespace>;
|
|
17
|
+
QUEUE?: Record<string, Queue>;
|
|
18
|
+
AI?: any;
|
|
19
|
+
vars?: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Type alias for backwards compatibility
|
|
23
|
+
export type Env = CloudflareEnv;
|
|
24
|
+
|
|
8
25
|
// ============================================================
|
|
9
26
|
// Route Handler Types
|
|
10
27
|
// ============================================================
|
|
@@ -12,13 +29,13 @@ import { json, notFound, badRequest } from '../utils/helpers';
|
|
|
12
29
|
export type RouteHandler = (
|
|
13
30
|
request: Request,
|
|
14
31
|
params?: Record<string, string>,
|
|
15
|
-
env?:
|
|
32
|
+
env?: CloudflareEnv,
|
|
16
33
|
ctx?: ExecutionContext
|
|
17
34
|
) => Promise<Response> | Response;
|
|
18
35
|
|
|
19
36
|
export type Middleware = (
|
|
20
37
|
request: Request,
|
|
21
|
-
env?:
|
|
38
|
+
env?: CloudflareEnv,
|
|
22
39
|
ctx?: ExecutionContext
|
|
23
40
|
) => Promise<Response | null> | Response | null;
|
|
24
41
|
|
|
@@ -147,7 +164,7 @@ export class Router {
|
|
|
147
164
|
*/
|
|
148
165
|
async handle(
|
|
149
166
|
request: Request,
|
|
150
|
-
env?:
|
|
167
|
+
env?: CloudflareEnv,
|
|
151
168
|
ctx?: ExecutionContext
|
|
152
169
|
): Promise<Response> {
|
|
153
170
|
const url = new URL(request.url);
|
|
@@ -499,7 +516,12 @@ export async function body<T = unknown>(request: Request): Promise<T> {
|
|
|
499
516
|
*/
|
|
500
517
|
export function query(request: Request): Record<string, string> {
|
|
501
518
|
const url = new URL(request.url);
|
|
502
|
-
|
|
519
|
+
const result: Record<string, string> = {};
|
|
520
|
+
// URLSearchParams.keys() is not available in Workers runtime
|
|
521
|
+
url.searchParams.forEach((value, key) => {
|
|
522
|
+
result[key] = value;
|
|
523
|
+
});
|
|
524
|
+
return result;
|
|
503
525
|
}
|
|
504
526
|
|
|
505
527
|
/**
|
|
@@ -19,7 +19,17 @@ export async function parseBody<T = unknown>(request: Request): Promise<T> {
|
|
|
19
19
|
|
|
20
20
|
if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
21
21
|
const formData = await request.formData();
|
|
22
|
-
|
|
22
|
+
const result: Record<string, unknown> = {};
|
|
23
|
+
// FormData.keys() is not available in Workers runtime
|
|
24
|
+
// Use alternative approach with for...of
|
|
25
|
+
const keys: string[] = [];
|
|
26
|
+
formData.forEach((value, key) => {
|
|
27
|
+
if (!keys.includes(key)) {
|
|
28
|
+
keys.push(key);
|
|
29
|
+
}
|
|
30
|
+
result[key] = value;
|
|
31
|
+
});
|
|
32
|
+
return result as T;
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
if (contentType.includes('text/')) {
|
|
@@ -297,16 +307,15 @@ export function generateCacheKey(request: Request, prefix?: string): string {
|
|
|
297
307
|
const parts = [prefix || 'cache', url.pathname];
|
|
298
308
|
|
|
299
309
|
// Add query params (sorted for consistency)
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
)
|
|
310
|
+
const params: string[] = [];
|
|
311
|
+
// URLSearchParams.keys() is not available in Workers runtime
|
|
312
|
+
url.searchParams.forEach((value, key) => {
|
|
313
|
+
params.push(`${key}=${value}`);
|
|
314
|
+
});
|
|
315
|
+
params.sort();
|
|
303
316
|
|
|
304
|
-
if (
|
|
305
|
-
parts.push(
|
|
306
|
-
sortedParams
|
|
307
|
-
.map(([key, value]) => `${key}=${value}`)
|
|
308
|
-
.join('&')
|
|
309
|
-
);
|
|
317
|
+
if (params.length > 0) {
|
|
318
|
+
parts.push(params.join('&'));
|
|
310
319
|
}
|
|
311
320
|
|
|
312
321
|
// Add auth header if present (for user-specific caching)
|
|
@@ -479,7 +488,12 @@ export function buildURL(base: string, params: Record<string, string | number |
|
|
|
479
488
|
*/
|
|
480
489
|
export function parseQueryParams(url: string): Record<string, string> {
|
|
481
490
|
const params = new URL(url).searchParams;
|
|
482
|
-
|
|
491
|
+
const result: Record<string, string> = {};
|
|
492
|
+
// URLSearchParams.keys() is not available in Workers runtime
|
|
493
|
+
params.forEach((value, key) => {
|
|
494
|
+
result[key] = value;
|
|
495
|
+
});
|
|
496
|
+
return result;
|
|
483
497
|
}
|
|
484
498
|
|
|
485
499
|
/**
|
|
@@ -115,12 +115,13 @@ export const transformUtils = {
|
|
|
115
115
|
*/
|
|
116
116
|
async streamToBlob(stream: ReadableStream): Promise<Blob> {
|
|
117
117
|
const reader = stream.getReader();
|
|
118
|
-
const chunks:
|
|
118
|
+
const chunks: BlobPart[] = [];
|
|
119
119
|
|
|
120
120
|
while (true) {
|
|
121
121
|
const { done, value } = await reader.read();
|
|
122
122
|
if (done) break;
|
|
123
|
-
|
|
123
|
+
// Convert Uint8Array to BlobPart
|
|
124
|
+
chunks.push(value as BlobPart);
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
return new Blob(chunks);
|