Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ coverage/
dist/
node_modules/
coincident-*
test/index-timeout.js
23 changes: 22 additions & 1 deletion src/local.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const {
* @property {Function} [remote=identity] The function used to intercept remote invokes *before* these happen. Usable to sync `events` or do other tasks.
* @property {Function} [module] The function used to import modules when remote asks to `import(...)` something.
* @property {boolean} [buffer=false] Optionally allows direct buffer serialization breaking JSON compatibility.
* @property {number} [timeout=-1] Optionally allows remote values to be cached when possible for a `timeout` milliseconds value. `-1` means no timeout.
*/

/**
Expand All @@ -99,6 +100,7 @@ export default ({
remote = identity,
module = name => import(name),
buffer = false,
timeout = -1,
} = object) => {
// received values arrive via postMessage so are compatible
// with the structured clone algorithm
Expand Down Expand Up @@ -191,6 +193,7 @@ export default ({

const { clear, id, ref, unref } = heap();

const memoize = -1 < timeout;
const weakRefs = new Map;
const globalTarget = tv(OBJECT, null);
const fr = new FinalizationRegistry(v => {
Expand Down Expand Up @@ -235,7 +238,25 @@ export default ({
switch (method) {
case GET: {
const key = fromKey(args[0]);
return toValue(isGlobal && key === 'import' ? module : get(target, key));
const asModule = isGlobal && key === 'import';
const value = toValue(asModule ? module : get(target, key));
if (memoize && isArray(value)) {
if (!asModule && (value[0] & REMOTE)) {
let cache = key in target, t = target, d;
if (cache) {
while (!(d = getOwnPropertyDescriptor(t, key))) {
t = getPrototypeOf(t);
/* c8 ignore start */
if (!t) break;
/* c8 ignore stop */
}
cache = !!d && 'value' in d;
value[1] = [cache, value[1]];
}
}
else value[1] = [false, value[1]];
}
return value;
}
case APPLY: {
const map = new Map;
Expand Down
109 changes: 99 additions & 10 deletions src/remote.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const { preventExtensions } = Object;
* @property {Function} [transform=identity] The function used to transform local values into simpler references that the remote side can understand.
* @property {Function} [released=identity] The function invoked when a reference is released.
* @property {boolean} [buffer=false] Optionally allows direct buffer deserialization breaking JSON compatibility.
* @property {number} [timeout=-1] Optionally allows remote values to be cached when possible for a `timeout` milliseconds value. `-1` means no timeout.
*/

/**
Expand All @@ -86,13 +87,13 @@ export default ({
transform = identity,
released = identity,
buffer = false,
timeout = -1,
} = object) => {
const fromKeys = loopValues(fromKey);
const toKeys = loopValues(toKey);

// OBJECT, DIRECT, VIEW, REMOTE_ARRAY, REMOTE_OBJECT, REMOTE_FUNCTION, SYMBOL, BIGINT
const fromValue = value => {
if (!isArray(value)) return value;
const fromArray = value => {
const [t, v] = value;
if (t & REMOTE) return asProxy(value, t, v);
switch (t) {
Expand All @@ -106,6 +107,8 @@ export default ({
}
};

const fromValue = value => isArray(value) ? fromArray(value) : value;

const toValue = (value, cache = new Map) => {
switch (typeof value) {
case 'object': {
Expand Down Expand Up @@ -185,12 +188,75 @@ export default ({
}
};

const memoize = -1 < timeout;

class Memo extends Map {
static entries = [];

static keys = Symbol();
static proto = Symbol();

static drop(entries) {
const cached = entries.splice(0);
let i = 0;
while (i < cached.length)
cached[i++].delete(cached[i++]);
}

static set(self, key) {
const { entries } = this;
if (entries.push(self, key) < 3)
setTimeout(this.drop, timeout, entries);
}

drop(key, value) {
this.delete(key);
if (key !== Memo.proto) this.delete(Memo.keys);
return value;
}

set(key, value) {
super.set(key, value);
Memo.set(this, key);
return value;
}
}

class Handler {
constructor(_) { this._ = _ }
constructor(_) {
this._ = _;
if (memoize) this.$ = new Memo;
}

get(_, key) { return fromValue(reflect(GET, this._, toKey(key))) }
set(_, key, value) { return reflect(SET, this._, toKey(key), toValue(value)) }
ownKeys(_) { return fromKeys(reflect(OWN_KEYS, this._), weakRefs) }
get(_, key) {
if (memoize && this.$.has(key)) return this.$.get(key);
const value = reflect(GET, this._, toKey(key));
if (!memoize) return fromValue(value);
if (isArray(value)) {
let [cache, ref] = value[1];
value[1] = ref;
ref = fromArray(value);
return cache ? this.$.set(key, ref) : ref;
}
return this.$.set(key, value);
}
set(_, key, value) {
const result = reflect(SET, this._, toKey(key), toValue(value));
return memoize ? this.$.drop(key, result) : result;
}

_oK() { return fromKeys(reflect(OWN_KEYS, this._), weakRefs) }
ownKeys(_) {
return memoize ?
(this.$.has(Memo.keys) ?
this.$.get(Memo.keys) :
this.$.set(Memo.keys, this._oK())) :
this._oK()
;
}

// this would require a cache a part per each key or make
// the Cache code more complex for probably little gain
getOwnPropertyDescriptor(_, key) {
const descriptor = fromValue(reflect(GET_OWN_PROPERTY_DESCRIPTOR, this._, toKey(key)));
if (descriptor) {
Expand All @@ -199,14 +265,37 @@ export default ({
}
return descriptor;
}
defineProperty(_, key, descriptor) { return reflect(DEFINE_PROPERTY, this._, toKey(key), toValue(descriptor)) }
deleteProperty(_, key) { return reflect(DELETE_PROPERTY, this._, toKey(key)) }
getPrototypeOf(_) { return fromValue(reflect(GET_PROTOTYPE_OF, this._)) }
setPrototypeOf(_, value) { return reflect(SET_PROTOTYPE_OF, this._, toValue(value)) }

defineProperty(_, key, descriptor) {
const result = reflect(DEFINE_PROPERTY, this._, toKey(key), toValue(descriptor));
return memoize ? this.$.drop(key, result) : result;
}

deleteProperty(_, key) {
const result = reflect(DELETE_PROPERTY, this._, toKey(key));
return memoize ? this.$.drop(key, result) : result;
}

_gPO() { return fromValue(reflect(GET_PROTOTYPE_OF, this._)) }
getPrototypeOf(_) {
return memoize ?
(this.$.has(Memo.proto) ?
this.$.get(Memo.proto) :
this.$.set(Memo.proto, this._gPO())) :
this._gPO()
;
}

setPrototypeOf(_, value) {
const result = reflect(SET_PROTOTYPE_OF, this._, toValue(value));
return memoize ? this.$.drop(Memo.proto, result) : result;
}
// way less common than others to be cached
isExtensible(_) { return reflect(IS_EXTENSIBLE, this._) }
preventExtensions(target) { return preventExtensions(target) && reflect(PREVENT_EXTENSIONS, this._) }
}

// TODO: should `in` operations be cached too?
const has = (_, $, prop) => prop === reflected ?
!!(reference = _) :
reflect(HAS, $, toKey(prop))
Expand Down
27 changes: 20 additions & 7 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,20 @@ import './array-buffer.js';

const array = [1, 2, 3];

const there = remote({
reflect: (...args) => here.reflect(...args),
transform: value => value === array ? there.direct(array) : value,
const bootstrap = timeout => ({
there: remote({
timeout,
reflect: (...args) => here.reflect(...args),
transform: value => value === array ? there.direct(array) : value,
}),
here: local({
timeout,
reflect: (...args) => there.reflect(...args),
transform: value => value === array ? here.direct(array) : value,
})
});

const here = local({
reflect: (...args) => there.reflect(...args),
transform: value => value === array ? here.direct(array) : value,
});
const { there, here } = bootstrap(globalThis.timeout || -1);

const { global } = there;

Expand Down Expand Up @@ -131,6 +136,14 @@ finally {
'use strict';
global.console.log.apply(global.console, ['test', 'completed']);
global.console.log.apply(global.console, arguments);
if (!globalThis.timeout) {
import('node:fs').then(async ({ copyFile }) => {
globalThis.timeout = 1;
copyFile('./test/index.js', './test/index-timeout.js', () => {
import('./index-timeout.js');
});
});
}
}, 250, { ok: true });
}

Expand Down
6 changes: 5 additions & 1 deletion types/local.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
declare function _default({ reflect, transform, remote, module, buffer, }?: LocalOptions): {
declare function _default({ reflect, transform, remote, module, buffer, timeout, }?: LocalOptions): {
/**
* Alows local references to be passed directly to the remote receiver,
* either as copy or serliazied values (it depends on the implementation).
Expand Down Expand Up @@ -48,4 +48,8 @@ export type LocalOptions = {
* Optionally allows direct buffer serialization breaking JSON compatibility.
*/
buffer?: boolean;
/**
* Optionally allows remote values to be cached when possible for a `timeout` milliseconds value. `-1` means no timeout.
*/
timeout?: number;
};
6 changes: 5 additions & 1 deletion types/remote.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
declare function _default({ reflect, transform, released, buffer, }?: RemoteOptions): {
declare function _default({ reflect, transform, released, buffer, timeout, }?: RemoteOptions): {
/**
* The local global proxy reference.
* @type {unknown}
Expand Down Expand Up @@ -71,4 +71,8 @@ export type RemoteOptions = {
* Optionally allows direct buffer deserialization breaking JSON compatibility.
*/
buffer?: boolean;
/**
* Optionally allows remote values to be cached when possible for a `timeout` milliseconds value. `-1` means no timeout.
*/
timeout?: number;
};