Skip to content

Instantly share code, notes, and snippets.

@justmoon
Created July 5, 2023 18:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save justmoon/691d7409938ef5a5553a0f1a6e8ce5dd to your computer and use it in GitHub Desktop.
Save justmoon/691d7409938ef5a5553a0f1a6e8ce5dd to your computer and use it in GitHub Desktop.
Signals with effects based on async generators
/**
* 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