From c87e6c1e54c5b677f34cd4bd5402257d1513751c Mon Sep 17 00:00:00 2001 From: webreflection Date: Mon, 16 Jun 2025 21:56:45 +0200 Subject: [PATCH] Minor improvements & tests + splitViews --- src/direct/decoder.js | 62 ++++++++---------- src/direct/encoder.js | 38 ++++++++--- src/direct/views.js | 1 - src/utils/index.js | 4 +- src/utils/typed.js | 32 ++++++--- test/buffer.js | 133 ++++++++++++++++++++++++++++++++++++++ test/direct.js | 18 +++++- test/index.html | 1 + test/index.js | 1 + test/view.js | 19 +++++- types/direct/encoder.d.ts | 3 +- types/direct/views.d.ts | 1 - types/utils/typed.d.ts | 1 - 13 files changed, 251 insertions(+), 63 deletions(-) create mode 100644 test/buffer.js diff --git a/src/direct/decoder.js b/src/direct/decoder.js index a5f3428..97a811e 100644 --- a/src/direct/decoder.js +++ b/src/direct/decoder.js @@ -52,19 +52,27 @@ const $ = (cache, index, value) => { /** * @param {Uint8Array} input - * @param {number} index */ -const number = (input, index) => { - while (index < 8) u8a8[index++] = input[i++]; +const number = input => { + u8a8[0] = input[i++]; + u8a8[1] = input[i++]; + u8a8[2] = input[i++]; + u8a8[3] = input[i++]; + u8a8[4] = input[i++]; + u8a8[5] = input[i++]; + u8a8[6] = input[i++]; + u8a8[7] = input[i++]; }; /** * @param {Uint8Array} input - * @param {number} i * @returns {number} */ -const size = (input, i) => { - for (let j = 0; j < 4; j++) u8a8[j] = input[i++]; +const size = input => { + u8a8[0] = input[i++]; + u8a8[1] = input[i++]; + u8a8[2] = input[i++]; + u8a8[3] = input[i++]; return dv.getUint32(0, true); }; @@ -76,23 +84,19 @@ const size = (input, i) => { const deflate = (input, cache) => { switch (input[i++]) { case NUMBER: { - number(input, 0); + number(input); return dv.getFloat64(0, true); } case UI8: return input[i++]; case OBJECT: { const object = $(cache, i - 1, {}); - const length = size(input, i); - i += 4; - for (let j = 0; j < length; j++) + for (let j = 0, length = size(input); j < length; j++) object[deflate(input, cache)] = deflate(input, cache); return object; } case ARRAY: { const array = $(cache, i - 1, []); - const length = size(input, i); - i += 4; - for (let j = 0; j < length; j++) + for (let j = 0, length = size(input); j < length; j++) array.push(deflate(input, cache)); return array; } @@ -103,32 +107,28 @@ const deflate = (input, cache) => { } case BUFFER: { const index = i - 1; - const length = size(input, i); - return $(cache, index, input.slice(i += 4, i += length).buffer); + const length = size(input); + return $(cache, index, input.slice(i, i += length).buffer); } case STRING: { const index = i - 1; - const length = size(input, i); + const length = size(input); // this could be a subarray but it's not supported on the Web and // it wouldn't work with arrays instead of typed arrays. - return $(cache, index, textDecoder.decode(input.slice(i += 4, i += length))); + return $(cache, index, textDecoder.decode(input.slice(i, i += length))); } case DATE: { return $(cache, i - 1, new Date(deflate(input, cache))); } case MAP: { const map = $(cache, i - 1, new Map); - const length = size(input, i); - i += 4; - for (let j = 0; j < length; j++) + for (let j = 0, length = size(input); j < length; j++) map.set(deflate(input, cache), deflate(input, cache)); return map; } case SET: { const set = $(cache, i - 1, new Set); - const length = size(input, i); - i += 4; - for (let j = 0; j < length; j++) + for (let j = 0, length = size(input); j < length; j++) set.add(deflate(input, cache)); return set; } @@ -153,20 +153,10 @@ const deflate = (input, cache) => { case ZERO: return 0; case N_ZERO: return -0; case NULL: return null; - case BIGINT: { - number(input, 0); - return dv.getBigInt64(0, true); - } - case BIGUINT: { - number(input, 0); - return dv.getBigUint64(0, true); - } + case BIGINT: return (number(input), dv.getBigInt64(0, true)); + case BIGUINT: return (number(input), dv.getBigUint64(0, true)); case SYMBOL: return fromSymbol(deflate(input, cache)); - case RECURSION: { - const index = size(input, i); - i += 4; - return cache.get(index); - } + case RECURSION: return cache.get(size(input)); // this covers functions too default: return undefined; } diff --git a/src/direct/encoder.js b/src/direct/encoder.js index ecfaa8c..f1eaad9 100644 --- a/src/direct/encoder.js +++ b/src/direct/encoder.js @@ -36,7 +36,7 @@ import { import { isArray, isView, push } from '../utils/index.js'; import { encoder as textEncoder } from '../utils/text.js'; import { toSymbol } from '../utils/symbol.js'; -import { dv, u8a8, u8a4 } from './views.js'; +import { dv, u8a8 } from './views.js'; import { toTag } from '../utils/global.js'; /** @typedef {Map} Cache */ @@ -56,7 +56,7 @@ const process = (input, output, cache) => { const unknown = !value; if (unknown) { dv.setUint32(0, output.length, true); - cache.set(input, [u8a4[0], u8a4[1], u8a4[2], u8a4[3]]); + cache.set(input, [u8a8[0], u8a8[1], u8a8[2], u8a8[3]]); } else output.push(RECURSION, value[0], value[1], value[2], value[3]); @@ -70,7 +70,7 @@ const process = (input, output, cache) => { */ const set = (output, type, length) => { dv.setUint32(0, length, true); - output.push(type, u8a4[0], u8a4[1], u8a4[2], u8a4[3]); + output.push(type, u8a8[0], u8a8[1], u8a8[2], u8a8[3]); }; /** @@ -117,7 +117,7 @@ const inflate = (input, output, cache) => { case input instanceof ArrayBuffer: { const ui8a = new Uint8Array(input); set(output, BUFFER, ui8a.length); - push(output, ui8a); + pushView(output, ui8a); break; } case input instanceof Date: @@ -173,7 +173,7 @@ const inflate = (input, output, cache) => { if (process(input, output, cache)) { const encoded = textEncoder.encode(input); set(output, STRING, encoded.length); - push(output, encoded); + pushView(output, encoded); } break; } @@ -204,22 +204,38 @@ const inflate = (input, output, cache) => { } }; +let pushView = push; + /** * @param {any} value * @returns {number[]} */ export const encode = value => { const output = []; + pushView = push; inflate(value, output, new Map); return output; }; /** - * @param {{ byteOffset?: number }} [options] + * @param {{ byteOffset?: number, splitViews?: boolean }} [options] * @returns {(value: any, buffer: SharedArrayBuffer) => number} */ -export const encoder = ({ byteOffset = 0 } = {}) => (value, buffer) => { - const output = encode(value); +export const encoder = ({ byteOffset = 0, splitViews = false } = {}) => (value, buffer) => { + let output = [], views = output; + pushView = splitViews ? + (views = [], (output, value) => { + const length = value.length; + // avoid complexity for small buffers (short keys and whatnot) + if (length < 129) output.push.apply(output, value); + else { + views.push([output.length, value]); + output.length += value.length; + } + }) : + push + ; + inflate(value, output, new Map); const length = output.length; const size = length + byteOffset; if (buffer.byteLength < size) { @@ -227,5 +243,11 @@ export const encoder = ({ byteOffset = 0 } = {}) => (value, buffer) => { buffer.grow(size); } new Uint8Array(buffer, byteOffset, length).set(output); + if (splitViews) { + for (let i = 0, length = views.length; i < length; i++) { + const [offset, value] = views[i]; + new Uint8Array(buffer, byteOffset + offset, value.length).set(value); + } + } return length; }; diff --git a/src/direct/views.js b/src/direct/views.js index 6af7383..38b5775 100644 --- a/src/direct/views.js +++ b/src/direct/views.js @@ -1,4 +1,3 @@ const buffer = new ArrayBuffer(8); export const dv = new DataView(buffer); export const u8a8 = new Uint8Array(buffer); -export const u8a4 = new Uint8Array(buffer, 0, 4); diff --git a/src/utils/index.js b/src/utils/index.js index 5bade4b..9e81d38 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -70,6 +70,6 @@ export const toKey = value => typeof value === 'string' ? const MAX_ARGS = 0x7FFF; export const push = (output, value) => { - for (let i = 0, length = value.length; i < length; i += MAX_ARGS) - output.push.apply(output, value.subarray(i, i + MAX_ARGS)); + for (let $ = output.push, i = 0, length = value.length; i < length; i += MAX_ARGS) + $.apply(output, value.subarray(i, i + MAX_ARGS)); }; diff --git a/src/utils/typed.js b/src/utils/typed.js index 9f48b48..7dfb3ea 100644 --- a/src/utils/typed.js +++ b/src/utils/typed.js @@ -4,22 +4,34 @@ import { fromArray } from './index.js'; /** @typedef {[ArrayBufferLike|number[], number]} BufferDetails */ /** @typedef {[string, BufferDetails, number, number]} ViewDetails */ -export const arrayBuffer = (length, maxByteLength, value) => { - const buffer = maxByteLength ? new ArrayBuffer(length, { maxByteLength }) : new ArrayBuffer(length); - new Uint8Array(buffer).set(value); - return buffer; -}; +/** + * @param {number} length + * @param {number} maxByteLength + * @returns {ArrayBufferLike} + */ +const resizable = (length, maxByteLength) => new ArrayBuffer(length, { maxByteLength }); /** * @param {BufferDetails} details * @param {boolean} direct * @returns {ArrayBufferLike} */ -export const fromBuffer = ([value, maxByteLength], direct) => arrayBuffer( - direct ? /** @type {ArrayBufferLike} */ (value).byteLength : /** @type {number[]} */ (value).length, - maxByteLength, - value, -); +export const fromBuffer = ([value, maxByteLength], direct) => { + const length = direct ? /** @type {ArrayBufferLike} */ (value).byteLength : /** @type {number[]} */ (value).length; + if (direct) { + if (maxByteLength) { + const buffer = resizable(length, maxByteLength); + new Uint8Array(buffer).set(new Uint8Array(/** @type {ArrayBufferLike} */ (value))); + value = buffer; + } + } + else { + const buffer = maxByteLength ? resizable(length, maxByteLength) : new ArrayBuffer(length); + new Uint8Array(buffer).set(/** @type {number[]} */ (value)); + value = buffer; + } + return /** @type {ArrayBufferLike} */ (value); +}; /** * @param {ViewDetails} details diff --git a/test/buffer.js b/test/buffer.js new file mode 100644 index 0000000..b4cc015 --- /dev/null +++ b/test/buffer.js @@ -0,0 +1,133 @@ +import local from '../src/local.js'; +import remote from '../src/remote.js'; + +const array = [1, 2, 3]; + +const there = remote({ + buffer: true, + reflect: (...args) => here.reflect(...args), + transform: value => value === array ? there.direct(array) : value, +}); + +const here = local({ + buffer: true, + reflect: (...args) => there.reflect(...args), + transform: value => value === array ? here.direct(array) : value, +}); + +const { global } = there; + +global.trapped = function trap() {}; + +console.assert(Object.isExtensible(global.Array)); +console.assert('isArray' in global.Array); +console.assert(global.Array.isArray([])); +console.assert(global.Array.isArray(new global.Array)); +console.assert(new global.Array instanceof global.Array); + +for (const _ of new global.Array(1, 2, 3)); + +console.assert(global.Symbol.iterator in global.Array.prototype); +console.assert(!(Symbol.for('iterator') in global.Array.prototype)); +console.assert(global.Object({}) instanceof global.Object); +console.assert(new global.Date instanceof global.Date); +console.assert(global.Object.getPrototypeOf(new global.Date) === global.Date.prototype); +console.assert(global.Reflect.isExtensible({})); + +let obj = global.Object({}); +obj.value = 123; +console.assert(obj.value === 123); +console.assert(Reflect.ownKeys(obj).length === 1); +console.assert(Reflect.ownKeys(obj)[0] === 'value'); +console.assert(global.Object.is(obj, obj)); +console.assert(!global.Object.is(obj, {})); +console.assert(!!Object.getOwnPropertyDescriptor(obj, 'value')); +console.assert(!!global.Object.getOwnPropertyDescriptor(obj, 'value')); +delete obj.value; +console.assert(!Object.getOwnPropertyDescriptor(obj, 'value')); +console.assert(!global.Object.getOwnPropertyDescriptor(obj, 'value')); + +global.Object.defineProperty(obj, 'value', { + configurable: true, + get: global.Function('return "get"'), + set: (_) => 'set', +}); +console.assert(obj.value === 'get'); + +console.assert(Object.getPrototypeOf(obj) === global.Object.prototype); +console.assert(Reflect.setPrototypeOf(obj, null)); +console.assert(Object.preventExtensions(obj)); + +let fn = global.Function('a', 'return a'); +console.assert(fn(true)); +console.assert(!!fn(globalThis)); +console.assert(fn(global) === global); +console.assert(fn(null) === null); +console.assert(fn(Symbol.iterator) === Symbol.iterator); +console.assert(fn(new ArrayBuffer(12)) instanceof ArrayBuffer); +console.assert(fn(new Int32Array([1,2,3])) instanceof Int32Array); + +console.assert(fn(Function) === fn(Function)); + +// visually check one is bound the other one isn't +console.log(fn(Function)); +global.console.log(fn(Function)); + +console.assert(there.isProxy(global.JSON)); +console.assert(there.isProxy(global.Array)); +console.assert(!there.isProxy(null)); +console.assert(!there.isProxy(false)); + +global.console.assert(fn(array) === array); +global.console.assert(fn(123n) === 123n); + +console.assert(ArrayBuffer.isView(new global.Int32Array([1, 2, 3]))); +console.assert((await global.import('../src/types.js')).DIRECT === 0); + +console.assert(there.evaluate((a, b) => a + b, 1, 2) === 3, 'arrow'); +console.assert(there.evaluate(function test(a, b) { return a + b }, 1, 2) === 3, 'named'); +console.assert(there.evaluate({test(a, b) { return a + b }}.test, 1, 2) === 3, 'method'); +console.assert(await there.evaluate(async function test(a, b) { return a + b }, 1, 2) === 3, 'async'); + +console.assert(there.query(global, 'Array.isArray.length') === there.query(globalThis, 'Array.isArray.length')); +console.assert(there.query(global, 'Array["isArray"]["length"]') === there.query(globalThis, 'Array.isArray.length')); +console.assert(there.query(global, 'Object.name[0]') === 'O'); + +Object.defineProperty(global, 'test', { value: 123 }); +console.assert(global.test === 123); + +global.setTimeout((a, b, c) => console.assert(a === 1 && b === 2 && c === 3), 10, 1, 2, 3); + +let arr = new global.Array(1, 2, 3); +fn(here.direct([arr, arr])); + +there.assign(global, { value: 1, array: [1, 2, 3] }); +console.assert(there.gather(global, 'value')[0] === 1); +console.assert(there.gather(global, 'value').length === 1); +console.assert(there.gather(global, 'value', 'array').length === 2); +console.assert(there.gather(global, 'value', 'array[1]')[1] === 2); +console.assert(there.gather(global, Symbol.iterator)[0] === void 0); + +obj = there.assign({}, { value: 1 }); +console.assert(there.gather(obj, 'value')[0] === 1); +console.assert(there.gather(obj, 'value').length === 1); + +global.console.log(new Uint8Array([1, 2, 3])); +console.log(new global.Uint8Array([1, 2, 3])); + +obj = null; +global.trapped = null; + +try { + setTimeout(gc); +} catch (e) { +} +finally { + setTimeout(function () { + 'use strict'; + global.console.log.apply(global.console, ['test', 'completed']); + global.console.log.apply(global.console, arguments); + }, 250, { ok: true }); +} + +setTimeout(here.terminate, 500); diff --git a/test/direct.js b/test/direct.js index a0939ed..1d5123c 100644 --- a/test/direct.js +++ b/test/direct.js @@ -30,6 +30,7 @@ assert(undefined); assert(1n); assert(-1n); assert('test'); +assert('a'.repeat(200)); assert('🥳'); assert(['a', 'b', 'a']); assert(Symbol.iterator); @@ -52,10 +53,25 @@ console.assert(roundtrip(new Int32Array([1, 2, 3])).join(',') === '1,2,3'); console.assert(roundtrip({ toJSON: () => 123 }) === 123); console.assert(roundtrip({ toJSON() { return this }}) === null); -const sab = new SharedArrayBuffer(4, { maxByteLength: 100 }); +const sab = new SharedArrayBuffer(4, { maxByteLength: 1024 }); const enc = encoder({ byteOffset: 0 }); const dec = decoder({ byteOffset: 0 }); const written = enc('hello encoder', sab); console.assert(written === 5 + 'hello encoder'.length); console.assert(dec(written, sab) === 'hello encoder'); + +const venc = encoder({ byteOffset: 0, splitViews: true }); +console.assert(venc('a'.repeat(200), sab) === 205); +console.assert(dec(205, sab) === 'a'.repeat(200)); +console.assert(venc('a'.repeat(10), sab) === 15); +console.assert(dec(15, sab) === 'a'.repeat(10)); + +const ab = new ArrayBuffer(4, { maxByteLength: 8 }); +const ui8a = dec(venc([new Uint8Array(ab), new Uint8Array(ab)], sab), sab); +console.assert(ui8a.length === 2); +console.assert(ui8a[0].length === 4); +console.assert(ui8a[1].length === 4); +console.assert(ui8a[0] !== ui8a[1]); +console.assert(ui8a[0] instanceof Uint8Array); +console.assert(ui8a[1] instanceof Uint8Array); diff --git a/test/index.html b/test/index.html index dd4d4c3..9abd5bb 100644 --- a/test/index.html +++ b/test/index.html @@ -5,6 +5,7 @@