Skip to content

Commit

Permalink
fix(immutable-arraybuffer): update to recent spec (#2688)
Browse files Browse the repository at this point in the history
Closes: tc39/proposal-immutable-arraybuffer#26
Refs: https://github.com/tc39/proposal-immutable-arraybuffer
tc39/proposal-immutable-arraybuffer#15
tc39/proposal-immutable-arraybuffer#9
tc39/proposal-immutable-arraybuffer#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
tc39/proposal-immutable-arraybuffer#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
tc39/proposal-immutable-arraybuffer#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.
  • Loading branch information
erights authored Jan 13, 2025
1 parent e02b0f6 commit daa1e2b
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 13 deletions.
62 changes: 53 additions & 9 deletions packages/immutable-arraybuffer/index.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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));
13 changes: 10 additions & 3 deletions packages/immutable-arraybuffer/shim.js
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
82 changes: 81 additions & 1 deletion packages/immutable-arraybuffer/test/index.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import test from 'ava';
import {
transferBufferToImmutable,
// isBufferImmutable,
isBufferImmutable,
sliceBufferToImmutable,
} from '../index.js';

const { isFrozen, getPrototypeOf } = Object;
Expand Down Expand Up @@ -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]);
});
79 changes: 79 additions & 0 deletions packages/immutable-arraybuffer/test/shim.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
});

0 comments on commit daa1e2b

Please sign in to comment.