Skip to content

Commit

Permalink
feat(ses): shim ArrayBuffer.p.transfer
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Sep 2, 2024
1 parent cc82132 commit 06e15fe
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 0 deletions.
4 changes: 4 additions & 0 deletions packages/ses/src/commons.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export { universalThis as globalThis };

export const {
Array,
ArrayBuffer,
Date,
FinalizationRegistry,
Float32Array,
Expand Down Expand Up @@ -124,6 +125,7 @@ export const {
} = Reflect;

export const { isArray, prototype: arrayPrototype } = Array;
export const { prototype: arrayBufferPrototype } = ArrayBuffer;
export const { prototype: mapPrototype } = Map;
export const { revocable: proxyRevocable } = Proxy;
export const { prototype: regexpPrototype } = RegExp;
Expand Down Expand Up @@ -178,6 +180,8 @@ export const arraySome = uncurryThis(arrayPrototype.some);
export const arraySort = uncurryThis(arrayPrototype.sort);
export const iterateArray = uncurryThis(arrayPrototype[iteratorSymbol]);
//
export const arrayBufferSlice = uncurryThis(arrayBufferPrototype.slice);
//
export const mapSet = uncurryThis(mapPrototype.set);
export const mapGet = uncurryThis(mapPrototype.get);
export const mapHas = uncurryThis(mapPrototype.has);
Expand Down
2 changes: 2 additions & 0 deletions packages/ses/src/lockdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { tameHarden } from './tame-harden.js';
import { tameSymbolConstructor } from './tame-symbol-constructor.js';
import { tameFauxDataProperties } from './tame-faux-data-properties.js';
import { tameRegeneratorRuntime } from './tame-regenerator-runtime.js';
import { shimArrayBufferTransfer } from './shim-arraybuffer-transfer.js';

/** @import {LockdownOptions} from '../types.js' */

Expand Down Expand Up @@ -284,6 +285,7 @@ export const repairIntrinsics = (options = {}) => {
addIntrinsics(tameMathObject(mathTaming));
addIntrinsics(tameRegExpConstructor(regExpTaming));
addIntrinsics(tameSymbolConstructor());
shimArrayBufferTransfer();

addIntrinsics(getAnonymousIntrinsics());

Expand Down
61 changes: 61 additions & 0 deletions packages/ses/src/shim-arraybuffer-transfer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
arrayBufferPrototype,
arrayBufferSlice,
globalThis,
TypeError,
defineProperty,
} from './commons.js';

export const shimArrayBufferTransfer = () => {
// @ts-expect-error TODO extend ArrayBuffer type to include transfer, etc.
if (typeof arrayBufferPrototype.transfer === 'function') {
// Assume already exists so does not need to be shimmed.
// Such conditional shimming is ok in this case since ArrayBuffer.p.transfer
// is already officially part of JS.
return;
}
const clone = globalThis.structuredClone;
if (typeof clone !== 'function') {
// Indeed, Node <= 16 has neither.
throw TypeError(
`Can only shim missing ArrayBuffer.prototype.transfer on a platform with "structuredClone"`,
);
}

/**
* @type {ThisType<ArrayBuffer>}
*/
const methods = {
/**
* @param {number} [newLength]
*/
transfer(newLength = undefined) {
// Hopefully, a zero-length slice is cheap, but still enforces that
// `this` is a genuine `ArrayBuffer` exotic object.
arrayBufferSlice(this, 0, 0);
const oldLength = this.byteLength;
if (newLength === undefined || newLength === oldLength) {
return clone(this, { transfer: [this] });
}
if (typeof newLength !== 'number') {
throw new TypeError(`transfer newLength if provided must be a number`);
}
if (newLength > oldLength) {
// TODO support this case somehow
throw new TypeError(
`Cannot yet emulate transfer to larger ArrayBuffer ${newLength}`,
);
}
const tmp = clone(this, { transfer: [this] });
return arrayBufferSlice(tmp, 0, newLength);
},
};

defineProperty(arrayBufferPrototype, 'transfer', {
// @ts-expect-error
value: methods.transfer,
writable: true,
enumerable: false,
configurable: true,
});
};
42 changes: 42 additions & 0 deletions packages/ses/test/shim-arraybuffer-transfer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import test from 'ava';
import '../index.js';

lockdown();

// The purpose of this test is to see if Array.prototype.transfer works
// correctly enough on platforms like Node 18 or Node 20 that don't yet have
// it natively, and so are testing the shim on those. On platforms where
// Array.prototype.transfer is present, like Node 22,
// we also run the same tests.Thus,
// this test only tests the intersection behavior of the standard and
// the shim. The shim does not yet support a `newLength` argument
// larger than the original.
//
// TODO once the shim supports transfering to a larger length, we must
// test that as well.

test('ArrayBuffer.p.transfer', t => {
const abX = new ArrayBuffer(3);
t.is(abX.byteLength, 3);
const taX = new Uint8Array(abX);
t.is(taX[2], 0);
t.is(taX[3], undefined);

// because this test must run on platforms prior to
// ArrayBuffer.prototype.detached, we test detachment by other means.

const abY = abX.transfer();
t.is(abY.byteLength, 3);
t.is(abX.byteLength, 0);
const taY = new Uint8Array(abY);
t.is(taX[2], undefined);
t.is(taY[2], 0);

const abZ = abY.transfer(2);
t.is(abY.byteLength, 0);
t.is(abZ.byteLength, 2);
const taZ = new Uint8Array(abZ);
t.is(taY[2], undefined);
t.is(taZ[2], undefined);
t.is(taZ[1], 0);
});

0 comments on commit 06e15fe

Please sign in to comment.