Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🚨 [experiment] Hash-Map based signals #153

Open
wants to merge 8 commits into
base: check-signals
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/perf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
116 changes: 116 additions & 0 deletions src/signal-polyfill.ts
Original file line number Diff line number Diff line change
@@ -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<Computed, Set<Watcher>> = new WeakMap();
const COMPUTED_SIGNALS: WeakMap<$Signal, Set<Computed>> = 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<Watcher> = 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<Computed> = 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,
},
};
1 change: 0 additions & 1 deletion src/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,6 @@ export function $_inElement(
} else if (isTagLike(elementRef)) {
appendRef = elementRef.value;
} else {
// @ts-expect-error
appendRef = elementRef;
}
const destructors: Destructors = [];
Expand Down
1 change: 0 additions & 1 deletion src/utils/glimmer-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export function trackedData<T extends object, K extends keyof T>(
let hasInitializer = typeof initializer === 'function';

function getter(self: T) {
// @ts-expect-error
consumeTag(cellFor(self, key));

let value;
Expand Down
1 change: 0 additions & 1 deletion src/utils/if.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ export function ifCondition(
}
},
runExistingDestructors,
// @ts-expect-error
opcodeFor(cell, (value) => {
if (throwedError) {
Promise.resolve().then(() => {
Expand Down
3 changes: 0 additions & 3 deletions src/utils/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ export class BasicListComponent<T extends { id: number }> {
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');
Expand Down Expand Up @@ -298,7 +297,6 @@ export class SyncListComponent<
constructor(params: ListComponentArgs<T>, outlet: RenderTarget) {
super(params, outlet);
associateDestroyable(params.ctx, [
// @ts-expect-error
opcodeFor(this.tag, (value) => {
this.syncList(value as T[]);
}),
Expand Down Expand Up @@ -333,7 +331,6 @@ export class AsyncListComponent<
constructor(params: ListComponentArgs<any>, outlet: RenderTarget) {
super(params, outlet);
associateDestroyable(params.ctx, [
// @ts-expect-error
opcodeFor(this.tag, async (value) => {
await this.syncList(value as T[]);
}),
Expand Down
6 changes: 2 additions & 4 deletions src/utils/reactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<tagOp>();
Expand Down Expand Up @@ -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<T extends unknown = unknown> {
_value!: Signal.State<T>;
_value!: any;
declare toHTML: () => string;
[Symbol.toPrimitive]() {
return this.value;
Expand All @@ -115,7 +115,6 @@ export class Cell<T extends unknown = unknown> {
this._value = new Signal.State(value);
if (IS_DEV_MODE) {
this._debugName = debugContext(debugName);
// @ts-expect-error
DEBUG_CELLS.add(this);
}
}
Expand Down Expand Up @@ -202,7 +201,6 @@ export function cellFor<T extends object, K extends keyof T>(
obj[key],
`${obj.constructor.name}.${String(key)}`,
);
// @ts-expect-error
refs.set(key, cellValue);
cellsMap.set(obj, refs);
Object.defineProperty(obj, key, {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/signals.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Signal } from "signal-polyfill";
import { Signal } from "../signal-polyfill";
import { isRehydrationScheduled } from "./rehydration";
import { setIsRendering } from "./reactive";

Expand Down
2 changes: 1 addition & 1 deletion src/utils/vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading