Skip to content

Commit

Permalink
restructure quel to have a push/pull mechanism instead
Browse files Browse the repository at this point in the history
  • Loading branch information
loreanvictor committed Jun 6, 2024
1 parent 024a3a0 commit ede750d
Show file tree
Hide file tree
Showing 42 changed files with 1,621 additions and 863 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,5 @@ dist
.tern-port

.DS_Store

**/*.bak.*
23 changes: 23 additions & 0 deletions _seite.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
section:first-child {
padding-top: 15vh;
}

@media (prefers-color-scheme: light) {
:root {
--primary-color: #22577A;
}

pre > code {
box-shadow: 0 6px 24px rgba(0 0 0 / 5%);
}
}

@media (prefers-color-scheme: dark) {
:root {
--primary-color: #80ED99;
}

pre > code {
box-shadow: 0 3px 12px rgba(0 0 0 / 50%);
}
}
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default {
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.test.{ts,tsx}',
'!src/**/*.bak.{ts,tsx}',
],
coverageThreshold: {
global: {
Expand Down
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"sleep-promise": "^9.1.0",
"streamlets": "^0.5.1",
"table": "^6.8.1",
"test-callbag-jsx": "^0.4.1",
"test-callbag-jsx": "^0.5.0",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"tslib": "^2.4.1",
Expand Down
157 changes: 157 additions & 0 deletions src/computed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { Source } from './source'
import { Signal, SignalOptions } from './signal'
import { deferred, Deferred } from './util/deferred'
import { isStoppable } from './util/stoppable'
import { AbortableOptions, checkAbort } from './util/abortable'


export const SKIP = Symbol()
export const STOP = Symbol()

export interface ExprOptions extends AbortableOptions {
initial?: boolean
}

export type ExprResultSync<T> = undefined | T | typeof SKIP | typeof STOP
export type ExprResult<T> = ExprResultSync<T> | Promise<ExprResultSync<T>>
export type ExprFn<T> = (track: Track, options: ExprOptions) => ExprResult<T>
export type SourceTrack = <T>(src: Source<T>) => T
export type ExprTrack = <T>(expr: ExprFn<T>) => T | undefined
export type Track = ExprTrack & SourceTrack
export type Trackable<T> = Source<T> | ExprFn<T>

export type ComputedOptions<T> = SignalOptions<T | undefined>

export function wrap<T>(expr: ExprFn<T>): Computed<T>
export function wrap<T>(src: Source<T>): Source<T>
export function wrap<T>(src: Trackable<T>): Source<T> | Computed<T>
export function wrap<T>(src: Trackable<T>): Source<T> | Computed<T> {
return typeof src === 'function' ?
((src as any).__computed__ ??= new Computed(src))
: src
}

export class Computed<T> extends Signal<T | undefined> {
private _changedDependencies?: Source<unknown>[]
private _initialRun?: Deferred<void>
private _initialError?: unknown
private _activeRuns = 0
private _activeDependencies = new WeakMap<Source<unknown>, boolean>()

constructor(private expr: ExprFn<T>, options?: ComputedOptions<T>) {
super({ initial: undefined, ...options })

this.run({ initial: true })
}

private track<R>(src: Source<R>, tracked: Source<unknown>[], signal?: AbortSignal): R {
if (!signal?.aborted && !src.stopped && !tracked.includes(src) && !this._activeDependencies.get(src)) {
tracked.push(src)
this._activeDependencies.set(src, true)

src.listen(() => {
(this._changedDependencies ??= []).push(src)
this._activeDependencies.set(src, false)
this.notify(this)
}, { once: true })
}

return src.last
}

override async get(options?: AbortableOptions) {
if (this._changedDependencies) {
const promises: Promise<unknown>[] = []
for (const dep of this._changedDependencies) {
const promise = dep.get()
!dep.valid && promises.push(promise)
}

this._changedDependencies = undefined
promises.length > 0 && await Promise.all(promises)

const p = this.run(options)
p instanceof Promise && await p
} else if (!this.valid) {
await (this._initialRun ??= deferred()).promise
} else if (this._initialError) {
throw this._initialError
}

return this._last
}

public async reevaluate(options?: AbortableOptions) {
const p = this.run(options)
p instanceof Promise && await p

return this._last
}

override get valid() {
return this._activeRuns === 0 && (
!this._changedDependencies || this._changedDependencies.length === 0
)
}

protected override invalidate() { this._activeRuns++ }
protected override validate() { this._activeRuns-- }

protected run(options?: ExprOptions): void | Promise<void> {
const tracked: Source<unknown>[] = []
try {
checkAbort(options?.signal)
const result = this.expr(
(<R>(t: Trackable<R>) => this.track(wrap(t), tracked, options?.signal)) as Track,
options ?? {}
)
tracked.length === 0 && this.stop()

if (result instanceof Promise) {
this.invalidate()

return result.finally(() => {
this.validate()
}).then((value) => {
checkAbort(options?.signal)
this.emit(value)
this._initialRun?.resolve()
}).catch(err => {
if (options?.initial) {
this._initialRun?.reject(err)
} else {
throw err
}
})
} else {
checkAbort(options?.signal)
this.emit(result)
}
} catch (err) {
if (options?.initial) {
this._initialError = err
} else {
throw err
}
}
}

protected override emit(val: T | typeof STOP | typeof SKIP | undefined) {
if (val === STOP) {
this.stop()
} else if (val === SKIP) {
return
} else if (!this.equals(val, this.last)) {
if (isStoppable(this.last)) {
this.last.stop()
this._activeDependencies.delete(this.last as any)
}

this._last = val
}
}
}

export function computed<T>(expr: ExprFn<T>, options?: ComputedOptions<T>): Computed<T> {
return new Computed(expr, options)
}
35 changes: 35 additions & 0 deletions src/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { dispose, isDisposable } from './util/disposable'
import { Signal, SignalOptions } from './signal'


export type Producer<T> = (
produce: (val: T) => void,
finalize: (diposable: Disposable) => void
) => Disposable | void | Promise<void>


export interface ProducedSignalOptions<T> extends SignalOptions<T> {
producer: Producer<T>
}


export class ProducedSignal<T> extends Signal<T> {
cleanup?: Disposable

constructor(options: ProducedSignalOptions<T>) {
super(options)

const cl = options.producer && options.producer(val => this.emit(val), cleanup => this.cleanup = cleanup)
cl && isDisposable(cl) && (this.cleanup = cl)
}

override clean() {
super.clean()
this.cleanup && dispose(this.cleanup)
}
}


export function create<T>(options: ProducedSignalOptions<T>) {
return new ProducedSignal(options)
}
15 changes: 0 additions & 15 deletions src/disposable.ts

This file was deleted.

24 changes: 13 additions & 11 deletions src/event.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { Source } from './source'
import { create } from './create'
import { disposable } from './util/disposable'
import { addListener, removeListener, EventMap } from './util/dom-events'


export class EventSource<EventName extends keyof EventMap> extends Source<EventMap[EventName]> {
constructor(
readonly node: EventTarget,
readonly name: EventName,
readonly options?: boolean | AddEventListenerOptions,
) {
super(emit => {
export function event<EventName extends keyof EventMap>(
node: EventTarget,
name: EventName,
options?: boolean | AddEventListenerOptions,
) {
return create<undefined | EventMap[EventName]>({
initial: undefined,
producer: emit => {
const handler = (evt: EventMap[EventName]) => emit(evt)
addListener(node, name, handler, options)

return () => removeListener(node, name, handler, options)
})
}
return disposable(() => removeListener(node, name, handler, options))
}
})
}
16 changes: 8 additions & 8 deletions src/from.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { EventMap } from './util/dom-events'
import { EventSource } from './event'
import { InputSource } from './input'
import { event } from './event'
import { input } from './input'


export function from(input: HTMLInputElement): InputSource
export function from(node: EventTarget): EventSource<'click'>
export function from(i: HTMLInputElement | HTMLTextAreaElement): ReturnType<typeof input>
export function from(node: EventTarget): ReturnType<typeof event<'click'>>
export function from<EventName extends keyof EventMap>(
node: EventTarget,
name: EventName,
options?: boolean | AddEventListenerOptions
): EventSource<EventName>
): ReturnType<typeof event<EventName>>
export function from<EventName extends keyof EventMap>(
node: EventTarget,
name?: EventName,
options?: boolean | AddEventListenerOptions,
): InputSource | EventSource<EventName> {
) {
if (!name && (node as any).tagName && (
(node as any).tagName === 'INPUT' || (node as any).tagName === 'TEXTAREA'
)) {
return new InputSource(node as HTMLInputElement)
return input(node as HTMLInputElement)
} else {
return new EventSource(node, name ?? 'click' as EventName, options)
return event(node, name ?? 'click' as EventName, options)
}
}
11 changes: 6 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
export * from './source'
export * from './noop'
export * from './create'
export * from './signal'
export * from './timer'
export * from './subject'
export * from './state'
export * from './observe'
export * from './iterate'
export * from './event'
export * from './input'
export * from './from'
export * from './types'
export * from './disposable'
export * from './source'
export * from './util/listenable'
export * from './util/disposable'

Loading

0 comments on commit ede750d

Please sign in to comment.