@takosjp/yurucommu-core 3.4.4 → 4.0.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.
@@ -11,16 +11,15 @@ import type {
11
11
  MessageBatch,
12
12
  Queue,
13
13
  R2Bucket,
14
- R2Object,
15
14
  } from "@cloudflare/workers-types";
16
15
  import { getDb } from "../../db/index.ts";
17
16
  import type {
18
17
  IKeyValueStore,
19
- IObjectStorage,
18
+ ObjectStore,
19
+ ObjectStoreBody,
20
+ ObjectStoreObject,
21
+ ObjectStorePutOptions,
20
22
  IStaticAssets,
21
- ListObjectsResult,
22
- ObjectMetadata,
23
- StorageObject,
24
23
  } from "./types.ts";
25
24
  import type {
26
25
  IQueueBatch,
@@ -33,76 +32,39 @@ import type {
33
32
  /**
34
33
  * Cloudflare R2 Storage Adapter
35
34
  */
36
- class CloudflareStorage implements IObjectStorage {
35
+ class CloudflareStorage implements ObjectStore {
37
36
  constructor(private bucket: R2Bucket) {}
38
37
 
39
38
  async put(
40
39
  key: string,
41
- value: ReadableStream | ArrayBuffer | string,
42
- options?: {
43
- httpMetadata?: ObjectMetadata["httpMetadata"];
44
- customMetadata?: Record<string, string>;
45
- },
40
+ value: ObjectStoreBody,
41
+ options?: ObjectStorePutOptions,
46
42
  ): Promise<void> {
47
43
  await this.bucket.put(key, value as Parameters<R2Bucket["put"]>[1], {
48
- httpMetadata: options?.httpMetadata,
49
- customMetadata: options?.customMetadata,
44
+ httpMetadata:
45
+ options?.contentType === undefined
46
+ ? undefined
47
+ : { contentType: options.contentType },
50
48
  });
51
49
  }
52
50
 
53
- async get(key: string): Promise<StorageObject | null> {
51
+ async get(key: string): Promise<ObjectStoreObject | null> {
54
52
  const obj = await this.bucket.get(key);
55
53
  if (!obj) return null;
56
54
 
57
55
  return {
58
56
  key,
59
57
  body: obj.body as unknown as ReadableStream,
60
- bodyUsed: obj.bodyUsed,
61
- httpEtag: obj.httpEtag,
62
- arrayBuffer: () => obj.arrayBuffer(),
63
- text: () => obj.text(),
64
- json: <T>() => obj.json<T>(),
65
- httpMetadata: obj.httpMetadata,
66
- customMetadata: obj.customMetadata,
67
- };
68
- }
69
-
70
- async delete(key: string | string[]): Promise<void> {
71
- await this.bucket.delete(key);
72
- }
73
-
74
- async list(options?: {
75
- prefix?: string;
76
- limit?: number;
77
- cursor?: string;
78
- delimiter?: string;
79
- }): Promise<ListObjectsResult> {
80
- const result = await this.bucket.list(options);
81
- return {
82
- objects: result.objects.map((obj: R2Object) => ({
83
- key: obj.key,
84
- size: obj.size,
85
- uploaded: obj.uploaded,
86
- etag: obj.etag,
87
- httpMetadata: obj.httpMetadata,
88
- })),
89
- truncated: result.truncated,
90
- cursor: result.truncated ? result.cursor : undefined,
91
- delimitedPrefixes: result.delimitedPrefixes,
58
+ contentType: obj.httpMetadata?.contentType,
59
+ etag: obj.httpEtag,
60
+ byteLength: obj.size,
92
61
  };
93
62
  }
94
63
 
95
- async head(key: string): Promise<ObjectMetadata | null> {
96
- const obj = await this.bucket.head(key);
97
- if (!obj) return null;
98
-
99
- return {
100
- contentType: obj.httpMetadata?.contentType,
101
- contentLength: obj.size,
102
- etag: obj.etag,
103
- httpMetadata: obj.httpMetadata,
104
- customMetadata: obj.customMetadata,
105
- };
64
+ async delete(key: string | readonly string[]): Promise<void> {
65
+ await this.bucket.delete(
66
+ typeof key === "string" ? key : ([...key] as string[]),
67
+ );
106
68
  }
107
69
  }
108
70
 
@@ -241,7 +203,7 @@ export function wrapCloudflareBindings<
241
203
  "DB" | "MEDIA" | "KV" | "ASSETS" | "DELIVERY_QUEUE" | "DELIVERY_DLQ"
242
204
  > & {
243
205
  DB_INSTANCE: ReturnType<typeof getDb>;
244
- MEDIA?: IObjectStorage;
206
+ MEDIA?: ObjectStore;
245
207
  KV: IKeyValueStore;
246
208
  ASSETS?: IStaticAssets;
247
209
  DELIVERY_QUEUE?: IQueueProducer<unknown>;
@@ -4,12 +4,10 @@ import {
4
4
  managedRuntimeGatewayFailure,
5
5
  managedRuntimeKeyValueListRequest,
6
6
  managedRuntimeKeyValueRequest,
7
- managedRuntimeObjectListRequest,
8
7
  managedRuntimeObjectRequest,
9
8
  managedRuntimeQueueBatchSendGatewayRequest,
10
9
  managedRuntimeQueueSendGatewayRequest,
11
10
  parseManagedRuntimeKeyValueListResponse,
12
- parseManagedRuntimeObjectListResponse,
13
11
  parseManagedRuntimeConnectionMaterialization,
14
12
  parseManagedRuntimeQueueSendResponse,
15
13
  type ManagedRuntimeConnectionMaterialization,
@@ -17,10 +15,10 @@ import {
17
15
 
18
16
  import type {
19
17
  IKeyValueStore,
20
- IObjectStorage,
21
- ListObjectsResult,
22
- ObjectMetadata,
23
- StorageObject,
18
+ ObjectStore,
19
+ ObjectStoreBody,
20
+ ObjectStoreObject,
21
+ ObjectStorePutOptions,
24
22
  } from "./types.ts";
25
23
  import type {
26
24
  IQueueProducer,
@@ -88,7 +86,7 @@ export function createManagedRuntimeKeyValueStore(
88
86
 
89
87
  export function createManagedRuntimeObjectStorage(
90
88
  options: ManagedRuntimeDataAdapterOptions,
91
- ): IObjectStorage {
89
+ ): ObjectStore {
92
90
  const materialization = parseManagedRuntimeConnectionMaterialization(
93
91
  options.materialization,
94
92
  );
@@ -326,7 +324,7 @@ class ManagedRuntimeKeyValueStore implements IKeyValueStore {
326
324
  }
327
325
  }
328
326
 
329
- class ManagedRuntimeObjectStorage implements IObjectStorage {
327
+ class ManagedRuntimeObjectStorage implements ObjectStore {
330
328
  constructor(private readonly options: ManagedRuntimeDataClientOptions) {
331
329
  assertResponseLimit(options.maxMetadataResponseBytes);
332
330
  assertResponseLimit(options.maxValueResponseBytes);
@@ -334,23 +332,17 @@ class ManagedRuntimeObjectStorage implements IObjectStorage {
334
332
 
335
333
  async put(
336
334
  key: string,
337
- value: ReadableStream | ArrayBuffer | string,
338
- options?: {
339
- httpMetadata?: ObjectMetadata["httpMetadata"];
340
- customMetadata?: Record<string, string>;
341
- },
335
+ value: ObjectStoreBody,
336
+ options?: ObjectStorePutOptions,
342
337
  ): Promise<void> {
343
338
  const request = managedRuntimeObjectRequest(this.options.authority, {
344
339
  method: "PUT",
345
340
  key,
346
341
  idempotencyKey: this.options.idempotencyKey(),
347
342
  value: value as BodyInit,
348
- ...(options?.httpMetadata === undefined
349
- ? {}
350
- : { httpMetadata: options.httpMetadata }),
351
- ...(options?.customMetadata === undefined
343
+ ...(options?.contentType === undefined
352
344
  ? {}
353
- : { customMetadata: options.customMetadata }),
345
+ : { httpMetadata: { contentType: options.contentType } }),
354
346
  });
355
347
  await expectOkResponse(
356
348
  await this.options.gateway.fetch(request),
@@ -358,7 +350,7 @@ class ManagedRuntimeObjectStorage implements IObjectStorage {
358
350
  );
359
351
  }
360
352
 
361
- async get(key: string): Promise<StorageObject | null> {
353
+ async get(key: string): Promise<ObjectStoreObject | null> {
362
354
  const request = managedRuntimeObjectRequest(this.options.authority, {
363
355
  method: "GET",
364
356
  key,
@@ -371,11 +363,11 @@ class ManagedRuntimeObjectStorage implements IObjectStorage {
371
363
  }
372
364
  return new ManagedRuntimeStorageObject(
373
365
  key,
374
- await checkedResponse(raw, this.options.maxValueResponseBytes),
366
+ await checkedStreamingResponse(raw, this.options.maxValueResponseBytes),
375
367
  );
376
368
  }
377
369
 
378
- async delete(key: string | string[]): Promise<void> {
370
+ async delete(key: string | readonly string[]): Promise<void> {
379
371
  for (const entry of Array.isArray(key) ? key : [key]) {
380
372
  const request = managedRuntimeObjectRequest(this.options.authority, {
381
373
  method: "DELETE",
@@ -388,149 +380,135 @@ class ManagedRuntimeObjectStorage implements IObjectStorage {
388
380
  );
389
381
  }
390
382
  }
391
-
392
- async list(options?: {
393
- prefix?: string;
394
- limit?: number;
395
- cursor?: string;
396
- delimiter?: string;
397
- }): Promise<ListObjectsResult> {
398
- const request = managedRuntimeObjectListRequest(this.options.authority, {
399
- idempotencyKey: this.options.idempotencyKey(),
400
- ...options,
401
- });
402
- const response = await checkedResponse(
403
- await this.options.gateway.fetch(request),
404
- this.options.maxMetadataResponseBytes,
405
- );
406
- const parsed = parseManagedRuntimeObjectListResponse(await response.json());
407
- return {
408
- objects: parsed.objects.map((entry) => ({
409
- key: entry.key,
410
- size: entry.size,
411
- uploaded: new Date(entry.uploaded),
412
- ...(entry.etag === undefined ? {} : { etag: entry.etag }),
413
- })),
414
- truncated: parsed.truncated,
415
- ...(parsed.cursor === undefined ? {} : { cursor: parsed.cursor }),
416
- ...(parsed.delimitedPrefixes === undefined
417
- ? {}
418
- : { delimitedPrefixes: [...parsed.delimitedPrefixes] }),
419
- };
420
- }
421
-
422
- async head(key: string): Promise<ObjectMetadata | null> {
423
- const request = managedRuntimeObjectRequest(this.options.authority, {
424
- method: "HEAD",
425
- key,
426
- idempotencyKey: this.options.idempotencyKey(),
427
- });
428
- const raw = await this.options.gateway.fetch(request);
429
- if (raw.status === 404) {
430
- await raw.body?.cancel().catch(() => undefined);
431
- return null;
432
- }
433
- const response = await checkedResponse(
434
- raw,
435
- this.options.maxMetadataResponseBytes,
436
- );
437
- return objectMetadata(response.headers);
438
- }
439
383
  }
440
384
 
441
- class ManagedRuntimeStorageObject implements StorageObject {
385
+ class ManagedRuntimeStorageObject implements ObjectStoreObject {
442
386
  constructor(
443
387
  readonly key: string,
444
388
  private readonly response: Response,
445
389
  ) {}
446
390
 
447
- get body(): ReadableStream | null {
391
+ get body(): ReadableStream<Uint8Array> | null {
448
392
  return this.response.body;
449
393
  }
450
394
 
451
- get bodyUsed(): boolean {
452
- return this.response.bodyUsed;
395
+ get contentType(): string | undefined {
396
+ const value = this.response.headers.get("content-type");
397
+ return value === null || value.length === 0 ? undefined : value;
453
398
  }
454
399
 
455
- get httpEtag(): string | undefined {
400
+ get etag(): string | undefined {
456
401
  return this.response.headers.get("etag") ?? undefined;
457
402
  }
458
403
 
459
- get httpMetadata(): ObjectMetadata["httpMetadata"] {
460
- return objectMetadata(this.response.headers).httpMetadata;
404
+ get byteLength(): number | undefined {
405
+ return parseContentLength(this.response.headers.get("content-length"));
461
406
  }
407
+ }
462
408
 
463
- get customMetadata(): Record<string, string> | undefined {
464
- return objectMetadata(this.response.headers).customMetadata;
409
+ function parseContentLength(value: string | null): number | undefined {
410
+ if (value === null) return undefined;
411
+ if (!/^\d+$/u.test(value)) {
412
+ throw new ManagedRuntimeGatewayError(
413
+ "managed_runtime_object_content_length_invalid",
414
+ 502,
415
+ false,
416
+ );
465
417
  }
466
-
467
- arrayBuffer(): Promise<ArrayBuffer> {
468
- return this.response.arrayBuffer();
418
+ const parsed = Number(value);
419
+ if (!Number.isSafeInteger(parsed)) {
420
+ throw new ManagedRuntimeGatewayError(
421
+ "managed_runtime_object_content_length_invalid",
422
+ 502,
423
+ false,
424
+ );
469
425
  }
426
+ return parsed;
427
+ }
470
428
 
471
- text(): Promise<string> {
472
- return this.response.text();
429
+ /**
430
+ * Validate a successful object response without consuming its body. The
431
+ * returned stream enforces the configured byte ceiling as it is read.
432
+ */
433
+ async function checkedStreamingResponse(
434
+ response: Response,
435
+ maxBytes: number,
436
+ ): Promise<Response> {
437
+ const declaredLength = parseContentLength(
438
+ response.headers.get("content-length"),
439
+ );
440
+ if (declaredLength !== undefined && declaredLength > maxBytes) {
441
+ await response.body?.cancel("managed_runtime_response_too_large");
442
+ throw new ManagedRuntimeGatewayError(
443
+ "managed_runtime_response_too_large",
444
+ 502,
445
+ false,
446
+ );
473
447
  }
474
-
475
- async json<T = unknown>(): Promise<T> {
476
- return (await this.response.json()) as T;
448
+ if (!response.ok) {
449
+ const failure = await managedRuntimeGatewayFailure(response);
450
+ throw new ManagedRuntimeGatewayError(
451
+ failure?.code ?? "managed_runtime_request_failed",
452
+ failure?.status ?? response.status,
453
+ failure?.retryable ?? false,
454
+ );
477
455
  }
456
+ if (!response.body) return response;
457
+ return new Response(boundedReadableStream(response.body, maxBytes), {
458
+ status: response.status,
459
+ statusText: response.statusText,
460
+ headers: response.headers,
461
+ });
478
462
  }
479
463
 
480
- function objectMetadata(headers: Headers): ObjectMetadata {
481
- const contentLength = headers.get("content-length");
482
- const custom = headers.get("x-takosumi-object-custom-metadata");
483
- let customMetadata: Record<string, string> | undefined;
484
- if (custom !== null) {
485
- try {
486
- const decoded = JSON.parse(decodeURIComponent(custom)) as unknown;
487
- if (
488
- decoded === null ||
489
- typeof decoded !== "object" ||
490
- Array.isArray(decoded) ||
491
- Object.values(decoded).some((value) => typeof value !== "string")
492
- ) {
493
- throw new Error("invalid");
494
- }
495
- customMetadata = decoded as Record<string, string>;
496
- } catch {
497
- throw new ManagedRuntimeGatewayError(
498
- "managed_runtime_object_metadata_invalid",
499
- 502,
500
- false,
501
- );
502
- }
503
- }
504
- const httpMetadata = {
505
- ...(headers.get("content-type")
506
- ? { contentType: headers.get("content-type")! }
507
- : {}),
508
- ...(headers.get("cache-control")
509
- ? { cacheControl: headers.get("cache-control")! }
510
- : {}),
511
- ...(headers.get("content-disposition")
512
- ? { contentDisposition: headers.get("content-disposition")! }
513
- : {}),
514
- ...(headers.get("content-encoding")
515
- ? { contentEncoding: headers.get("content-encoding")! }
516
- : {}),
517
- ...(headers.get("content-language")
518
- ? { contentLanguage: headers.get("content-language")! }
519
- : {}),
520
- };
521
- return {
522
- ...(headers.get("content-type")
523
- ? { contentType: headers.get("content-type")! }
524
- : {}),
525
- ...(contentLength !== null &&
526
- /^\d+$/u.test(contentLength) &&
527
- Number.isSafeInteger(Number(contentLength))
528
- ? { contentLength: Number(contentLength) }
529
- : {}),
530
- ...(headers.get("etag") ? { etag: headers.get("etag")! } : {}),
531
- ...(Object.keys(httpMetadata).length === 0 ? {} : { httpMetadata }),
532
- ...(customMetadata === undefined ? {} : { customMetadata }),
464
+ function boundedReadableStream(
465
+ stream: ReadableStream<Uint8Array>,
466
+ maxBytes: number,
467
+ ): ReadableStream<Uint8Array> {
468
+ const reader = stream.getReader();
469
+ let size = 0;
470
+ let finished = false;
471
+ const finish = async (): Promise<void> => {
472
+ if (finished) return;
473
+ finished = true;
474
+ reader.releaseLock();
533
475
  };
476
+ return new ReadableStream<Uint8Array>(
477
+ {
478
+ async pull(controller) {
479
+ try {
480
+ const result = await reader.read();
481
+ if (result.done) {
482
+ controller.close();
483
+ await finish();
484
+ return;
485
+ }
486
+ size += result.value.byteLength;
487
+ if (size > maxBytes) {
488
+ await reader.cancel("managed_runtime_response_too_large");
489
+ controller.error(
490
+ new ManagedRuntimeGatewayError(
491
+ "managed_runtime_response_too_large",
492
+ 502,
493
+ false,
494
+ ),
495
+ );
496
+ await finish();
497
+ return;
498
+ }
499
+ controller.enqueue(result.value);
500
+ } catch (error) {
501
+ controller.error(error);
502
+ await finish();
503
+ }
504
+ },
505
+ async cancel(reason) {
506
+ await reader.cancel(reason).catch(() => undefined);
507
+ await finish();
508
+ },
509
+ },
510
+ { highWaterMark: 0 },
511
+ );
534
512
  }
535
513
 
536
514
  async function checkedResponse(