From daa1e2b59894600fea1f1669fa84d14ce307789e Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Mon, 13 Jan 2025 14:20:01 -0800 Subject: [PATCH] fix(immutable-arraybuffer): update to recent spec (#2688) Closes: https://github.com/tc39/proposal-immutable-arraybuffer/issues/26 Refs: https://github.com/tc39/proposal-immutable-arraybuffer https://github.com/tc39/proposal-immutable-arraybuffer/issues/15 https://github.com/tc39/proposal-immutable-arraybuffer/issues/9 https://github.com/tc39/proposal-immutable-arraybuffer/issues/16 ## Description Since the last work on this immutable-arraybuffer shim, at the tc39 plenary we decided that - `transferToImmutable` should take an optional `newLength` parameter, to stay parallel to the other `transfer*` methods. - The `sliceToImmutable` method should be added. The spec at https://github.com/tc39/proposal-immutable-arraybuffer and the Moddable XS implementation both already reflect these changes. This PR brings this shim up to date with those changes, closing https://github.com/tc39/proposal-immutable-arraybuffer/issues/26 ### Security Considerations none ### Scaling Considerations none ### Documentation Considerations The proposal's draft spec has already been updated. ### Testing Considerations New tests lightly test the new functionality. The code and tests may differ from the current draft spec on order of operations and errors thrown. But since these issues are purposely still open https://github.com/tc39/proposal-immutable-arraybuffer/issues/16 , this divergence is not yet a big deal. ### Compatibility Considerations No more than the baseline shim already on master. ### Upgrade Considerations No production code yet depends on this shim. So, none. --- packages/immutable-arraybuffer/index.js | 62 ++++++++++++-- packages/immutable-arraybuffer/shim.js | 13 ++- .../immutable-arraybuffer/test/index.test.js | 82 ++++++++++++++++++- .../immutable-arraybuffer/test/shim.test.js | 79 ++++++++++++++++++ 4 files changed, 223 insertions(+), 13 deletions(-) diff --git a/packages/immutable-arraybuffer/index.js b/packages/immutable-arraybuffer/index.js index 1dd47432d7..10625d142e 100644 --- a/packages/immutable-arraybuffer/index.js +++ b/packages/immutable-arraybuffer/index.js @@ -1,12 +1,14 @@ /* global globalThis */ -const { setPrototypeOf, getOwnPropertyDescriptor } = Object; +const { setPrototypeOf, getOwnPropertyDescriptors } = Object; const { apply } = Reflect; const { prototype: arrayBufferPrototype } = ArrayBuffer; const { slice, - // @ts-expect-error At the time of this writing, the `ArrayBuffer` type built + // TODO used to be a-ts-expect-error, but my local IDE's TS server + // seems to use a more recent definition of the `ArrayBuffer` type. + // @ts-ignore At the time of this writing, the `ArrayBuffer` type built // into TypeScript does not know about the recent standard `transfer` method. // Indeed, the `transfer` method is absent from Node <= 20. transfer, @@ -99,8 +101,13 @@ class ImmutableArrayBufferInternal { return true; } - slice(begin = undefined, end = undefined) { - return arrayBufferSlice(this.#buffer, begin, end); + slice(start = undefined, end = undefined) { + return arrayBufferSlice(this.#buffer, start, end); + } + + sliceToImmutable(start = undefined, end = undefined) { + // eslint-disable-next-line no-use-before-define + return sliceBufferToImmutable(this.#buffer, start, end); } resize(_newByteLength = undefined) { @@ -129,17 +136,36 @@ const immutableArrayBufferPrototype = ImmutableArrayBufferInternal.prototype; delete immutableArrayBufferPrototype.constructor; const { - // @ts-expect-error We know it is there. - get: isImmutableGetter, -} = getOwnPropertyDescriptor(immutableArrayBufferPrototype, 'immutable'); + slice: { value: sliceOfImmutable }, + immutable: { get: isImmutableGetter }, +} = getOwnPropertyDescriptors(immutableArrayBufferPrototype); setPrototypeOf(immutableArrayBufferPrototype, arrayBufferPrototype); -export const transferBufferToImmutable = buffer => - new ImmutableArrayBufferInternal(buffer); +export const transferBufferToImmutable = (buffer, newLength = undefined) => { + if (newLength !== undefined) { + if (transfer) { + buffer = apply(transfer, buffer, [newLength]); + } else { + buffer = arrayBufferTransfer(buffer); + const oldLength = buffer.byteLength; + // eslint-disable-next-line @endo/restrict-comparison-operands + if (newLength <= oldLength) { + buffer = arrayBufferSlice(buffer, 0, newLength); + } else { + const oldTA = new Uint8Array(buffer); + const newTA = new Uint8Array(newLength); + newTA.set(oldTA); + buffer = newTA.buffer; + } + } + } + return new ImmutableArrayBufferInternal(buffer); +}; export const isBufferImmutable = buffer => { try { + // @ts-expect-error Getter should be typed as this-sensitive return apply(isImmutableGetter, buffer, []); } catch (err) { if (err instanceof TypeError) { @@ -150,3 +176,21 @@ export const isBufferImmutable = buffer => { throw err; } }; + +const sliceBuffer = (buffer, start = undefined, end = undefined) => { + try { + // @ts-expect-error We know it is really there + return apply(sliceOfImmutable, buffer, [start, end]); + } catch (err) { + if (err instanceof TypeError) { + return arrayBufferSlice(buffer, start, end); + } + throw err; + } +}; + +export const sliceBufferToImmutable = ( + buffer, + start = undefined, + end = undefined, +) => transferBufferToImmutable(sliceBuffer(buffer, start, end)); diff --git a/packages/immutable-arraybuffer/shim.js b/packages/immutable-arraybuffer/shim.js index 8c0be9ba9c..fab02531c4 100644 --- a/packages/immutable-arraybuffer/shim.js +++ b/packages/immutable-arraybuffer/shim.js @@ -1,11 +1,18 @@ -import { transferBufferToImmutable, isBufferImmutable } from './index.js'; +import { + transferBufferToImmutable, + isBufferImmutable, + sliceBufferToImmutable, +} from './index.js'; const { getOwnPropertyDescriptors, defineProperties } = Object; const { prototype: arrayBufferPrototype } = ArrayBuffer; const arrayBufferMethods = { - transferToImmutable() { - return transferBufferToImmutable(this); + transferToImmutable(newLength = undefined) { + return transferBufferToImmutable(this, newLength); + }, + sliceToImmutable(start = undefined, end = undefined) { + return sliceBufferToImmutable(this, start, end); }, get immutable() { return isBufferImmutable(this); diff --git a/packages/immutable-arraybuffer/test/index.test.js b/packages/immutable-arraybuffer/test/index.test.js index 0505951780..7ecb7f396d 100644 --- a/packages/immutable-arraybuffer/test/index.test.js +++ b/packages/immutable-arraybuffer/test/index.test.js @@ -1,7 +1,8 @@ import test from 'ava'; import { transferBufferToImmutable, - // isBufferImmutable, + isBufferImmutable, + sliceBufferToImmutable, } from '../index.js'; const { isFrozen, getPrototypeOf } = Object; @@ -124,3 +125,82 @@ test('TypedArray on Immutable ArrayBuffer ponyfill limitations', t => { const ta3 = new Uint8Array(iab); t.is(ta3.byteLength, 0); }); + +const testTransfer = t => { + const ta12 = new Uint8Array([3, 4, 5]); + const ab12 = ta12.buffer; + t.is(ab12.byteLength, 3); + t.deepEqual([...ta12], [3, 4, 5]); + + const ab2 = ab12.transfer(5); + t.false(isBufferImmutable(ab2)); + t.is(ab2.byteLength, 5); + t.is(ab12.byteLength, 0); + const ta2 = new Uint8Array(ab2); + t.deepEqual([...ta2], [3, 4, 5, 0, 0]); + + const ta13 = new Uint8Array([3, 4, 5]); + const ab13 = ta13.buffer; + + const ab3 = ab13.transfer(2); + t.false(isBufferImmutable(ab3)); + t.is(ab3.byteLength, 2); + t.is(ab13.byteLength, 0); + const ta3 = new Uint8Array(ab3); + t.deepEqual([...ta3], [3, 4]); +}; + +{ + // `transfer` is absent in Node <= 20. Present in Node >= 22 + const maybeTest = 'transfer' in ArrayBuffer.prototype ? test : test.skip; + maybeTest('Standard buf.transfer(newLength) behavior baseline', testTransfer); +} + +test('Analogous transferBufferToImmutable(buf, newLength) ponyfill', t => { + const ta12 = new Uint8Array([3, 4, 5]); + const ab12 = ta12.buffer; + t.is(ab12.byteLength, 3); + t.deepEqual([...ta12], [3, 4, 5]); + + const ab2 = transferBufferToImmutable(ab12, 5); + t.true(isBufferImmutable(ab2)); + t.is(ab2.byteLength, 5); + t.is(ab12.byteLength, 0); + // slice needed due to ponyfill limitations. + const ta2 = new Uint8Array(ab2.slice()); + t.deepEqual([...ta2], [3, 4, 5, 0, 0]); + + const ta13 = new Uint8Array([3, 4, 5]); + const ab13 = ta13.buffer; + + const ab3 = transferBufferToImmutable(ab13, 2); + t.true(isBufferImmutable(ab3)); + t.is(ab3.byteLength, 2); + t.is(ab13.byteLength, 0); + // slice needed due to ponyfill limitations. + const ta3 = new Uint8Array(ab3.slice()); + t.deepEqual([...ta3], [3, 4]); +}); + +test('sliceBufferToImmutable ponyfill', t => { + const ta12 = new Uint8Array([3, 4, 5]); + const ab12 = ta12.buffer; + t.is(ab12.byteLength, 3); + t.deepEqual([...ta12], [3, 4, 5]); + + const ab2 = sliceBufferToImmutable(ab12, 1, 5); + t.true(isBufferImmutable(ab2)); + t.is(ab2.byteLength, 2); + t.is(ab12.byteLength, 3); + // slice needed due to ponyfill limitations. + const ta2 = new Uint8Array(ab2.slice()); + t.deepEqual([...ta2], [4, 5]); + + const ab3 = sliceBufferToImmutable(ab2, 1, 2); + t.true(isBufferImmutable(ab3)); + t.is(ab3.byteLength, 1); + t.is(ab2.byteLength, 2); + // slice needed due to ponyfill limitations. + const ta3 = new Uint8Array(ab3.slice()); + t.deepEqual([...ta3], [5]); +}); diff --git a/packages/immutable-arraybuffer/test/shim.test.js b/packages/immutable-arraybuffer/test/shim.test.js index 747d6a6939..631c0ad34e 100644 --- a/packages/immutable-arraybuffer/test/shim.test.js +++ b/packages/immutable-arraybuffer/test/shim.test.js @@ -121,3 +121,82 @@ test('TypedArray on Immutable ArrayBuffer shim limitations', t => { const ta3 = new Uint8Array(iab); t.is(ta3.byteLength, 0); }); + +const testTransfer = t => { + const ta12 = new Uint8Array([3, 4, 5]); + const ab12 = ta12.buffer; + t.is(ab12.byteLength, 3); + t.deepEqual([...ta12], [3, 4, 5]); + + const ab2 = ab12.transfer(5); + t.false(ab2.immutable); + t.is(ab2.byteLength, 5); + t.is(ab12.byteLength, 0); + const ta2 = new Uint8Array(ab2); + t.deepEqual([...ta2], [3, 4, 5, 0, 0]); + + const ta13 = new Uint8Array([3, 4, 5]); + const ab13 = ta13.buffer; + + const ab3 = ab13.transfer(2); + t.false(ab3.immutable); + t.is(ab3.byteLength, 2); + t.is(ab13.byteLength, 0); + const ta3 = new Uint8Array(ab3); + t.deepEqual([...ta3], [3, 4]); +}; + +{ + // `transfer` is absent in Node <= 20. Present in Node >= 22 + const maybeTest = 'transfer' in ArrayBuffer.prototype ? test : test.skip; + maybeTest('Standard buf.transfer(newLength) behavior baseline', testTransfer); +} + +test('Analogous buf.transferToImmutable(newLength) shim', t => { + const ta12 = new Uint8Array([3, 4, 5]); + const ab12 = ta12.buffer; + t.is(ab12.byteLength, 3); + t.deepEqual([...ta12], [3, 4, 5]); + + const ab2 = ab12.transferToImmutable(5); + t.true(ab2.immutable); + t.is(ab2.byteLength, 5); + t.is(ab12.byteLength, 0); + // slice needed due to ponyfill limitations. + const ta2 = new Uint8Array(ab2.slice()); + t.deepEqual([...ta2], [3, 4, 5, 0, 0]); + + const ta13 = new Uint8Array([3, 4, 5]); + const ab13 = ta13.buffer; + + const ab3 = ab13.transferToImmutable(2); + t.true(ab3.immutable); + t.is(ab3.byteLength, 2); + t.is(ab13.byteLength, 0); + // slice needed due to ponyfill limitations. + const ta3 = new Uint8Array(ab3.slice()); + t.deepEqual([...ta3], [3, 4]); +}); + +test('sliceToImmutable shim', t => { + const ta12 = new Uint8Array([3, 4, 5]); + const ab12 = ta12.buffer; + t.is(ab12.byteLength, 3); + t.deepEqual([...ta12], [3, 4, 5]); + + const ab2 = ab12.sliceToImmutable(1, 5); + t.true(ab2.immutable); + t.is(ab2.byteLength, 2); + t.is(ab12.byteLength, 3); + // slice needed due to ponyfill limitations. + const ta2 = new Uint8Array(ab2.slice()); + t.deepEqual([...ta2], [4, 5]); + + const ab3 = ab2.sliceToImmutable(1, 2); + t.true(ab3.immutable); + t.is(ab3.byteLength, 1); + t.is(ab2.byteLength, 2); + // slice needed due to ponyfill limitations. + const ta3 = new Uint8Array(ab3.slice()); + t.deepEqual([...ta3], [5]); +});