Created
July 5, 2023 18:11
-
-
Save justmoon/691d7409938ef5a5553a0f1a6e8ce5dd to your computer and use it in GitHub Desktop.
Signals with effects based on async generators
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 is an experimental API design for a signal/reactive library. | |
* | |
* Compared to @preact/signals, the main difference is the introduction of a | |
* different way of creating effects. Effects are async functions which contain | |
* a loop centered around a signal reader which is an async generator. | |
* | |
* This makes it so there is a persistent scope (outside of the loop) and a | |
* dynamic scope (inside the loop). This results in very readable reactive | |
* code that is mostly plain JavaScript. | |
* | |
* It also allows the use of the upcoming using keyword to automatically | |
* clean up resources when the loop exits. | |
* | |
* This code is an MVP and is obviously not optimized and not intended for | |
* real-world use. | |
*/ | |
type Dependent = () => void | |
interface Dependable { | |
addDependent(dependent: Dependent): void | |
removeDependent(dependent: Dependent): void | |
clearDependents(): void | |
} | |
interface ReadonlySignal<TType> extends Dependable { | |
read(): TType | |
} | |
interface Signal<TType> extends ReadonlySignal<TType> { | |
write(newValue: TType): void | |
update(reducer: (oldValue: TType) => TType): void | |
} | |
const createSignal = <TType>(initialValue: TType): Signal<TType> => { | |
let value = initialValue | |
const dependents = new Set<Dependent>() | |
const write = (newValue: TType) => { | |
value = newValue | |
for (const dependent of [...dependents]) { | |
dependent() | |
} | |
} | |
return { | |
read() { | |
return value | |
}, | |
write, | |
update(reducer) { | |
write(reducer(value)) | |
}, | |
addDependent(dependent: Dependent) { | |
dependents.add(dependent) | |
}, | |
removeDependent(dependent: Dependent) { | |
dependents.delete(dependent) | |
}, | |
clearDependents() { | |
dependents.clear() | |
} | |
} | |
} | |
interface SignalReader extends Dependable { | |
get<TType>(signal: ReadonlySignal<TType>): TType | |
clearDependencies(): void | |
} | |
const createReader = (): SignalReader => { | |
const dependencies = new Set<Dependable>() | |
const dependents = new Set<Dependent>() | |
const notify = () => { | |
for (const dependent of dependents) { | |
dependent() | |
} | |
} | |
const reader: SignalReader = { | |
get(signal) { | |
signal.addDependent(notify) | |
dependencies.add(signal) | |
return signal.read() | |
}, | |
clearDependencies() { | |
for (const dependency of dependencies) { | |
dependency.removeDependent(notify) | |
} | |
dependencies.clear() | |
}, | |
addDependent(dependent: Dependent) { | |
if (dependents.size === 0) { | |
for (const dependency of dependencies) { | |
dependency.addDependent(notify) | |
} | |
} | |
dependents.add(dependent) | |
}, | |
removeDependent(dependent: Dependent) { | |
dependents.delete(dependent) | |
if (dependents.size === 0) { | |
for (const dependency of dependencies) { | |
dependency.removeDependent(notify) | |
} | |
} | |
}, | |
clearDependents() { | |
dependents.clear() | |
for (const dependency of dependencies) { | |
dependency.removeDependent(notify) | |
} | |
} | |
} | |
return reader | |
} | |
const createComputed = <TResult>( | |
computation: (reader: SignalReader) => TResult | |
): ReadonlySignal<TResult> => { | |
let value!: TResult | |
const reader = createReader() | |
const dependents = new Set<Dependent>() | |
const computed = { | |
addDependent(dependent: Dependent) { | |
if (dependents.size === 0) { | |
reader.addDependent(compute) | |
} | |
dependents.add(dependent) | |
}, | |
removeDependent(dependent: Dependent) { | |
dependents.delete(dependent) | |
if (dependents.size === 0) { | |
reader.removeDependent(compute) | |
} | |
}, | |
clearDependents() { | |
dependents.clear() | |
reader.removeDependent(compute) | |
}, | |
read() { | |
compute() | |
return value | |
}, | |
} | |
function compute() { | |
reader.clearDependencies() | |
const previousValue = value | |
value = computation(reader) | |
if (previousValue !== value) { | |
for (const dependent of dependents) { | |
dependent() | |
} | |
} | |
} | |
compute() | |
return computed | |
} | |
interface IterativeSignalReader { | |
[Symbol.asyncIterator](): AsyncGenerator<SignalReader, never, void> | |
} | |
const createDeferred = () => { | |
let resolve!: () => void | |
const promise = new Promise<void>((_resolve) => (resolve = _resolve)) | |
return { | |
promise, | |
resolve, | |
} | |
} | |
const signals = { | |
async *[Symbol.asyncIterator]() { | |
const reader = createReader() | |
try { | |
while (true) { | |
const deferred = createDeferred() | |
reader.addDependent(deferred.resolve) | |
yield reader | |
await deferred.promise | |
reader.removeDependent(deferred.resolve) | |
reader.clearDependencies() | |
} | |
} finally { | |
reader.clearDependencies() | |
} | |
}, | |
} | |
const exampleSignal = createSignal(1) | |
const exampleComputed = createComputed((reader) => { | |
return reader.get(exampleSignal) * 2 | |
}) | |
async function exampleEffect() { | |
for await (const reader of signals) { | |
const value = reader.get(exampleSignal) | |
const double = reader.get(exampleComputed) | |
console.log(`${value} * 2 = ${double}`) | |
if (value >= 5) { | |
break | |
} | |
} | |
} | |
exampleEffect() | |
setInterval(() => exampleSignal.update((value) => value + 1), 1000) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment