From 1837b1bf4b8ab2b35687a1379833f1a98c7b7b7f Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 27 Feb 2025 14:24:04 +0100 Subject: [PATCH 1/6] chore: support typed arrays in indexeddb --- docs/src/api/class-browsercontext.md | 4 -- packages/playwright-client/types/types.d.ts | 3 -- .../isomorphic/utilityScriptSerializers.ts | 47 ++++++++++++++++++- packages/playwright-core/types/types.d.ts | 3 -- .../browsercontext-storage-state.spec.ts | 10 ++-- 5 files changed, 52 insertions(+), 15 deletions(-) diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 91e43e22dd600..fbcde01a45b97 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -1530,10 +1530,6 @@ Returns storage state for this browser context, contains current cookies, local Set to `true` to include IndexedDB in the storage state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase Authentication, enable this. -:::note -IndexedDBs with typed arrays are currently not supported. -::: - ## property: BrowserContext.tracing * since: v1.12 - type: <[Tracing]> diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 26115f729a6f6..2f41dee5e8c55 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -9273,9 +9273,6 @@ export interface BrowserContext { /** * Set to `true` to include IndexedDB in the storage state snapshot. If your application uses IndexedDB to store * authentication tokens, like Firebase Authentication, enable this. - * - * **NOTE** IndexedDBs with typed arrays are currently not supported. - * */ indexedDB?: boolean; diff --git a/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts b/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts index 6df7988caf7f9..3c758cbe92123 100644 --- a/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts +++ b/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +type TypedArrayKind = 'i8' | 'ui8' | 'ui8c' | 'i16' | 'ui16' | 'i32' | 'ui32' | 'f32' | 'f64' | 'bi64' | 'bui64'; + export type SerializedValue = undefined | boolean | number | string | { v: 'null' | 'undefined' | 'NaN' | 'Infinity' | '-Infinity' | '-0' } | @@ -25,7 +27,8 @@ export type SerializedValue = { a: SerializedValue[], id: number } | { o: { k: string, v: SerializedValue }[], id: number } | { ref: number } | - { h: number }; + { h: number } | + { ta: { b: string, k: TypedArrayKind } }; export type HandleOrValue = { h: number } | { fallThrough: any }; @@ -68,6 +71,42 @@ export function source() { } } + const typedArrayCtors: Record = { + i8: Int8Array, + ui8: Uint8Array, + ui8c: Uint8ClampedArray, + i16: Int16Array, + ui16: Uint16Array, + i32: Int32Array, + ui32: Uint32Array, + // TODO: add Float16Array once it's in baseline + f32: Float32Array, + f64: Float64Array, + bi64: BigInt64Array, + bui64: BigUint64Array, + }; + + function typedArrayToBase64(array: any) { + if (globalThis.Buffer) + return Buffer.from(array).toString('base64'); + + const binary = Array.from(new Uint8Array(array.buffer)).map(b => String.fromCharCode(b)).join(''); + return btoa(binary); + } + + function base64ToTypedArray(base64: string, TypedArrayConstructor: any) { + if (globalThis.Buffer) { + const buf = Buffer.from(base64, 'base64'); + return new TypedArrayConstructor(buf.buffer, buf.byteOffset, buf.byteLength / buf.BYTES_PER_ELEMENT); + } + + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) + bytes[i] = binary.charCodeAt(i); + return new TypedArrayConstructor(bytes.buffer); + } + function parseEvaluationResultValue(value: SerializedValue, handles: any[] = [], refs: Map = new Map()): any { if (Object.is(value, undefined)) return undefined; @@ -119,6 +158,8 @@ export function source() { } if ('h' in value) return handles[value.h]; + if ('ta' in value) + return base64ToTypedArray(value.ta.b, typedArrayCtors[value.ta.k]); } return value; } @@ -186,6 +227,10 @@ export function source() { return { u: value.toJSON() }; if (isRegExp(value)) return { r: { p: value.source, f: value.flags } }; + for (const [k, ctor] of Object.entries(typedArrayCtors) as [TypedArrayKind, Function][]) { + if (value instanceof ctor) + return { ta: { b: typedArrayToBase64(value), k } }; + } const id = visitorInfo.visited.get(value); if (id) diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 26115f729a6f6..2f41dee5e8c55 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -9273,9 +9273,6 @@ export interface BrowserContext { /** * Set to `true` to include IndexedDB in the storage state snapshot. If your application uses IndexedDB to store * authentication tokens, like Firebase Authentication, enable this. - * - * **NOTE** IndexedDBs with typed arrays are currently not supported. - * */ indexedDB?: boolean; diff --git a/tests/library/browsercontext-storage-state.spec.ts b/tests/library/browsercontext-storage-state.spec.ts index e22c66f23cb4f..56f1540a3a514 100644 --- a/tests/library/browsercontext-storage-state.spec.ts +++ b/tests/library/browsercontext-storage-state.spec.ts @@ -100,7 +100,7 @@ it('should round-trip through the file', async ({ contextFactory }, testInfo) => .put({ name: 'foo', date: new Date(0) }); transaction .objectStore('store2') - .put('bar', 'foo'); + .put(new TextEncoder().encode('bar'), 'foo'); transaction.addEventListener('complete', resolve); transaction.addEventListener('error', reject); }; @@ -126,16 +126,18 @@ it('should round-trip through the file', async ({ contextFactory }, testInfo) => expect(cookie).toEqual('username=John Doe'); const idbValues = await page2.evaluate(() => new Promise((resolve, reject) => { const openRequest = indexedDB.open('db', 42); - openRequest.addEventListener('success', () => { + openRequest.addEventListener('success', async () => { const db = openRequest.result; const transaction = db.transaction(['store', 'store2'], 'readonly'); const request1 = transaction.objectStore('store').get('foo'); const request2 = transaction.objectStore('store2').get('foo'); - Promise.all([request1, request2].map(request => new Promise((resolve, reject) => { + const [result1, result2] = await Promise.all([request1, request2].map(request => new Promise((resolve, reject) => { request.addEventListener('success', () => resolve(request.result)); request.addEventListener('error', () => reject(request.error)); - }))).then(resolve, reject); + }))); + + resolve([result1, new TextDecoder().decode(result2 as any)]); }); openRequest.addEventListener('error', () => reject(openRequest.error)); })); From ee70854b8e54f89a30180056b3bb07773fa13f25 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 27 Feb 2025 14:52:11 +0100 Subject: [PATCH 2/6] rename --- .../src/server/isomorphic/utilityScriptSerializers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts b/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts index 3c758cbe92123..b3b3e99da1ea3 100644 --- a/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts +++ b/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts @@ -71,7 +71,7 @@ export function source() { } } - const typedArrayCtors: Record = { + const typedArrayConstructors: Record = { i8: Int8Array, ui8: Uint8Array, ui8c: Uint8ClampedArray, @@ -159,7 +159,7 @@ export function source() { if ('h' in value) return handles[value.h]; if ('ta' in value) - return base64ToTypedArray(value.ta.b, typedArrayCtors[value.ta.k]); + return base64ToTypedArray(value.ta.b, typedArrayConstructors[value.ta.k]); } return value; } @@ -227,7 +227,7 @@ export function source() { return { u: value.toJSON() }; if (isRegExp(value)) return { r: { p: value.source, f: value.flags } }; - for (const [k, ctor] of Object.entries(typedArrayCtors) as [TypedArrayKind, Function][]) { + for (const [k, ctor] of Object.entries(typedArrayConstructors) as [TypedArrayKind, Function][]) { if (value instanceof ctor) return { ta: { b: typedArrayToBase64(value), k } }; } From dfd44ee743424a63e096e63214f58907c49e577d Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 28 Feb 2025 08:48:43 +0100 Subject: [PATCH 3/6] remove node code --- .../src/server/isomorphic/utilityScriptSerializers.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts b/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts index b3b3e99da1ea3..1d5c5a3a772cd 100644 --- a/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts +++ b/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts @@ -87,19 +87,11 @@ export function source() { }; function typedArrayToBase64(array: any) { - if (globalThis.Buffer) - return Buffer.from(array).toString('base64'); - const binary = Array.from(new Uint8Array(array.buffer)).map(b => String.fromCharCode(b)).join(''); return btoa(binary); } function base64ToTypedArray(base64: string, TypedArrayConstructor: any) { - if (globalThis.Buffer) { - const buf = Buffer.from(base64, 'base64'); - return new TypedArrayConstructor(buf.buffer, buf.byteOffset, buf.byteLength / buf.BYTES_PER_ELEMENT); - } - const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) From 1610cca9598a1b27d6b0fda585fc528c73c2260a Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 28 Feb 2025 10:36:03 +0100 Subject: [PATCH 4/6] fix firefox --- .../server/isomorphic/utilityScriptSerializers.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts b/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts index 1d5c5a3a772cd..84d54f3cc35e5 100644 --- a/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts +++ b/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts @@ -71,6 +71,14 @@ export function source() { } } + function isTypedArray(obj: any, constructor: Function): boolean { + try { + return obj instanceof constructor || Object.prototype.toString.call(obj) === `[object ${constructor.name}]`; + } catch (error) { + return false; + } + } + const typedArrayConstructors: Record = { i8: Int8Array, ui8: Uint8Array, @@ -87,6 +95,8 @@ export function source() { }; function typedArrayToBase64(array: any) { + if ('toBase64' in array) + return array.toBase64(); const binary = Array.from(new Uint8Array(array.buffer)).map(b => String.fromCharCode(b)).join(''); return btoa(binary); } @@ -220,7 +230,7 @@ export function source() { if (isRegExp(value)) return { r: { p: value.source, f: value.flags } }; for (const [k, ctor] of Object.entries(typedArrayConstructors) as [TypedArrayKind, Function][]) { - if (value instanceof ctor) + if (isTypedArray(value, ctor)) return { ta: { b: typedArrayToBase64(value), k } }; } From 37f1f9febbd5741912822058d49db886fe718b4a Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 28 Feb 2025 10:39:52 +0100 Subject: [PATCH 5/6] add comment --- .../src/server/isomorphic/utilityScriptSerializers.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts b/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts index 84d54f3cc35e5..12dd3d41aaa7d 100644 --- a/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts +++ b/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts @@ -95,6 +95,10 @@ export function source() { }; function typedArrayToBase64(array: any) { + /** + * Firefox does not support iterating over typed arrays, so we use `.toBase64`. + * Error: 'Accessing TypedArray data over Xrays is slow, and forbidden in order to encourage performant code. To copy TypedArrays across origin boundaries, consider using Components.utils.cloneInto().' + */ if ('toBase64' in array) return array.toBase64(); const binary = Array.from(new Uint8Array(array.buffer)).map(b => String.fromCharCode(b)).join(''); From 769fda67ee0c5f460b8a45d4f4b49dec17b3936b Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 28 Feb 2025 12:13:46 +0100 Subject: [PATCH 6/6] supply offset and length --- .../src/server/isomorphic/utilityScriptSerializers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts b/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts index 12dd3d41aaa7d..1f8f22b82a320 100644 --- a/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts +++ b/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts @@ -101,7 +101,7 @@ export function source() { */ if ('toBase64' in array) return array.toBase64(); - const binary = Array.from(new Uint8Array(array.buffer)).map(b => String.fromCharCode(b)).join(''); + const binary = Array.from(new Uint8Array(array.buffer, array.byteOffset, array.byteLength)).map(b => String.fromCharCode(b)).join(''); return btoa(binary); }