| 1 | // Alchemy modifications are licensed under Apache-2.0. |
| 2 | // This file includes third-party code; see /THIRD_PARTY_LICENSES.md. |
| 3 | type ViewConstructor = new (buffer: ArrayBufferLike) => ArrayBufferView; |
| 4 | |
| 5 | function buildCompactView(view: ArrayBufferView): ArrayBufferView { |
| 6 | const tightBuffer = view.buffer.slice( |
| 7 | view.byteOffset, |
| 8 | view.byteOffset + view.byteLength, |
| 9 | ); |
| 10 | return new (view.constructor as ViewConstructor)(tightBuffer); |
| 11 | } |
| 12 | |
| 13 | /** |
| 14 | * Returns a copy of `value` that is safe to persist via Durable Object storage |
| 15 | * without dragging unrelated bytes along with typed-array views. |
| 16 | * |
| 17 | * Background: workerd's `v8::ValueSerializer` writes the entire backing |
| 18 | * `ArrayBuffer` for typed-array views, not just `byteLength` bytes. A view |
| 19 | * sliced from a much larger buffer (`crypto.getRandomValues`, `arr.slice(...)`, |
| 20 | * fetch-stream copies) blows up the wire size by a factor of |
| 21 | * (backing-size / view-size) and can hit `SQLITE_TOOBIG` at view sizes well |
| 22 | * below the documented 1MiB output limit (see issue #14101). Copying the |
| 23 | * view's bytes into a tight backing buffer before persistence brings local |
| 24 | * `wrangler dev` behaviour in line with production. |
| 25 | * |
| 26 | * The walk is recursive (cycle-safe via a `WeakMap`) so views nested inside |
| 27 | * objects, arrays, Maps, and Sets are also compacted. View types are preserved |
| 28 | * (`Uint8Array` stays `Uint8Array`, `Int16Array` stays `Int16Array`, etc.) so |
| 29 | * the persisted shape matches the live shape. Class instances and host objects |
| 30 | * (`Date`, `RegExp`, raw `ArrayBuffer`, streams, `Blob`, …) are passed through |
| 31 | * unchanged — recursing into them would either fail to reconstruct the |
| 32 | * original type or trigger their own structured-clone path. |
| 33 | * |
| 34 | * @param value The value to normalize for storage. |
| 35 | * @param seen Previously visited objects, used to preserve cycles. |
| 36 | * @returns A storage-safe copy of the value. |
| 37 | */ |
| 38 | export function normalizeForStorage( |
| 39 | value: unknown, |
| 40 | seen: WeakMap<object, unknown> = new WeakMap(), |
| 41 | ): unknown { |
| 42 | if (value === null || typeof value !== "object") { |
| 43 | return value; |
| 44 | } |
| 45 | |
| 46 | if (seen.has(value)) { |
| 47 | return seen.get(value); |
| 48 | } |
| 49 | |
| 50 | if (ArrayBuffer.isView(value)) { |
| 51 | return buildCompactView(value); |
| 52 | } |
| 53 | |
| 54 | if (Array.isArray(value)) { |
| 55 | const result: Array<unknown> = []; |
| 56 | seen.set(value, result); |
| 57 | for (const item of value) { |
| 58 | result.push(normalizeForStorage(item, seen)); |
| 59 | } |
| 60 | return result; |
| 61 | } |
| 62 | |
| 63 | if (value instanceof Map) { |
| 64 | const result = new Map<unknown, unknown>(); |
| 65 | seen.set(value, result); |
| 66 | for (const [key, item] of value) { |
| 67 | result.set( |
| 68 | normalizeForStorage(key, seen), |
| 69 | normalizeForStorage(item, seen), |
| 70 | ); |
| 71 | } |
| 72 | return result; |
| 73 | } |
| 74 | |
| 75 | if (value instanceof Set) { |
| 76 | const result = new Set<unknown>(); |
| 77 | seen.set(value, result); |
| 78 | for (const item of value) { |
| 79 | result.add(normalizeForStorage(item, seen)); |
| 80 | } |
| 81 | return result; |
| 82 | } |
| 83 | |
| 84 | const prototype = Object.getPrototypeOf(value); |
| 85 | if (prototype === Object.prototype || prototype === null) { |
| 86 | const result: Record<string, unknown> = {}; |
| 87 | seen.set(value, result); |
| 88 | for (const key of Object.keys(value)) { |
| 89 | result[key] = normalizeForStorage( |
| 90 | (value as Record<string, unknown>)[key], |
| 91 | seen, |
| 92 | ); |
| 93 | } |
| 94 | return result; |
| 95 | } |
| 96 | |
| 97 | return value; |
| 98 | } |