diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml index f8a0c9a5..e549b693 100644 --- a/.github/workflows/perf.yml +++ b/.github/workflows/perf.yml @@ -5,8 +5,9 @@ on: push: branches: - master + - check-signals pull_request: - branches: [master] + branches: [master, check-signals] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} diff --git a/src/signal-polyfill.ts b/src/signal-polyfill.ts new file mode 100644 index 00000000..536867d8 --- /dev/null +++ b/src/signal-polyfill.ts @@ -0,0 +1,116 @@ +//gist.github.com/lifeart/b6fc9ec2e111a12bb78a0558ef5afa11 + +// one of possible signals implementations + +let USED_SIGNALS: Set<$Signal> | null = null; +let inUntrackCall = false; +const RELATED_WATCHERS: WeakMap> = new WeakMap(); +const COMPUTED_SIGNALS: WeakMap<$Signal, Set> = new WeakMap(); + +class $Signal { + value: any; + constructor(value: any) { + this.value = value; + } + get() { + USED_SIGNALS?.add(this); + return this.value; + } + set(value: any) { + this.value = value; + const Watchers: Set = new Set(); + COMPUTED_SIGNALS.get(this)?.forEach((computed) => { + computed.isValid = false; + for (const watcher of RELATED_WATCHERS.get(computed)!) { + Watchers.add(watcher); + } + }); + Watchers.forEach((watcher) => { + watcher.callback(); + }); + } +} + +function untrack(fn = () => {}) { + inUntrackCall = true; + try { + fn(); + } catch(e) { + // EOL + } finally { + inUntrackCall = false; + } +}; + +class Computed { + fn: Function; + isValid = false; + result: any; + constructor(fn = () => {}) { + this.fn = fn; + } + get() { + if (this.isValid) { + return this.result; + } + const oldSignals = USED_SIGNALS; + USED_SIGNALS = inUntrackCall ? USED_SIGNALS : new Set(); + try { + this.result = this.fn(); + this.isValid = true; + return this.result; + } finally { + if (!inUntrackCall) { + USED_SIGNALS?.forEach((signal) => { + if (!COMPUTED_SIGNALS.has(signal)) { + COMPUTED_SIGNALS.set(signal, new Set()); + } + COMPUTED_SIGNALS.get(signal)!.add(this); + }); + USED_SIGNALS = oldSignals; + } + } + } +} +class Watcher { + constructor(callback: Function) { + this.callback = callback; + } + watched: Set = new Set(); + callback: Function; + isWatching = true; + watch(computed?: Computed) { + if (!computed) { + this.isWatching = true; + return; + } + if (!RELATED_WATCHERS.has(computed)) { + RELATED_WATCHERS.set(computed, new Set()); + } + RELATED_WATCHERS.get(computed)!.add(this); + this.watched.add(computed); + } + unwatch(computed: Computed) { + this.watched.delete(computed); + RELATED_WATCHERS.get(computed)!.delete(this); + } + getPending() { + if (!this.isWatching) { + return []; + } + try { + return Array.from(this.watched).filter((computed) => !computed.isValid); + } finally { + this.isWatching = false; + } + } +} + +export const Signal = { + State: $Signal, + Computed, + untrack, + subtle: { + Watcher, + }, +}; diff --git a/src/utils/dom.ts b/src/utils/dom.ts index 2661636d..d745de4c 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -517,7 +517,6 @@ export function $_inElement( } else if (isTagLike(elementRef)) { appendRef = elementRef.value; } else { - // @ts-expect-error appendRef = elementRef; } const destructors: Destructors = []; diff --git a/src/utils/glimmer-validator.ts b/src/utils/glimmer-validator.ts index e1c64b14..ac1f6a02 100644 --- a/src/utils/glimmer-validator.ts +++ b/src/utils/glimmer-validator.ts @@ -41,7 +41,6 @@ export function trackedData( let hasInitializer = typeof initializer === 'function'; function getter(self: T) { - // @ts-expect-error consumeTag(cellFor(self, key)); let value; diff --git a/src/utils/if.ts b/src/utils/if.ts index ab38b0d0..1c7a07af 100644 --- a/src/utils/if.ts +++ b/src/utils/if.ts @@ -83,7 +83,6 @@ export function ifCondition( } }, runExistingDestructors, - // @ts-expect-error opcodeFor(cell, (value) => { if (throwedError) { Promise.resolve().then(() => { diff --git a/src/utils/list.ts b/src/utils/list.ts index 04847e8e..6934c41e 100644 --- a/src/utils/list.ts +++ b/src/utils/list.ts @@ -110,7 +110,6 @@ export class BasicListComponent { if (!isTagLike(tag)) { if (isArray(tag)) { console.warn('iterator for @each should be a cell'); - // @ts-expect-error tag = new Cell(tag, 'list tag'); } else if (isFn(originalTag)) { tag = formula(() => deepFnValue(originalTag), 'list tag'); @@ -298,7 +297,6 @@ export class SyncListComponent< constructor(params: ListComponentArgs, outlet: RenderTarget) { super(params, outlet); associateDestroyable(params.ctx, [ - // @ts-expect-error opcodeFor(this.tag, (value) => { this.syncList(value as T[]); }), @@ -333,7 +331,6 @@ export class AsyncListComponent< constructor(params: ListComponentArgs, outlet: RenderTarget) { super(params, outlet); associateDestroyable(params.ctx, [ - // @ts-expect-error opcodeFor(this.tag, async (value) => { await this.syncList(value as T[]); }), diff --git a/src/utils/reactive.ts b/src/utils/reactive.ts index 13c7e8c8..0e9d243c 100644 --- a/src/utils/reactive.ts +++ b/src/utils/reactive.ts @@ -4,7 +4,7 @@ We explicitly update DOM only when it's needed and only if tags are changed. */ import { isFn, isTag, isTagLike, debugContext } from '@/utils/shared'; -import { Signal } from "signal-polyfill"; +import { Signal } from "../signal-polyfill"; export const asyncOpcodes = new WeakSet(); @@ -104,7 +104,7 @@ export function setIsRendering(value: boolean) { // "data" cell, it's value can be updated, and it's used to create derived cells export class Cell { - _value!: Signal.State; + _value!: any; declare toHTML: () => string; [Symbol.toPrimitive]() { return this.value; @@ -115,7 +115,6 @@ export class Cell { this._value = new Signal.State(value); if (IS_DEV_MODE) { this._debugName = debugContext(debugName); - // @ts-expect-error DEBUG_CELLS.add(this); } } @@ -202,7 +201,6 @@ export function cellFor( obj[key], `${obj.constructor.name}.${String(key)}`, ); - // @ts-expect-error refs.set(key, cellValue); cellsMap.set(obj, refs); Object.defineProperty(obj, key, { diff --git a/src/utils/signals.ts b/src/utils/signals.ts index 013a14da..64a89e33 100644 --- a/src/utils/signals.ts +++ b/src/utils/signals.ts @@ -1,4 +1,4 @@ -import { Signal } from "signal-polyfill"; +import { Signal } from "../signal-polyfill"; import { isRehydrationScheduled } from "./rehydration"; import { setIsRendering } from "./reactive"; diff --git a/src/utils/vm.ts b/src/utils/vm.ts index a5d391ce..29703408 100644 --- a/src/utils/vm.ts +++ b/src/utils/vm.ts @@ -8,7 +8,7 @@ import { inNewTrackingFrame, } from './reactive'; import { isFn } from './shared'; -import { Signal } from "signal-polyfill"; +import { Signal } from "../signal-polyfill"; import { w } from './signals'; type maybeDestructor = undefined | (() => void);