-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: simplified observer tracking
Simplifies observer/dependency tracking by making signals the only bookkeepers of their observers. Pros: - Shaves off 300 bytes. - Observer tracking and code is simpler to reason about. Cons: - Observers don't get cleared from memory after they're disposed of until the signal that is tracking them is written into again. This might cause a memory leak in an extreme case where there's a signal that doesn't change, but keeps getting more and more observers subscribed to it in a long observe→dispose chain, such as (P)React components mounting and unmounting.
- Loading branch information
1 parent
2724fd2
commit bafbb4c
Showing
4 changed files
with
380 additions
and
90 deletions.
There are no files selected for viewing
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,322 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.once = exports.reaction = exports.createAction = exports.action = exports.computed = exports.signal = exports.toJS = exports.toJSON = exports.fnName = exports.nameFn = exports.CircularReactionError = void 0; | ||
const IS_COMPUTED = Symbol('computed'); | ||
const IS_STATIN_ERROR = Symbol('statin_error'); | ||
const observersStack = []; | ||
const dependencyMap = new WeakMap(); | ||
const observerMap = new WeakMap(); | ||
const nameCounters = {}; | ||
let effectQueue = null; | ||
let doNotTrack = false; | ||
let currentEffect; | ||
const reactionsDepthMap = new Map(); | ||
const MAX_REACTION_DEPTH = 100; | ||
class CircularReactionError extends Error { | ||
constructor(message) { | ||
super(message); | ||
this[IS_STATIN_ERROR] = true; | ||
} | ||
} | ||
exports.CircularReactionError = CircularReactionError; | ||
function nameFn(name, fn) { | ||
fn.displayName = name; | ||
return fn; | ||
} | ||
exports.nameFn = nameFn; | ||
function fnName(fn, fallbackName = 'Unknown') { | ||
return ((fn && (fn.displayName || fn.name)) || | ||
`${fallbackName}${nameCounters[fallbackName] ? nameCounters[fallbackName]++ : (nameCounters[fallbackName] = 0)}`); | ||
} | ||
exports.fnName = fnName; | ||
const isComputed = (value) => value[IS_COMPUTED] === true; | ||
function describeError(maybeError, source) { | ||
var _a; | ||
if ((_a = maybeError) === null || _a === void 0 ? void 0 : _a[IS_STATIN_ERROR]) | ||
return maybeError; | ||
const error = maybeError instanceof Error ? maybeError : new Error(`${maybeError}`); | ||
error.message = `Error in ${source}: ${error.message}`; | ||
error[IS_STATIN_ERROR] = true; | ||
return error; | ||
} | ||
function registerDependency(dependency) { | ||
const observer = observersStack[observersStack.length - 1]; | ||
if (!observer || doNotTrack) | ||
return; | ||
let observersSet = observerMap.get(dependency); | ||
if (!observersSet) { | ||
observersSet = new Set(); | ||
observerMap.set(dependency, observersSet); | ||
} | ||
observersSet.add(observer); | ||
let dependencySet = dependencyMap.get(observer); | ||
if (!dependencySet) { | ||
dependencySet = new Set(); | ||
dependencyMap.set(observer, dependencySet); | ||
} | ||
dependencySet.add(dependency); | ||
} | ||
function clearDependencies(observer) { | ||
let dependencySet = dependencyMap.get(observer); | ||
if (!dependencySet) | ||
return; | ||
for (let dependency of dependencySet) { | ||
const trackersSet = observerMap.get(dependency); | ||
if (trackersSet) { | ||
trackersSet.delete(observer); | ||
if (trackersSet.size === 0) | ||
observerMap.delete(dependency); | ||
} | ||
} | ||
dependencyMap.delete(observer); | ||
} | ||
function triggerObservers(dependency) { | ||
const observersSet = observerMap.get(dependency); | ||
const currentObserver = observersStack[observersStack.length - 1]; | ||
if (observersSet) { | ||
for (let observer of [...observersSet]) { | ||
if (observer === currentObserver) | ||
continue; | ||
if (effectQueue && !isComputed(observer)) { | ||
effectQueue.add(observer); | ||
} | ||
else { | ||
observer(); | ||
} | ||
} | ||
} | ||
} | ||
function resumeTracking(fn) { | ||
let parentDoNotTrack = doNotTrack; | ||
doNotTrack = false; | ||
try { | ||
fn(); | ||
} | ||
finally { | ||
doNotTrack = parentDoNotTrack; | ||
} | ||
} | ||
function toJSON(value) { | ||
return (value === null || value === void 0 ? void 0 : value.toJSON) ? value.toJSON() : value; | ||
} | ||
exports.toJSON = toJSON; | ||
function toJS(value) { | ||
const stringified = JSON.stringify(value); | ||
return stringified == null ? stringified : JSON.parse(stringified); | ||
} | ||
exports.toJS = toJS; | ||
function handleOrLog(error, onError) { | ||
try { | ||
if (onError) | ||
onError(error); | ||
else | ||
throw error; | ||
} | ||
catch (error) { | ||
console.error(error); | ||
} | ||
} | ||
function signal(value) { | ||
function getSet(value) { | ||
return arguments.length ? write(value) : read(); | ||
} | ||
getSet.value = value; | ||
getSet.changed = () => { | ||
if (!effectQueue) | ||
bulkEffects(() => triggerObservers(getSet)); | ||
else | ||
triggerObservers(getSet); | ||
}; | ||
getSet.edit = (editor) => { | ||
editor(getSet.value); | ||
getSet.changed(); | ||
}; | ||
getSet.toJSON = () => toJSON(read()); | ||
function read() { | ||
registerDependency(getSet); | ||
return getSet.value; | ||
} | ||
function write(value) { | ||
if (value !== getSet.value) { | ||
getSet.value = value; | ||
getSet.changed(); | ||
} | ||
} | ||
return getSet; | ||
} | ||
exports.signal = signal; | ||
function computed(compute) { | ||
const name = fnName(compute, 'Computed'); | ||
let value; | ||
let hasChanges = true; | ||
let hasError = false; | ||
let error; | ||
function computedObserver() { | ||
value = undefined; | ||
hasError = false; | ||
error = undefined; | ||
hasChanges = true; | ||
clearDependencies(computedObserver); | ||
triggerObservers(computedObserver); | ||
} | ||
computedObserver.displayName = name; | ||
computedObserver[IS_COMPUTED] = true; | ||
function get() { | ||
registerDependency(computedObserver); | ||
if (hasChanges) { | ||
observersStack.push(computedObserver); | ||
try { | ||
hasError = false; | ||
error = undefined; | ||
resumeTracking(() => (value = compute())); | ||
} | ||
catch (error_) { | ||
hasError = true; | ||
error = describeError(error_, name); | ||
} | ||
finally { | ||
hasChanges = false; | ||
observersStack.pop(); | ||
} | ||
} | ||
if (hasError) | ||
throw error; | ||
return value; | ||
} | ||
get.toJSON = () => toJSON(get()); | ||
return get; | ||
} | ||
exports.computed = computed; | ||
function bulkEffects(effect, { onError } = {}) { | ||
let isRootEffect = false; | ||
if (effectQueue == null) { | ||
isRootEffect = true; | ||
effectQueue = new Set(); | ||
reactionsDepthMap.clear(); | ||
} | ||
let result; | ||
const parentEffect = currentEffect; | ||
currentEffect = effect; | ||
try { | ||
result = effect(); | ||
} | ||
catch (error) { | ||
error = describeError(error, fnName(currentEffect)); | ||
if (onError) | ||
onError(error); | ||
else | ||
console.error(error); | ||
} | ||
finally { | ||
if (isRootEffect) { | ||
while (effectQueue.size > 0) { | ||
const effects = effectQueue; | ||
effectQueue = new Set(); | ||
for (let effect of effects) | ||
effect(); | ||
} | ||
reactionsDepthMap.clear(); | ||
effectQueue = null; | ||
} | ||
currentEffect = parentEffect; | ||
} | ||
return result; | ||
} | ||
function action(fn) { | ||
const parentDoNotTrack = doNotTrack; | ||
doNotTrack = true; | ||
try { | ||
return bulkEffects(nameFn(fnName(fn, 'Action'), fn), { | ||
onError: (error) => { | ||
throw error; | ||
}, | ||
}); | ||
} | ||
finally { | ||
doNotTrack = parentDoNotTrack; | ||
} | ||
} | ||
exports.action = action; | ||
function createAction(run) { | ||
return (...args) => action(() => run(...args)); | ||
} | ||
exports.createAction = createAction; | ||
function reaction(action, effectOrOptions, options) { | ||
let value; | ||
let onceDisposer; | ||
let effect; | ||
if (typeof effectOrOptions === 'function') { | ||
effect = effectOrOptions; | ||
} | ||
else { | ||
options = effectOrOptions; | ||
} | ||
const { immediate, onError } = options || {}; | ||
const actionName = fnName(action, effect ? 'ReactionAction' : 'Reaction'); | ||
const effectName = fnName(effect, 'ReactionEffect'); | ||
const actionWrap = nameFn(actionName, (dispose) => { | ||
value = action(dispose); | ||
}); | ||
const effectWrap = nameFn(effectName, () => { | ||
try { | ||
effect === null || effect === void 0 ? void 0 : effect(value, dispose); | ||
} | ||
catch (error) { | ||
handleError(describeError(error, effectName)); | ||
} | ||
}); | ||
const onceEffect = nameFn(effectName, () => { | ||
const stackSize = reactionsDepthMap.get(onceEffect) || 0; | ||
if (stackSize > MAX_REACTION_DEPTH) { | ||
throw new CircularReactionError(`Circular reaction in ${actionName}${effect ? `->${effectName}` : ''}:\n---\n${(effect || action).toString()}\n---`); | ||
} | ||
reactionsDepthMap.set(onceEffect, stackSize + 1); | ||
createOnceLoop(); | ||
effectWrap(); | ||
}); | ||
function dispose() { | ||
onceDisposer === null || onceDisposer === void 0 ? void 0 : onceDisposer(); | ||
} | ||
function handleError(error) { | ||
handleOrLog(error, onError ? (error) => onError(error, dispose) : undefined); | ||
} | ||
function createOnceLoop() { | ||
onceDisposer = once(actionWrap, onceEffect, { | ||
onError: (error, dispose) => { | ||
onceDisposer = dispose; | ||
handleError(error); | ||
}, | ||
}); | ||
} | ||
createOnceLoop(); | ||
if (immediate) | ||
effectWrap(); | ||
return dispose; | ||
} | ||
exports.reaction = reaction; | ||
function once(observe, effect, { onError } = {}) { | ||
const dispose = () => clearDependencies(observer); | ||
const observerName = fnName(observe, 'OnceObserver'); | ||
const effectName = fnName(effect, 'OnceEffect'); | ||
const errorHandler = onError ? (error) => onError(error, dispose) : undefined; | ||
const observer = nameFn(observerName, () => { | ||
dispose(); | ||
bulkEffects(effect, { onError: (error) => handleOrLog(describeError(error, effectName), errorHandler) }); | ||
}); | ||
observersStack.push(observer); | ||
try { | ||
let internalDisposerCalled = false; | ||
const internalDisposer = () => (internalDisposerCalled = true); | ||
resumeTracking(() => observe(internalDisposer)); | ||
if (internalDisposerCalled) | ||
dispose(); | ||
} | ||
catch (error) { | ||
handleOrLog(describeError(error, observerName), errorHandler); | ||
} | ||
finally { | ||
observersStack.pop(); | ||
} | ||
return dispose; | ||
} | ||
exports.once = once; |
Oops, something went wrong.