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]); +});