Created
April 27, 2024 09:08
-
-
Save saolof/f75a9545ddeb26df80f298900fda8ba9 to your computer and use it in GitHub Desktop.
This is just deepsignal concatenated on top of usignal with typescript types removed, for the sake of being easily importable with a script tag in personal projects
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
/* start of usignal */ | |
/* usignal license: https://github.com/WebReflection/usignal/blob/18920ecf613d31b4a8f20f1acc4bcb111a3ab987/LICENSE */ | |
/*! (c) Andrea Giammarchi */ | |
const {is} = Object; | |
let batches; | |
/** | |
* Execute a callback that will not side-effect until its top-most batch is | |
* completed. | |
* @param {() => void} callback a function that batches changes to be notified | |
* through signals. | |
*/ | |
export const batch = callback => { | |
const prev = batches; | |
batches = prev || []; | |
try { | |
callback(); | |
if (!prev) | |
for (const {value} of batches); | |
} | |
finally { batches = prev } | |
}; | |
/** | |
* A signal with a value property also exposed via toJSON, toString and valueOf. | |
* When created via computed, the `value` property is **readonly**. | |
* @template T | |
*/ | |
export class Signal { | |
constructor(value) { | |
this._ = value; | |
} | |
/** @returns {T} */ | |
toJSON() { return this.value } | |
/** @returns {string} */ | |
toString() { return String(this.value) } | |
/** @returns {T} */ | |
valueOf() { return this.value } | |
} | |
let computedSignal; | |
/** | |
* @template T | |
* @extends {Signal<T>} | |
*/ | |
class Computed extends Signal { | |
/** | |
* @private | |
* @type{Reactive<T>} | |
*/ | |
s | |
/** | |
* @param {(v: T) => T} _ | |
* @param {T} v | |
* @param {{ equals?: Equals<T> }} o | |
* @param {boolean} f | |
*/ | |
constructor(_, v, o, f) { | |
super(_); | |
this.f = f; // is effect? | |
this.$ = true; // should update ("value for money") | |
this.r = new Set; // related signals | |
this.s = new Reactive(v, o); // signal | |
} | |
peek() { return this.s.peek() } | |
get value() { | |
if (this.$) { | |
const prev = computedSignal; | |
computedSignal = this; | |
try { this.s.value = this._(this.s._) } | |
finally { | |
this.$ = false; | |
computedSignal = prev; | |
} | |
} | |
return this.s.value; | |
} | |
} | |
const defaults = {async: false, equals: true}; | |
/** | |
* Returns a read-only Signal that is invoked only when any of the internally | |
* used signals, as in within the callback, is unknown or updated. | |
* @type {<R, V, T = unknown extends V ? R : R|V>(fn: (v: T) => R, value?: V, options?: { equals?: Equals<T> }) => ComputedSignal<T>} | |
*/ | |
export const computed = (fn, value, options = defaults) => | |
new Computed(fn, value, options, false); | |
let outerEffect; | |
const empty = []; | |
const noop = () => {}; | |
const dispose = ({s}) => { | |
if (typeof s._ === 'function') | |
s._ = s._(); | |
}; | |
export class FX extends Computed { | |
constructor(_, v, o) { | |
super(_, v, o, true); | |
this.e = empty; | |
} | |
run() { | |
this.$ = true; | |
this.value; | |
return this; | |
} | |
stop() { | |
this._ = noop; | |
this.r.clear(); | |
this.s.c.clear(); | |
} | |
} | |
export class Effect extends FX { | |
constructor(_, v, o) { | |
super(_, v, o); | |
this.i = 0; // index | |
this.a = !!o.async; // async | |
this.m = true; // microtask | |
this.e = []; // effects | |
// "I am effects" ^_^;; | |
} | |
get value() { | |
this.a ? this.async() : this.sync(); | |
} | |
async() { | |
if (this.m) { | |
this.m = false; | |
queueMicrotask(() => { | |
this.m = true; | |
this.sync(); | |
}); | |
} | |
} | |
sync() { | |
const prev = outerEffect; | |
(outerEffect = this).i = 0; | |
dispose(this); | |
super.value; | |
outerEffect = prev; | |
} | |
stop() { | |
super.stop(); | |
dispose(this); | |
for (const effect of this.e.splice(0)) | |
effect.stop(); | |
} | |
} | |
/** | |
* Invokes a function when any of its internal signals or computed values change. | |
* | |
* Returns a dispose callback. | |
* @template T | |
* @type {<T>(fn: (v: T) => T, value?: T, options?: { async?: boolean }) => () => void} | |
*/ | |
export const effect = (callback, value, options = defaults) => { | |
let unique; | |
if (outerEffect) { | |
const {i, e} = outerEffect; | |
const isNew = i === e.length; | |
// bottleneck: | |
// there's literally no way to optimize this path *unless* the callback is | |
// already a known one. however, latter case is not really common code so | |
// the question is: should I optimize this more than this? 'cause I don't | |
// think the amount of code needed to understand if a callback is *likely* | |
// the same as before makes any sense + correctness would be trashed. | |
if (isNew || e[i]._ !== callback) { | |
if (!isNew) e[i].stop(); | |
e[i] = new Effect(callback, value, options).run(); | |
} | |
unique = e[i]; | |
outerEffect.i++; | |
} | |
else | |
unique = new Effect(callback, value, options).run(); | |
return () => { unique.stop() }; | |
}; | |
const skip = () => false; | |
/** | |
* @template T | |
* @extends {Signal<T>} | |
*/ | |
class Reactive extends Signal { | |
constructor(_, {equals}) { | |
super(_) | |
this.c = new Set; // computeds | |
this.s = equals === true ? is : (equals || skip); // (don't) skip updates | |
} | |
/** | |
* Allows to get signal.value without subscribing to updates in an effect | |
* @returns {T} | |
*/ | |
peek() { return this._ } | |
/** @returns {T} */ | |
get value() { | |
if (computedSignal) { | |
this.c.add(computedSignal); | |
computedSignal.r.add(this); | |
} | |
return this._; | |
} | |
set value(_) { | |
const prev = this._; | |
if (!this.s((this._ = _), prev)) { | |
if (this.c.size) { | |
const effects = []; | |
const stack = [this]; | |
for (const signal of stack) { | |
for (const computed of signal.c) { | |
if (!computed.$ && computed.r.has(signal)) { | |
computed.r.clear(); | |
computed.$ = true; | |
if (computed.f) { | |
effects.push(computed); | |
const stack = [computed]; | |
for (const c of stack) { | |
for (const effect of c.e) { | |
effect.r.clear(); | |
effect.$ = true; | |
stack.push(effect); | |
} | |
} | |
} | |
else | |
stack.push(computed.s); | |
} | |
} | |
} | |
for (const effect of effects) | |
batches ? batches.push(effect) : effect.value; | |
} | |
} | |
} | |
} | |
/** | |
* Returns a writable Signal that side-effects whenever its value gets updated. | |
* @template T | |
* @type {<T>(initialValue: T, options?: { equals?: Equals<T> }) => ReactiveSignal<T>} | |
*/ | |
export const signal = (value, options = defaults) => new Reactive(value, options); | |
/** | |
* @template [T=any] | |
* @typedef {boolean | ((prev: T, next: T) => boolean)} Equals | |
*/ | |
/** | |
* @public | |
* @template T | |
* @typedef {Omit<Reactive<T>, '_'|'s'|'c'>} ReactiveSignal<T> | |
*/ | |
/** | |
* @public | |
* @template T | |
* @typedef {Omit<Computed<T>, '$'|'s'|'f'|'r'|'_'>} ComputedSignal<T> | |
*/ | |
/* end of usignal */ | |
/* start of typestripped deepsignal */ | |
/* License: https://github.com/luisherranz/deepsignal/blob/8fb50d677615b368eae90a4e8b9e4570fa459f62/LICENSE */ | |
const proxyToSignals = new WeakMap() | |
const objToProxy = new WeakMap() | |
const arrayToArrayOfSignals = new WeakMap() | |
const ignore = new WeakSet() | |
const objToIterable = new WeakMap() | |
const rg = /^\$/ | |
const descriptor = Object.getOwnPropertyDescriptor | |
let peeking = false | |
export const deepSignal = obj => { | |
if (!shouldProxy(obj)) throw new Error("This object can't be observed.") | |
if (!objToProxy.has(obj)) | |
objToProxy.set(obj, createProxy(obj, objectHandlers)) | |
return objToProxy.get(obj) | |
} | |
export const peek = (obj, key) => { | |
peeking = true | |
const value = obj[key] | |
try { | |
peeking = false | |
} catch (e) {} | |
return value | |
} | |
const isShallow = Symbol("shallow") | |
export function shallow(obj) { | |
ignore.add(obj) | |
return obj | |
} | |
const createProxy = (target, handlers) => { | |
const proxy = new Proxy(target, handlers) | |
ignore.add(proxy) | |
return proxy | |
} | |
const throwOnMutation = () => { | |
throw new Error("Don't mutate the signals directly.") | |
} | |
const get = isArrayOfSignals => (target, fullKey, receiver) => { | |
if (peeking) return Reflect.get(target, fullKey, receiver) | |
let returnSignal = isArrayOfSignals || fullKey[0] === "$" | |
if (!isArrayOfSignals && returnSignal && Array.isArray(target)) { | |
if (fullKey === "$") { | |
if (!arrayToArrayOfSignals.has(target)) | |
arrayToArrayOfSignals.set(target, createProxy(target, arrayHandlers)) | |
return arrayToArrayOfSignals.get(target) | |
} | |
returnSignal = fullKey === "$length" | |
} | |
if (!proxyToSignals.has(receiver)) proxyToSignals.set(receiver, new Map()) | |
const signals = proxyToSignals.get(receiver) | |
const key = returnSignal ? fullKey.replace(rg, "") : fullKey | |
if (!signals.has(key) && typeof descriptor(target, key)?.get === "function") { | |
signals.set( | |
key, | |
computed(() => Reflect.get(target, key, receiver)) | |
) | |
} else { | |
let value = Reflect.get(target, key, receiver) | |
if (returnSignal && typeof value === "function") return | |
if (typeof key === "symbol" && wellKnownSymbols.has(key)) return value | |
if (!signals.has(key)) { | |
if (shouldProxy(value)) { | |
if (!objToProxy.has(value)) | |
objToProxy.set(value, createProxy(value, objectHandlers)) | |
value = objToProxy.get(value) | |
} | |
signals.set(key, signal(value)) | |
} | |
} | |
return returnSignal ? signals.get(key) : signals.get(key).value | |
} | |
const objectHandlers = { | |
get: get(false), | |
set(target, fullKey, val, receiver) { | |
if (typeof descriptor(target, fullKey)?.set === "function") | |
return Reflect.set(target, fullKey, val, receiver) | |
if (!proxyToSignals.has(receiver)) proxyToSignals.set(receiver, new Map()) | |
const signals = proxyToSignals.get(receiver) | |
if (fullKey[0] === "$") { | |
if (!(val instanceof Signal)) throwOnMutation() | |
const key = fullKey.replace(rg, "") | |
signals.set(key, val) | |
return Reflect.set(target, key, val.peek(), receiver) | |
} else { | |
let internal = val | |
if (shouldProxy(val)) { | |
if (!objToProxy.has(val)) | |
objToProxy.set(val, createProxy(val, objectHandlers)) | |
internal = objToProxy.get(val) | |
} | |
const isNew = !(fullKey in target) | |
const result = Reflect.set(target, fullKey, val, receiver) | |
if (!signals.has(fullKey)) signals.set(fullKey, signal(internal)) | |
else signals.get(fullKey).value = internal | |
if (isNew && objToIterable.has(target)) objToIterable.get(target).value++ | |
if (Array.isArray(target) && signals.has("length")) | |
signals.get("length").value = target.length | |
return result | |
} | |
}, | |
deleteProperty(target, key) { | |
if (key[0] === "$") throwOnMutation() | |
const signals = proxyToSignals.get(objToProxy.get(target)) | |
const result = Reflect.deleteProperty(target, key) | |
if (signals && signals.has(key)) signals.get(key).value = undefined | |
objToIterable.has(target) && objToIterable.get(target).value++ | |
return result | |
}, | |
ownKeys(target) { | |
if (!objToIterable.has(target)) objToIterable.set(target, signal(0)) | |
objToIterable._ = objToIterable.get(target).value | |
return Reflect.ownKeys(target) | |
} | |
} | |
const arrayHandlers = { | |
get: get(true), | |
set: throwOnMutation, | |
deleteProperty: throwOnMutation | |
} | |
const wellKnownSymbols = new Set( | |
Object.getOwnPropertyNames(Symbol) | |
.map(key => Symbol[key]) | |
.filter(value => typeof value === "symbol") | |
) | |
const supported = new Set([Object, Array]) | |
const shouldProxy = val => { | |
if (typeof val !== "object" || val === null) return false | |
return supported.has(val.constructor) && !ignore.has(val) | |
} | |
/* end of typestripped deepsignal */ | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment