Last active
April 15, 2021 15:22
-
-
Save bmeck/0f74a23d15d87fb2ba400123eea818cb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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