Skip to content

Instantly share code, notes, and snippets.

@queerviolet
Created January 10, 2020 17:46
Show Gist options
  • Save queerviolet/d8b56bb06e0e18b72619d7bd79fd3bce to your computer and use it in GitHub Desktop.
Save queerviolet/d8b56bb06e0e18b72619d7bd79fd3bce to your computer and use it in GitHub Desktop.
a cellular reactive framework
export const Source_outputs = Symbol('Cell outputs')
export const Sink_inputs = Symbol('Cell inputs')
export const Source_willOpen = Symbol('Source will attach its first output')
export const Source_didClose = Symbol('Source did detach its last output')
export const Sink_willOpen = Symbol('Sink did attach its first input')
export const Sink_didClose = Symbol('Sink did detach its last input')
export const Sink_update = Symbol('Sink update')
export type Source<O extends object> = O & {
[Source_outputs]?: Set<Sink<O>>
[Source_willOpen]?(signal: Signal): void
[Source_didClose]?(signal: Signal): void
}
export interface Sink<I extends object> {
[Sink_inputs]?: Set<Source<I>>
[Sink_update]?(signal: Signal): void
[Sink_willOpen]?(signal: Signal): void
[Sink_didClose]?(signal: Signal): void
}
export function source<T extends object>(object: T, onClose?: (signal: Signal) => void): Source<T> {
(object as any)[Source_didClose] = onClose || (object as any)[Source_didClose]
return object
}
export function sink<I extends object, T extends object>(object: T, update?: (signal: Signal<I>) => void): T & Sink<I> {
(object as any)[Sink_update] = update || (object as any)[Sink_update]
return object
}
export function attach<T extends object>(source: Source<T>, sink: Sink<T>) {
const outputs = source[Source_outputs] = source[Source_outputs] || new Set
const inputs = sink[Sink_inputs] = sink[Sink_inputs] || new Set
outputs.add(sink)
inputs.add(source)
return new Signal().update(source, sink)
}
export function detach<T extends object>(sink: Sink<T>, source?: Source<T>) {
return new Signal().detach(sink, source)
}
export function fire<T extends object>(source: Source<T>) {
return new Signal().fire(source)
}
const Node_edges = Symbol('Edges out of this node')
export function edge<E extends Edge>(create: E): E {
function findOrCreate(source: any, ...args: any[]) {
const edges = source[Node_edges] = source[Node_edges] || new WeakMap
const existing = edges.get(create)
if (existing) return existing
const created = create.call(this, source, ...args)
edges.set(create, created)
return created
}
return findOrCreate as any
}
export type Edge = (source: object, ...args: any) => any
export class Signal<I extends object=any> {
changed: Set<Source<I>> = new Set
closedSources: Set<Source<any>> = new Set
closedSinks: Set<Sink<any>> = new Set
static nextId = 0
id = Signal.nextId++
constructor() {
console.log(`Created signal #${this.id}`)
}
update<T extends object>(source: Source<T>, sink: Sink<T>) {
console.log(`[#${this.id}] updating`, sink, 'from', source)
this.changed.add(source as any)
sink[Sink_update] && sink[Sink_update](this)
}
fire<O extends object>(source: Source<O>) {
console.log(`[#${this.id}] firing`, source)
this.changed.add(source as any)
const outputs = source[Source_outputs]
if (outputs) for (const sink of outputs) {
this.update(source, sink)
}
}
detach<T extends object>(sink: Sink<T>, source?: Source<T>) {
if (!source) {
for (const src of (sink[Sink_inputs] || [])) {
this.detach(sink, src)
}
return
}
const outputs = source && source[Source_outputs]
if (outputs) {
outputs.delete(sink)
if (!outputs.size) {
this.detach(source as any)
source[Source_didClose] && source[Source_didClose](this)
this.closedSources.add(source)
}
}
const inputs = sink[Sink_inputs]
if (inputs) {
inputs.delete(source)
if (!inputs.size) {
sink[Sink_didClose] && sink[Sink_didClose](this)
this.closedSinks.add(sink)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment