Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ses): ImmutableArrayBuffer #2309

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/ses/NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
User-visible changes in SES:

# Next release

- Adds `ImmutableArrayBuffer` as a shim for a future proposal. `ImmutableArrayBuffer` is supposed to be a peer to `ArrayBuffer` and `SharedArrayBuffer`, except without any mutating methods. As a shim, it does not implement some features that will be proposed.
- Unlike `ArrayBuffer` and `SharedArrayBuffer` it cannot be passed between JS threads.
- Unlike `ArrayBuffer` and `SharedArrayBuffer`, it cannot be used as the backing store of TypeArrays or DataViews.

# v1.5.0 (2024-05-06)

- Adds `importNowHook` to the `Compartment` options. The compartment will invoke the hook whenever it encounters a missing dependency while running `compartmentInstance.importNow(specifier)`, which cannot use an asynchronous `importHook`.
Expand Down
1 change: 1 addition & 0 deletions packages/ses/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
"Float64Array",
"Function",
"HandledPromise",
"ImmutableArrayBuffer",
"Int16Array",
"Int32Array",
"Int8Array",
Expand Down
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 { isView, prototype: arrayBufferPrototype } = ArrayBuffer;
export const { prototype: mapPrototype } = Map;
export const { revocable: proxyRevocable } = Proxy;
export const { prototype: regexpPrototype } = RegExp;
Expand Down Expand Up @@ -174,6 +176,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
4 changes: 4 additions & 0 deletions packages/ses/src/immutable-array-buffer-shim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { globalThis } from './commons.js';
import { ImmutableArrayBuffer } from './immutable-array-buffer.js';

globalThis.ImmutableArrayBuffer = ImmutableArrayBuffer;
105 changes: 105 additions & 0 deletions packages/ses/src/immutable-array-buffer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/* eslint-disable class-methods-use-this */
import {
defineProperties,
arrayBufferSlice,
toStringTagSymbol,
isView,
speciesSymbol,
} from './commons.js';

/**
* `ImmutableArrayBuffer` is intended to be a peer of `ArrayBuffer` and
* `SharedArrayBuffer`, but only with the non-mutating methods they have in
* common. We're adding this to ses as if it was already part of the
* language, and we consider this implementation to be a shim for an
* upcoming tc39 proposal.
*
* As a proposal it would take additional steps that would the shim does not:
* - to have `ImmutableArrayBuffer` be shared between
* threads (in spec speak, "agent") in exactly the same way
* `SharedArrayBuffer` is shared between agents. Unlike `SharedArrayBuffer`,
* sharing an `ImmutableArrayBuffer` does not introduce any observable
* concurrency. Unlike `ArrayBuffer`, sharing an `ImmutableArrayBuffer`
* does not detach anything.
Comment on lines +22 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, only "transferred" ArrayBuffer become detached on the web. If not transferring, the AB is simply copied, which is for an immutable buffer is observably indistinguishable from sharing. Basically an ImmutableArrayBuffer would simply not be "transferable" on the web, but it could remain "cloneable".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I did not realize that structured clone has a distinct knob for clone vs transfer. I'll clarify that the advantage is sharing immutable data without copying.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still view copying or not as an optimization that engines could do regardless. There is no guarantee that they would not copy a cloned immutable array buffer, and if the engine was implementing copy-on-write, it could avoid copying a cloned array buffer already.

* - when used as a backing store of a `TypedArray` or `DataView`, all the query
* methods would work, but the mutating methods would throw. In this sense,
* the wrapping `TypedArray` or `DataView` would also be immutable.
*
* Technically, this file is a ponyfill because it does not install this class
* on `globalThis` or have any other effects on primordial state. It only
* defines and exports a new class.
* `immutable-array-buffer-shim.js` is the corresponding shim which
* installs `ImmutableArrayBuffer` on `globalThis`. It is imported by
* `lockdown`, so that `ImmutableArrayBuffer` can act as-if defined as
* part of the language.
*
* Note that the class isn't immutable until hardened by lockdown.
* Even then, the instances are not immutable until hardened.
* This class does not harden its instances itself to preserve similarity
* with `ArrayBuffer` and `SharedArrayBuffer`.
*/
export class ImmutableArrayBuffer {
/** @type {ArrayBuffer} */
#buffer;

/**
* @param {unknown} arg
* @returns {arg is DataView}
*/
static isView(arg) {
// TODO should this just share/alias`isView` instead?
return isView(arg);
}

// TODO how to type this?
static get [speciesSymbol]() {
return ImmutableArrayBuffer;
}

/** @param {ArrayBuffer} buffer */
constructor(buffer) {
// This also enforces that `buffer` is a genuine `ArrayBuffer`
this.#buffer = arrayBufferSlice(buffer, 0);
}

/** @type {number} */
get byteLength() {
return this.#buffer.byteLength;
}

/** @type {boolean} */
get detached() {
return false;
}

/** @type {number} */
get maxByteLength() {
// Not underlying maxByteLength, which is irrelevant
return this.#buffer.byteLength;
}

/** @type {boolean} */
get resizable() {
return false;
}

/**
* @param {number} begin
* @param {number} [end]
* @returns {ArrayBuffer}
* Returns a genuine ArrayBuffer, not a SharedArrayBuffer
*/
slice(begin, end = undefined) {
return arrayBufferSlice(this.#buffer, begin, end);
}

/** @type {string} */
get [toStringTagSymbol]() {
// Is remade into a data property by mutating the class prototype below.
return 'ImmutableArrayBuffer';
}
}

defineProperties(ImmutableArrayBuffer.prototype, {
[toStringTagSymbol]: { value: 'ImmutableArrayBuffer' },
});
1 change: 1 addition & 0 deletions packages/ses/src/lockdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { makeCompartmentConstructor } from './compartment.js';
import { tameHarden } from './tame-harden.js';
import { tameSymbolConstructor } from './tame-symbol-constructor.js';
import { tameFauxDataProperties } from './tame-faux-data-properties.js';
import './immutable-array-buffer-shim.js';

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

Expand Down
19 changes: 19 additions & 0 deletions packages/ses/src/permits.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const universalPropertyNames = {
Float16Array: 'Float16Array',
Float32Array: 'Float32Array',
Float64Array: 'Float64Array',
ImmutableArrayBuffer: 'ImmutableArrayBuffer',
Int8Array: 'Int8Array',
Int16Array: 'Int16Array',
Int32Array: 'Int32Array',
Expand Down Expand Up @@ -1273,6 +1274,24 @@ export const permitted = {
SharedArrayBuffer: false, // UNSAFE and purposely suppressed.
'%SharedArrayBufferPrototype%': false, // UNSAFE and purposely suppressed.

ImmutableArrayBuffer: {
// Properties of the ImmutableArrayBuffer Constructor
'[[Proto]]': '%FunctionPrototype%',
isView: fn,
prototype: '%ImmutableArrayBufferPrototype%',
'@@species': getter,
},

'%ImmutableArrayBufferPrototype%': {
byteLength: getter,
constructor: 'ImmutableArrayBuffer',
slice: fn,
'@@toStringTag': 'string',
resizable: getter,
maxByteLength: getter,
detached: getter,
},

DataView: {
// Properties of the DataView Constructor
'[[Proto]]': '%FunctionPrototype%',
Expand Down
98 changes: 98 additions & 0 deletions packages/ses/test/immutable-array-buffer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/* global ImmutableArrayBuffer */
// TODO make the above global declaration unnecessary.
// TODO ensure ImmutableArrayBuffer is typed like ArrayBuffer is typed
// Where are these configured?

import test from 'ava';
import '../index.js';

const { isFrozen } = Object;

lockdown();

test('ImmutableArrayBuffer installed and hardened', t => {
t.true(isFrozen(ImmutableArrayBuffer));
t.true(isFrozen(ImmutableArrayBuffer.isView));
t.true(isFrozen(ImmutableArrayBuffer.prototype));
t.true(isFrozen(ImmutableArrayBuffer.prototype.slice));
});

test('ImmutableArrayBuffer ops', t => {
const ab1 = new ArrayBuffer(2, { maxByteLength: 7 });
const ta1 = new Uint8Array(ab1);
ta1[0] = 3;
ta1[1] = 4;
const iab = new ImmutableArrayBuffer(ab1);
t.true(iab instanceof ImmutableArrayBuffer);
t.false(iab instanceof ArrayBuffer);
ta1[1] = 5;
const ab2 = iab.slice(0);
const ta2 = new Uint8Array(ab2);
t.is(ta1[1], 5);
t.is(ta2[1], 4);
ta2[1] = 6;

const ab3 = iab.slice(0);
t.false(ab3 instanceof ImmutableArrayBuffer);
t.true(ab3 instanceof ArrayBuffer);

const ta3 = new Uint8Array(ab3);
t.is(ta1[1], 5);
t.is(ta2[1], 6);
t.is(ta3[1], 4);

t.false(ArrayBuffer.isView({}));
t.false(ImmutableArrayBuffer.isView({}));
const dv1 = new DataView(ab1);
t.true(ArrayBuffer.isView(dv1));
t.true(ImmutableArrayBuffer.isView(dv1));

t.is(ImmutableArrayBuffer[Symbol.species], ImmutableArrayBuffer);

t.is(ab1.byteLength, 2);
t.is(iab.byteLength, 2);
t.is(ab2.byteLength, 2);

t.is(iab.maxByteLength, 2);
if ('maxByteLength' in ab1) {
// ArrayBuffer.p.maxByteLength absent from Node 18
t.is(ab1.maxByteLength, 7);
t.is(ab2.maxByteLength, 2);
}

t.false(iab.detached);
t.false(iab.resizable);
});

// This could have been written as a test.failing as compared to
// the ImmutableArrayBuffer we'll propose. However, I'd rather test what
// the shim purposesly does instead.
test('ImmutableArrayBuffer shim limitations', t => {
const ab1 = new ArrayBuffer(2);
const dv1 = new DataView(ab1);
t.is(dv1.buffer, ab1);
t.is(dv1.byteLength, 2);
const ta1 = new Uint8Array(ab1);
ta1[0] = 3;
ta1[1] = 4;
t.is(ta1.byteLength, 2);

t.throws(() => new DataView({}), { instanceOf: TypeError });
// Unfortutanely, calling a TypeArray constructor with an object that
// is not a TypeArray, ArrayBuffer, or Iterable just creates a useless
// empty TypedArray, rather than throwing.
const ta2 = new Uint8Array({});
t.is(ta2.byteLength, 0);

const iab = new ImmutableArrayBuffer(ab1);
t.throws(() => new DataView(iab), {
instanceOf: TypeError,
});
// Unfortunately, unlike the ImmutableArrayBuffer to be proposed,
// calling a TypedArray constructor with the shim implementation of
// ImmutableArrayBuffer as argument treats it as an unrecognized object,
// rather than throwing an error.
t.is(iab.byteLength, 2);
const ta3 = new Uint8Array(iab);
erights marked this conversation as resolved.
Show resolved Hide resolved
t.is(ta3.byteLength, 0);
});
Loading