chore: support typed arrays in indexeddb (#34949)

This commit is contained in:
Simon Knott 2025-03-26 18:04:45 +01:00 committed by GitHub
parent febb95a638
commit 3340855109
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 131 additions and 15 deletions

View File

@ -1527,10 +1527,6 @@ Returns storage state for this browser context, contains current cookies, local
Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) in the storage state snapshot. Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) in the storage state snapshot.
If your application uses IndexedDB to store authentication tokens, like Firebase Authentication, enable this. 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 ## property: BrowserContext.tracing
* since: v1.12 * since: v1.12
- type: <[Tracing]> - type: <[Tracing]>

View File

@ -9274,9 +9274,6 @@ export interface BrowserContext {
* Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) in the storage * Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) in the storage
* state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase Authentication, * state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase Authentication,
* enable this. * enable this.
*
* **NOTE** IndexedDBs with typed arrays are currently not supported.
*
*/ */
indexedDB?: boolean; indexedDB?: boolean;

View File

@ -57,6 +57,10 @@ function innerParseSerializedValue(value: SerializedValue, handles: any[] | unde
} }
if (value.r !== undefined) if (value.r !== undefined)
return new RegExp(value.r.p, value.r.f); return new RegExp(value.r.p, value.r.f);
if (value.ta !== undefined) {
const ctor = typedArrayKindToConstructor[value.ta.k] as any;
return new ctor(value.ta.b.buffer, value.ta.b.byteOffset, value.ta.b.length);
}
if (value.a !== undefined) { if (value.a !== undefined) {
const result: any[] = []; const result: any[] = [];
@ -128,6 +132,10 @@ function innerSerializeValue(value: any, handleSerializer: (value: any) => Handl
if (isRegExp(value)) if (isRegExp(value))
return { r: { p: value.source, f: value.flags } }; return { r: { p: value.source, f: value.flags } };
const typedArrayKind = constructorToTypedArrayKind.get(value.constructor);
if (typedArrayKind)
return { ta: { b: Buffer.from(value.buffer, value.byteOffset, value.length), k: typedArrayKind } };
const id = visitorInfo.visited.get(value); const id = visitorInfo.visited.get(value);
if (id) if (id)
return { ref: id }; return { ref: id };
@ -178,3 +186,20 @@ function isError(obj: any): obj is Error {
const proto = obj ? Object.getPrototypeOf(obj) : null; const proto = obj ? Object.getPrototypeOf(obj) : null;
return obj instanceof Error || proto?.name === 'Error' || (proto && isError(proto)); return obj instanceof Error || proto?.name === 'Error' || (proto && isError(proto));
} }
type TypedArrayKind = NonNullable<SerializedValue['ta']>['k'];
const typedArrayKindToConstructor: Record<TypedArrayKind, Function> = {
i8: Int8Array,
ui8: Uint8Array,
ui8c: Uint8ClampedArray,
i16: Int16Array,
ui16: Uint16Array,
i32: Int32Array,
ui32: Uint32Array,
f32: Float32Array,
f64: Float64Array,
bi64: BigInt64Array,
bui64: BigUint64Array,
};
const constructorToTypedArrayKind: Map<Function, TypedArrayKind> = new Map(Object.entries(typedArrayKindToConstructor).map(([k, v]) => [v, k as TypedArrayKind]));

View File

@ -58,6 +58,10 @@ scheme.SerializedValue = tObject({
d: tOptional(tString), d: tOptional(tString),
u: tOptional(tString), u: tOptional(tString),
bi: tOptional(tString), bi: tOptional(tString),
ta: tOptional(tObject({
b: tBinary,
k: tEnum(['i8', 'ui8', 'ui8c', 'i16', 'ui16', 'i32', 'ui32', 'f32', 'f64', 'bi64', 'bui64']),
})),
e: tOptional(tObject({ e: tOptional(tObject({
m: tString, m: tString,
n: tString, n: tString,

View File

@ -16,6 +16,8 @@
import type { Builtins } from './builtins'; import type { Builtins } from './builtins';
type TypedArrayKind = 'i8' | 'ui8' | 'ui8c' | 'i16' | 'ui16' | 'i32' | 'ui32' | 'f32' | 'f64' | 'bi64' | 'bui64';
export type SerializedValue = export type SerializedValue =
undefined | boolean | number | string | undefined | boolean | number | string |
{ v: 'null' | 'undefined' | 'NaN' | 'Infinity' | '-Infinity' | '-0' } | { v: 'null' | 'undefined' | 'NaN' | 'Infinity' | '-Infinity' | '-0' } |
@ -27,7 +29,8 @@ export type SerializedValue =
{ a: SerializedValue[], id: number } | { a: SerializedValue[], id: number } |
{ o: { k: string, v: SerializedValue }[], id: number } | { o: { k: string, v: SerializedValue }[], id: number } |
{ ref: number } | { ref: number } |
{ h: number }; { h: number } |
{ ta: { b: string, k: TypedArrayKind } };
type HandleOrValue = { h: number } | { fallThrough: any }; type HandleOrValue = { h: number } | { fallThrough: any };
@ -70,6 +73,48 @@ export function source(builtins: Builtins) {
} }
} }
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<TypedArrayKind, Function> = {
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) {
/**
* 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, array.byteOffset, array.byteLength)).map(b => String.fromCharCode(b)).join('');
return btoa(binary);
}
function base64ToTypedArray(base64: string, TypedArrayConstructor: any) {
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: Builtins.Map<number, object> = new builtins.Map()): any { function parseEvaluationResultValue(value: SerializedValue, handles: any[] = [], refs: Builtins.Map<number, object> = new builtins.Map()): any {
if (Object.is(value, undefined)) if (Object.is(value, undefined))
return undefined; return undefined;
@ -121,6 +166,8 @@ export function source(builtins: Builtins) {
} }
if ('h' in value) if ('h' in value)
return handles[value.h]; return handles[value.h];
if ('ta' in value)
return base64ToTypedArray(value.ta.b, typedArrayConstructors[value.ta.k]);
} }
return value; return value;
} }
@ -191,6 +238,10 @@ export function source(builtins: Builtins) {
return { u: value.toJSON() }; return { u: value.toJSON() };
if (isRegExp(value)) if (isRegExp(value))
return { r: { p: value.source, f: value.flags } }; return { r: { p: value.source, f: value.flags } };
for (const [k, ctor] of Object.entries(typedArrayConstructors) as [TypedArrayKind, Function][]) {
if (isTypedArray(value, ctor))
return { ta: { b: typedArrayToBase64(value), k } };
}
const id = visitorInfo.visited.get(value); const id = visitorInfo.visited.get(value);
if (id) if (id)

View File

@ -9274,9 +9274,6 @@ export interface BrowserContext {
* Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) in the storage * Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) in the storage
* state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase Authentication, * state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase Authentication,
* enable this. * enable this.
*
* **NOTE** IndexedDBs with typed arrays are currently not supported.
*
*/ */
indexedDB?: boolean; indexedDB?: boolean;

View File

@ -180,6 +180,10 @@ export type SerializedValue = {
d?: string, d?: string,
u?: string, u?: string,
bi?: string, bi?: string,
ta?: {
b: Binary,
k: 'i8' | 'ui8' | 'ui8c' | 'i16' | 'ui16' | 'i32' | 'ui32' | 'f32' | 'f64' | 'bi64' | 'bui64',
},
e?: { e?: {
m: string, m: string,
n: string, n: string,

View File

@ -82,6 +82,25 @@ SerializedValue:
u: string? u: string?
# String representation of BigInt. # String representation of BigInt.
bi: string? bi: string?
# Typed array.
ta:
type: object?
properties:
b: binary
k:
type: enum
literals:
- i8
- ui8
- ui8c
- i16
- ui16
- i32
- ui32
- f32
- f64
- bi64
- bui64
# Serialized Error object. # Serialized Error object.
e: e:
type: object? type: object?

View File

@ -98,7 +98,7 @@ it('should round-trip through the file', async ({ contextFactory }, testInfo) =>
.put({ name: 'foo', date: new Date(0), null: null }); .put({ name: 'foo', date: new Date(0), null: null });
transaction transaction
.objectStore('store2') .objectStore('store2')
.put('bar', 'foo'); .put(new TextEncoder().encode('bar'), 'foo');
transaction.addEventListener('complete', resolve); transaction.addEventListener('complete', resolve);
transaction.addEventListener('error', reject); transaction.addEventListener('error', reject);
}; };
@ -124,16 +124,18 @@ it('should round-trip through the file', async ({ contextFactory }, testInfo) =>
expect(cookie).toEqual('username=John Doe'); expect(cookie).toEqual('username=John Doe');
const idbValues = await page2.evaluate(() => new Promise((resolve, reject) => { const idbValues = await page2.evaluate(() => new Promise((resolve, reject) => {
const openRequest = indexedDB.open('db', 42); const openRequest = indexedDB.open('db', 42);
openRequest.addEventListener('success', () => { openRequest.addEventListener('success', async () => {
const db = openRequest.result; const db = openRequest.result;
const transaction = db.transaction(['store', 'store2'], 'readonly'); const transaction = db.transaction(['store', 'store2'], 'readonly');
const request1 = transaction.objectStore('store').get('foo'); const request1 = transaction.objectStore('store').get('foo');
const request2 = transaction.objectStore('store2').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('success', () => resolve(request.result));
request.addEventListener('error', () => reject(request.error)); request.addEventListener('error', () => reject(request.error));
}))).then(resolve, reject); })));
resolve([result1, new TextDecoder().decode(result2 as any)]);
}); });
openRequest.addEventListener('error', () => reject(openRequest.error)); openRequest.addEventListener('error', () => reject(openRequest.error));
})); }));

View File

@ -94,6 +94,27 @@ it('should transfer arrays as arrays, not objects', async ({ page }) => {
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('should transfer typed arrays', async ({ page }) => {
const testCases = [
new Int8Array([1, 2, 3]),
new Uint8Array([1, 2, 3]),
new Uint8ClampedArray([1, 2, 3]),
new Int16Array([1, 2, 3]),
new Uint16Array([1, 2, 3]),
new Int32Array([1, 2, 3]),
new Uint32Array([1, 2, 3]),
new Float32Array([1.1, 2.2, 3.3]),
new Float64Array([1.1, 2.2, 3.3]),
new BigInt64Array([1n, 2n, 3n]),
new BigUint64Array([1n, 2n, 3n])
];
for (const typedArray of testCases) {
const result = await page.evaluate(a => a, typedArray);
expect(result).toEqual(typedArray);
}
});
it('should transfer bigint', async ({ page }) => { it('should transfer bigint', async ({ page }) => {
expect(await page.evaluate(() => 42n)).toBe(42n); expect(await page.evaluate(() => 42n)).toBe(42n);
expect(await page.evaluate(a => a, 17n)).toBe(17n); expect(await page.evaluate(a => a, 17n)).toBe(17n);