packages/remote-context/src/Context.js
/**
* Copyright 2017 Moshe Simantov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import AssignableContext from './AssignableContext';
import EnvContext from './EnvContext';
import RemoteSession from './RemoteSession';
import parser from './parser';
import LocalPromise from './LocalPromise';
const kChildRefs = Symbol('childRefs');
/**
* @example
* import { Context } from 'remote-context';
*
* const context = new Context(envContext);
*/
export default class Context extends AssignableContext {
/**
* Check if the given value is unique per this environment.
* For example the string `'foo'` is not referable, but the object `{ foo: 1 }` is referable.
*
* @param {*} value The given value to check
* @return {boolean} True if the given value is referable
*/
static isReferable(value) {
switch (typeof value) {
case 'function':
case 'symbol':
return true;
case 'object':
return value !== null;
default:
return false;
}
}
/**
* Create a new virtual context on the given environment.
*
* @param {EnvContext} envContext The given environment
* @param {object} context The context to create
* @param {object} opts Options for {@link AssignableContext.constructor}
*/
constructor(envContext, context = {}, opts = {}) {
if (!(envContext instanceof EnvContext)) {
throw new TypeError(`Invalid "envContext" param: ${envContext}`);
}
super({}, envContext, opts);
/**
* @type {Map}
* @private
*/
this[kChildRefs] = new Map();
this.copyFrom(context);
}
/**
* Get the remote context of this context.
* A single context can be served to multiple peers, each peer will get a separate copy of the
* context.
*
* @param {Stream} stream a duplex stream to the remote peer context
* @param {object} [opts] Options to {@link RemoteSession}
* @return {RemoteContext}
*/
remote(stream, opts = {}) {
const objectStream =
opts.objectMode !== true ? parser.transform(stream) : stream;
const session = new RemoteSession(objectStream, this, opts);
return session.remote;
}
/**
* Fetch a local reference and return a local promise.
*
* @param {Reference} reference The local reference to fetch
* @return {LocalPromise}
*/
fetch(reference) {
try {
return new LocalPromise(this.get(reference));
} catch (error) {
return new LocalPromise(error, true);
}
}
/**
* Resolve a local value and return a local promise to that value.
*
* @param {*} value The local value to resolve
* @return {LocalPromise}
*/
// eslint-disable-next-line class-methods-use-this
resolve(value) {
return new LocalPromise(value);
}
/**
* @override
*/
set(reference, value) {
if (typeof reference !== 'string' && typeof reference !== 'number') {
throw new TypeError(
`Only string or number references are allowed: ${typeof reference}`
);
}
super.set(reference, value);
let childRefs = this[kChildRefs].get(value);
if (!childRefs) {
childRefs = new Set();
this[kChildRefs].set(value, childRefs);
}
const addChild = childValue => {
if (!this.constructor.isReferable(childValue) || this.exists(childValue))
return;
childRefs.add(this.assign(childValue));
};
addChild(Object.getPrototypeOf(value));
Object.getOwnPropertyNames(value).forEach(propName => {
const desc = Object.getOwnPropertyDescriptor(value, propName);
addChild(desc.value);
addChild(desc.get);
addChild(desc.set);
});
Object.getOwnPropertySymbols(value).forEach(symbol => {
if (!this.exists(symbol)) return;
const desc = Object.getOwnPropertyDescriptor(value, symbol);
addChild(desc.value);
addChild(desc.get);
addChild(desc.set);
});
return this;
}
/**
* @override
*/
release(value) {
const ret = super.release(value);
const childRefs = this[kChildRefs].get(value);
if (childRefs) {
childRefs.forEach(ref => this.delete(ref));
this[kChildRefs].delete(value);
}
return ret;
}
/**
* @override
*/
clear() {
super.clear();
this[kChildRefs].clear();
}
}