Skip to content

Commit

Permalink
feat(ses): ImmutableArrayBuffer
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Jun 7, 2024
1 parent f845665 commit dbc18f7
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 0 deletions.
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.
* - 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
65 changes: 65 additions & 0 deletions packages/ses/test/immutable-array-buffer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/* 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);
});

0 comments on commit dbc18f7

Please sign in to comment.