@comyata/run
Version:
Simplify data workflows and management with data templates, for browser and server.
115 lines (113 loc) • 5.16 kB
JavaScript
import { CircularNodeDependencyError } from '@comyata/run/Errors';
import { ValueSubscriberPromise } from '@comyata/run/ValueSubscriberPromise';
import jsonpointer from 'json-pointer';
const PROXY_FLAG = Symbol('ProxyFlag');
const PROXY_VALUE = Symbol('ProxyValue');
/**
* @todo verify behaviour with arrays, especially for `arr.slice(0, 2)` and similar
* e.g. this expression doesn't resolve the second entry:
* `l: [1, "${1+1}", 3, "${$root().l[[0..1]]}"]` => `{ "l": [1, 2, 3, [1, {}] ] }`
* (which may be that jsonata doesn't support Promise resolving in range-query)
* @todo this doesn't work if the whole object is accessed, doesn't prevent recursion if accessing a parent object which contains computed fields;
* to support that, a tree/graph must be build, which allows checking where computed fields are included and tracking the path during recursive proxy creation
* e.g. this doesn't fail but creates a cyclic reference: `{ a: "${$root().b}", b: { b1: "${$root().a}" } }`
* but it could be detected:
* `a` > `b`
* `b.b1` > `a` > `b`
* note that this doesn't result in a deadLock, as `a` > `b` can be resolved without waiting for `b.b1` and the final result of `b.b1` is set via reference,
* yet it still leads to circular references instead of materialized data
* @todo support adding to usages graph for transparent reporting on what uses what (interop with `addUsage` of DataFile)
* @experimental
*/
export function createValueProxy(data, dataNode, getNodeContext, path = []) {
if (data instanceof ValueSubscriberPromise) {
// check against the dataNode in subscriber promise if it's a circular loop.
// it must iterate over all dependents and check the whole chain against occurrence of the dataNode
// in dependencies of the new dependency of `dataNode`.
// todo: include information about the respective chains for better errors?
const open = [dataNode];
while (open.length) {
const uncheckedDataNode = open.pop();
const nodeValue = getNodeContext(uncheckedDataNode)?.[1]();
if (dataNode === data.dataNode() || nodeValue.hasDependent(data.dataNode())) {
throw new CircularNodeDependencyError(new Set([uncheckedDataNode]), data.dataNode(), `Circular dependency between data-nodes ${JSON.stringify(jsonpointer.compile(uncheckedDataNode.path))} and ${JSON.stringify(jsonpointer.compile(data.dataNode().path))}.`);
}
open.push(...nodeValue.getDependents());
}
data.addDependent(dataNode);
return new Promise((resolve, reject) => {
data.then(value => {
resolve(createValueProxy(value, dataNode, getNodeContext, path));
}).catch(reject);
});
}
if (data === null || typeof data !== 'object') {
return data;
}
return new Proxy(data, {
get: (target, prop) => {
// if(typeof prop === 'string' && !['constructor', 'tagName', 'nodeType', 'hasAttribute', 'toJSON', 'sequence', 'then', '_jsonata_lambda'].includes(prop) && !prop.startsWith('@@') && !prop.startsWith('$$')) {
// console.log(
// 'prop',
// prop,
// path,
// dataNode.path,
// // e.g. for array `[...arr]`
// // prop === Symbol.iterator,
// Reflect.get(target, prop) instanceof ValueSubscriberPromise,
// )
// }
if (prop === PROXY_VALUE) {
return target;
}
if (
// todo: self reference does no longer fail, as it won't reach creating nested proxy
isSameBranch(path, dataNode.path)
// `prop` is a `string`, also for arrays, while `.path` has `number`
&& String(dataNode.path[dataNode.path.length - 1]) === prop) {
// self ref
return null;
}
const value = Reflect.get(target, prop);
if (value !== null && typeof value === 'object' && typeof prop !== 'symbol') {
return createValueProxy(value, dataNode, getNodeContext, [...path, prop]);
}
return value;
},
set: (_target, prop) => {
throw new TypeError(`Value is readonly, can not set ${String(prop)}`);
},
ownKeys: target => {
return Reflect.ownKeys(target);
},
has: (target, prop) => {
return Reflect.has(target, prop);
},
getPrototypeOf: () => {
return {
[PROXY_FLAG]: true
};
}
});
}
function isSameBranch(path, dataNodePath) {
if (path.length !== dataNodePath.length - 1) {
return false;
}
for (let i = 0; i < path.length; i++) {
const a = path[i];
const b = dataNodePath[i];
// `path` may be purely from `props`, thus would be an `string` for array indexes,
// but could also be initialized with a `dataNode.path`, which has `number` for indexes,
// while `dataNodePath` always will be `number|string`
if (String(a) !== String(b)) return false;
// if(a !== b) return false
}
return true;
}
export function isProxy(proxy) {
return Boolean(proxy && typeof proxy === 'object' && Object.getPrototypeOf(proxy)[PROXY_FLAG] === true);
}
export function toRaw(proxy) {
return proxy[PROXY_VALUE];
}