1// Alchemy modifications are licensed under Apache-2.0.
2// This file includes third-party code; see /THIRD_PARTY_LICENSES.md.
3type ViewConstructor = new (buffer: ArrayBufferLike) => ArrayBufferView;
4
5function 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 */
38export 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}