Skip to content

Instantly share code, notes, and snippets.

@bmeck
Last active April 15, 2021 15:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bmeck/0f74a23d15d87fb2ba400123eea818cb to your computer and use it in GitHub Desktop.
Save bmeck/0f74a23d15d87fb2ba400123eea818cb to your computer and use it in GitHub Desktop.
'use strict';
const readOnlyViews = new WeakMap();
function ReadOnlyProxy(target) {
if (!canBeAProxyTarget(target)) return target;
if (readOnlyViews.has(target)) return readOnlyViews.get(target);
/**
* @type {()=>never}
*/
function fail() {
throw new Error('cannot mutate a read only view');
}
const proxy = new Proxy(target, {
defineProperty: fail,
preventExtensions: fail,
set: fail,
setPrototypeOf: fail,
deleteProperty: fail,
/**
* FIXME: technically, getters can have side-effects
*/
get(target, name, receiver) {
return ReadOnlyProxy(Reflect.get(target, name, receiver));
},
getPrototypeOf(target) {
return ReadOnlyProxy(Reflect.getPrototypeOf(target));
},
getOwnPropertyDescriptor(target, name) {
const ret = Reflect.getOwnPropertyDescriptor(target, name);
if (!ret) return ret;
if (Reflect.getOwnPropertyDescriptor(ret, 'value')) {
ret.value = ReadOnlyProxy(ret.value);
}
if (Reflect.getOwnPropertyDescriptor(ret, 'get')) {
ret.get = ReadOnlyProxy(ret.get);
}
if (Reflect.getOwnPropertyDescriptor(ret, 'set')) {
ret.set = ReadOnlyProxy(ret.set);
}
return ReadOnlyProxy(ret);
},
/**
* These are too unwieldy to try and prevent side effects
*/
apply: fail,
construct: fail,
});
readOnlyViews.set(target, proxy);
return proxy;
}
function canBeAProxyTarget(o) {
return o && (typeof o === 'object' || typeof o === 'function');
}
module.exports = TransactionProxy;
function TransactionProxy(base) {
if (typeof base === 'function') {
throw new TypeError('base cannot be a function');
}
const toRevoke = [];
let committed = false;
return {
/**
* creates a transaction fork, each fork provides a parallel mutable
* proxy that is exclusive when committed
*/
fork() {
/**
* Saves our mutations and the proxy membrane mapping
*/
let affectedValues = new Map();
toRevoke.push(() => {
affectedValues.clear();
affectedValues = null;
});
/**
* @param {any} base
* @param {boolean} acrossBarrier - make a new proxy, but keep existing mutations
*/
function MutableProxy(base, acrossBarrier) {
if (!canBeAProxyTarget(base)) return base;
if (!acrossBarrier && affectedValues.has(base)) {
return affectedValues.get(base).proxy;
}
let deleted = acrossBarrier ?
affectedValues.get(base).deleted :
{__proto__: null};
let shadow = acrossBarrier ?
affectedValues.get(base).shadow :
{__proto__: Reflect.getPrototypeOf(base)};
if (Reflect.isExtensible(base) !== true) {
Reflect.preventExtensions(shadow);
}
function markMutated() {
affectedValues.get(base).mutated = true;
}
function ensureShadowDescriptor(name) {
if (name in shadow !== true) {
const desc = Reflect.getOwnPropertyDescriptor(base, name) || {
configurable: true,
enumerable: true,
value: undefined,
writable: true
};
return Reflect.defineProperty(
shadow,
name,
desc
);
}
}
const {revoke, proxy} = Proxy.revocable(
shadow, {
getOwnPropertyDescriptor(target, name) {
if (name in deleted) {
return undefined;
} else if (name in target) {
// already wrapped
return Reflect.getOwnPropertyDescriptor(shadow, name);
}
const desc = Reflect.getOwnPropertyDescriptor(base, name);
if (Reflect.getOwnPropertyDescriptor(desc, 'value')) {
desc.value = MutableProxy(desc.value, false);
}
if (Reflect.getOwnPropertyDescriptor(desc, 'get')) {
desc.get = MutableProxy(desc.get, false);
}
if (Reflect.getOwnPropertyDescriptor(desc, 'set')) {
desc.set = MutableProxy(desc.set, false);
}
return desc;
},
has(target, name) {
if (name in deleted) {
return Reflect.has(Reflect.getPrototypeOf(base), name);
} else if (name in target) {
return Reflect.has(target, name);
}
return Reflect.has(base, name);
},
deleteProperty(target, name) {
if (name in deleted) {
return true;
}
ensureShadowDescriptor(name);
let success = Reflect.deleteProperty(target, name);
if (success) {
markMutated();
deleted[name] = true;
}
return success;
},
defineProperty(target, name, desc) {
ensureShadowDescriptor(name);
let success = Object.defineProperty(target, name, desc);
if (success) {
markMutated();
delete deleted[name];
}
return success;
},
getPrototypeOf(target) {
return ReadOnlyProxy(Reflect.getPrototypeOf(target));
},
setPrototypeOf(target, value) {
markMutated();
return Reflect.setPrototypeOf(target, value);
},
ownKeys(target) {
return [
...new Set([
...Reflect.ownKeys(target),
...Reflect.ownKeys(base).filter(name => name in deleted === false)
])
]
},
get(target, name, receiver) {
if (name in deleted) {
return Reflect.get(Reflect.getPrototypeOf(base), name, shadow);
} else if (name in target) {
return Reflect.get(target, name, receiver);
}
return MutableProxy(Reflect.get(base, name, receiver), false);
},
set(target, name, value, receiver) {
if (name in deleted) {
// this could be firing a setter in the prototype chain
// that mutates the prototype chain
// unfortunately JS will still create own properties if
// 1. there is not a descriptor in the prototype
// 2. if there is a non-writable data descriptor in the prototype
// so we get to crawl and see if that is what is going on
let proto = Reflect.getPrototypeOf(base);
let seenPrototypes = new Set();
while (proto) {
if (seenPrototypes.has(proto)) {
// infinite loop, fun
throw new Error('Infinite prototype chain found');
}
seenPrototypes.add(proto);
const desc = Reflect.getOwnPropertyDescriptor(proto, name);
if (desc) {
return Reflect.set(proto, name, value, receiver);
}
proto = Reflect.getPrototypeOf(proto);
}
}
ensureShadowDescriptor(name);
const ret = Reflect.set(target, name, value, receiver);
if (ret) {
markMutated();
}
return ret;
}
}
);
affectedValues.set(base, {deleted, shadow, mutated: acrossBarrier ? affectedValues.get(base).mutated : false, revoke, proxy});
return proxy;
}
const proxy = MutableProxy(base, false);
const API = {
proxy,
/**
* Revokes all nested proxies to allow passing through a transition
* without performing a commit. Useful for passing through multiple
* event handlers while waiting
*/
barrier: () => {
if (committed) {
throw new Error('transaction already committed');
}
for (const [base, meta] of affectedValues) {
meta.revoke();
meta.proxy = MutableProxy(base, true);
}
return API.proxy = affectedValues.get(base).proxy;
},
/**
* Commits this fork as the final result of the transaction
* This invalidates all other forks of the same transaction
*/
commit: () => {
if (committed) {
throw new Error('transaction already committed');
}
committed = true;
// we need to wait to prevent extensions
const toPreventExtensions = new Set();
for (const [base, {deleted, shadow, mutated, revoke}] of affectedValues) {
revoke();
if (!mutated) continue;
for (var name in deleted) {
Reflect.deleteProperty(base, name);
}
for (const name of Reflect.ownKeys(shadow)) {
// FIXME?: This *assumes* success
Reflect.defineProperty(
base,
name,
Reflect.getOwnPropertyDescriptor(shadow, name));
}
if (Reflect.isExtensible(base) && !Reflect.isExtensible(shadow)) {
toPreventExtensions.add(base);
}
}
for (const obj of toPreventExtensions) {
Object.preventExtensions(obj);
}
for (let i = 0; i < toRevoke.length; i++) {
toRevoke[i]();
}
}
}
return API;
}
}
}
const root = { x: {y: 1} };
const tp = TransactionProxy(root);
const o1 = tp.fork();
o1.proxy.x.z = 2;
const o1PrebarrierProxy = o1.proxy;
const o2 = tp.fork();
o2.proxy.x.y = 3;
o1.barrier();
// o1PrebarrierProxy.x
o1.proxy.a = 1;
o2.commit();
// o1.commit();
console.log(root);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment