You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
163 lines
6.7 KiB
163 lines
6.7 KiB
// This currentContext variable will only be used if the makeSlotClass |
|
// function is called, which happens only if this is the first copy of the |
|
// @wry/context package to be imported. |
|
let currentContext = null; |
|
// This unique internal object is used to denote the absence of a value |
|
// for a given Slot, and is never exposed to outside code. |
|
const MISSING_VALUE = {}; |
|
let idCounter = 1; |
|
// Although we can't do anything about the cost of duplicated code from |
|
// accidentally bundling multiple copies of the @wry/context package, we can |
|
// avoid creating the Slot class more than once using makeSlotClass. |
|
const makeSlotClass = () => class Slot { |
|
constructor() { |
|
// If you have a Slot object, you can find out its slot.id, but you cannot |
|
// guess the slot.id of a Slot you don't have access to, thanks to the |
|
// randomized suffix. |
|
this.id = [ |
|
"slot", |
|
idCounter++, |
|
Date.now(), |
|
Math.random().toString(36).slice(2), |
|
].join(":"); |
|
} |
|
hasValue() { |
|
for (let context = currentContext; context; context = context.parent) { |
|
// We use the Slot object iself as a key to its value, which means the |
|
// value cannot be obtained without a reference to the Slot object. |
|
if (this.id in context.slots) { |
|
const value = context.slots[this.id]; |
|
if (value === MISSING_VALUE) |
|
break; |
|
if (context !== currentContext) { |
|
// Cache the value in currentContext.slots so the next lookup will |
|
// be faster. This caching is safe because the tree of contexts and |
|
// the values of the slots are logically immutable. |
|
currentContext.slots[this.id] = value; |
|
} |
|
return true; |
|
} |
|
} |
|
if (currentContext) { |
|
// If a value was not found for this Slot, it's never going to be found |
|
// no matter how many times we look it up, so we might as well cache |
|
// the absence of the value, too. |
|
currentContext.slots[this.id] = MISSING_VALUE; |
|
} |
|
return false; |
|
} |
|
getValue() { |
|
if (this.hasValue()) { |
|
return currentContext.slots[this.id]; |
|
} |
|
} |
|
withValue(value, callback, |
|
// Given the prevalence of arrow functions, specifying arguments is likely |
|
// to be much more common than specifying `this`, hence this ordering: |
|
args, thisArg) { |
|
const slots = { |
|
__proto__: null, |
|
[this.id]: value, |
|
}; |
|
const parent = currentContext; |
|
currentContext = { parent, slots }; |
|
try { |
|
// Function.prototype.apply allows the arguments array argument to be |
|
// omitted or undefined, so args! is fine here. |
|
return callback.apply(thisArg, args); |
|
} |
|
finally { |
|
currentContext = parent; |
|
} |
|
} |
|
// Capture the current context and wrap a callback function so that it |
|
// reestablishes the captured context when called. |
|
static bind(callback) { |
|
const context = currentContext; |
|
return function () { |
|
const saved = currentContext; |
|
try { |
|
currentContext = context; |
|
return callback.apply(this, arguments); |
|
} |
|
finally { |
|
currentContext = saved; |
|
} |
|
}; |
|
} |
|
// Immediately run a callback function without any captured context. |
|
static noContext(callback, |
|
// Given the prevalence of arrow functions, specifying arguments is likely |
|
// to be much more common than specifying `this`, hence this ordering: |
|
args, thisArg) { |
|
if (currentContext) { |
|
const saved = currentContext; |
|
try { |
|
currentContext = null; |
|
// Function.prototype.apply allows the arguments array argument to be |
|
// omitted or undefined, so args! is fine here. |
|
return callback.apply(thisArg, args); |
|
} |
|
finally { |
|
currentContext = saved; |
|
} |
|
} |
|
else { |
|
return callback.apply(thisArg, args); |
|
} |
|
} |
|
}; |
|
function maybe(fn) { |
|
try { |
|
return fn(); |
|
} |
|
catch (ignored) { } |
|
} |
|
// We store a single global implementation of the Slot class as a permanent |
|
// non-enumerable property of the globalThis object. This obfuscation does |
|
// nothing to prevent access to the Slot class, but at least it ensures the |
|
// implementation (i.e. currentContext) cannot be tampered with, and all copies |
|
// of the @wry/context package (hopefully just one) will share the same Slot |
|
// implementation. Since the first copy of the @wry/context package to be |
|
// imported wins, this technique imposes a steep cost for any future breaking |
|
// changes to the Slot class. |
|
const globalKey = "@wry/context:Slot"; |
|
const host = |
|
// Prefer globalThis when available. |
|
// https://github.com/benjamn/wryware/issues/347 |
|
maybe(() => globalThis) || |
|
// Fall back to global, which works in Node.js and may be converted by some |
|
// bundlers to the appropriate identifier (window, self, ...) depending on the |
|
// bundling target. https://github.com/endojs/endo/issues/576#issuecomment-1178515224 |
|
maybe(() => global) || |
|
// Otherwise, use a dummy host that's local to this module. We used to fall |
|
// back to using the Array constructor as a namespace, but that was flagged in |
|
// https://github.com/benjamn/wryware/issues/347, and can be avoided. |
|
Object.create(null); |
|
// Whichever globalHost we're using, make TypeScript happy about the additional |
|
// globalKey property. |
|
const globalHost = host; |
|
export const Slot = globalHost[globalKey] || |
|
// Earlier versions of this package stored the globalKey property on the Array |
|
// constructor, so we check there as well, to prevent Slot class duplication. |
|
Array[globalKey] || |
|
(function (Slot) { |
|
try { |
|
Object.defineProperty(globalHost, globalKey, { |
|
value: Slot, |
|
enumerable: false, |
|
writable: false, |
|
// When it was possible for globalHost to be the Array constructor (a |
|
// legacy Slot dedup strategy), it was important for the property to be |
|
// configurable:true so it could be deleted. That does not seem to be as |
|
// important when globalHost is the global object, but I don't want to |
|
// cause similar problems again, and configurable:true seems safest. |
|
// https://github.com/endojs/endo/issues/576#issuecomment-1178274008 |
|
configurable: true |
|
}); |
|
} |
|
finally { |
|
return Slot; |
|
} |
|
})(makeSlotClass()); |
|
//# sourceMappingURL=slot.js.map
|