Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
IT NachrichtenPeloton has made a foldable (treadmill)(22.09.2026 um 13:00 Uhr)
IT NachrichtenPeloton has made a foldable (treadmill)(22.09.2026 um 13:00 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Building a Minimal Signal API

Introduction This post continues the idea from the end of the previous article: using closures + destructuring assignment to implement a tiny state “storage” mechanism. Here’s the starting point: export type Signal<T> = { get(…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!




Introduction



This post continues the idea from the end of the previous article: using closures + destructuring assignment to implement a tiny state “storage” mechanism.



Here’s the starting point:




export type Signal<T> = {
get(): T;
set(next: T | ((prev: T) => T)): void;
};

export function signal<T>(initial: T): Signal<T> {
let value = initial;

const get = () => value;

const set = (next: T | ((p: T) => T)) => {
const nxtVal = typeof next === "function" ? (next as (p: T) => T)(value) : next;
const isEqual = Object.is(value, nxtVal);
if (!isEqual) value = nxtVal;
};

return { get, set };
}






Remember this diagram?

signal structure



We’re still missing one crucial piece: a place to store dependencies—namely, the Observers. That’s the last puzzle piece needed to make a Signal “reactive”.







Tracking vs Observing



Let’s clarify terminology first:





  • Tracking / Trackable : the “source” of information — the thing that can be tracked / subscribed to.
    The most obvious example: a Signal.


  • Observing / Observer: the “subscriber” — the thing that reacts to changes.
    The most obvious example: an Effect.



With this simple split, we can summarize:





Trackable



  • A subscribable source (e.g. signal, computed)

  • Internally maintains: subs: Set<Observer> (who is subscribing to me)





Observer



  • An observer (e.g. computed, effect)

  • Internally maintains: deps: Set<Trackable> (who I depend on)



Diagram:

Role structure



From the source perspective:

source view



From the observer perspective:

observer view



This forms a bidirectional graph:




  • Sources know their subscribers

  • Subscribers know their sources



Since the rest of the series will optimize around these structures, it helps to be comfortable with basic graph concepts.







TypeScript Types and the Core Model



We can simplify the concept into types like this:




export interface Trackable {
subs: Set<Observer>;
}

export interface Observer {
deps: Set<Trackable>;
}












Minimal Dependency Tracking Core



We’ll implement dependency tracking with currentObserver + track.




let currentObserver: Observer | null = null;

export function withObserver<T>(obs: Observer, fn: () => T): T {
const prev = currentObserver;
currentObserver = obs;
try {
return fn();
} finally {
currentObserver = prev;
}
}

export function track(dep: Trackable) {
if (!currentObserver) return;
dep.subs.add(currentObserver);
currentObserver.deps.add(dep);
}






Key idea:




Any read happening inside the tracking window creates an edge: who read whom.




At this stage, we only build edges.

We do not notify anyone yet. Notification / invalidation (dirtying) will be handled in the next article.







Combine It with the Basic Closure-Based signal



Now we merge the tracking mechanism into the original closure-based Signal and get a minimal subscribable implementation.



We’ll introduce a unified Node model for signal / computed / effect:




type Kind = "signal" | "computed" | "effect";

export interface Node {
kind: Kind;
deps: Set<Node>; // who I depend on (used by computed/effect)
subs: Set<Node>; // who depends on me (signal/computed can be subscribed)
}

// Invariant: signal cannot have deps; effect doesn't expose subs
export function link(from: Node, to: Node) {
if (from.kind === "signal") {
throw new Error("Signal nodes cannot depend on others");
}
from.deps.add(to);
to.subs.add(from);
}

export function unlink(from: Node, to: Node) {
from.deps.delete(to);
to.subs.delete(from);
}

// Tracking tool: while inside an "observer context", reads auto-create edges
// (build graph only, no notification yet)
let currentObserver: Node | null = null;

export function withObserver<T>(obs: Node, fn: () => T): T {
const prev = currentObserver;
currentObserver = obs;
try {
return fn();
} finally {
currentObserver = prev;
}
}

function track(dep: Node) {
if (!currentObserver) return; // normal read outside tracking
link(currentObserver, dep); // Observer -> Trackable
}

// Object return is destructuring-friendly.
// Extract Object.is into equals; next article will use it for notification decisions.
type Comparator<T> = (a: T, b: T) => boolean;
const defaultEquals = Object.is;

export function signal<T>(initial: T, equals: Comparator<T> = defaultEquals) {
// Single node + private value
const node: Node & { kind: "signal"; value: T; equals: Comparator<T> } = {
kind: "signal",
deps: new Set(), // always empty (enforced by link())
subs: new Set(),
value: initial,
equals,
};

const get = () => {
track(node);
return node.value;
};

const set = (next: T | ((prev: T) => T)) => {
const nxtVal = typeof next === "function" ? (next as (p: T) => T)(node.value) : next;
if (node.equals(node.value, nxtVal)) return;
node.value = nxtVal;
// This post only covers subscription graph building.
// No dirtying / notification yet — next article continues from here.
};

// Imperative subscription (for contrast with declarative tracking)
// Returns an unsubscribe function.
const subscribe = (observer: Node) => {
if (observer.kind === "signal") {
throw new Error("A signal cannot subscribe to another node");
}
link(observer, node);
return () => unlink(observer, node);
};

return { get, set, subscribe, peek: () => node.value };
}












Why Provide Both track and subscribe?



Because they serve different purposes:




  • track() is declarative dependency tracking: inside a tracking block, whatever you read gets subscribed automatically.

    This is what computed / effect will use.


  • subscribe() is imperative subscription: you can manually attach an Observer to a signal.

    This resembles traditional event subscription and is useful for interoperability / bridging to other systems.



  • peek() is a practical escape hatch:




    • convenient for tests

    • useful when integrating with external frameworks without creating dependencies














Event Subscription vs Dependency Tracking

































Aspect Event subscription (subscribe(cb)) Dependency tracking (track/withObserver)
Goal Call callbacks immediately when value changes Build a dataflow graph for later recomputation/scheduling
How edges are created Manual register/unregister Automatically tracked during the “read phase”
Best for I/O, logging, bridging third-party systems Collecting sources for computed/effect and propagating invalidation
Lifecycle Managed by the user Can be managed by computed/effect lifecycle automatically








Closing



At this point, we’ve completed “signal + subscription graph building”:




  • Inside a tracking block like withObserver(() => a.get()), reads automatically create dependency edges: Observer → Trackable.

  • This post only builds the graph and triggers no re-execution.



Next article is straightforward: implement effect so the graph actually “moves”.



Planned steps:




  1. Create a kind: "effect" node, and on first run use withObserver to collect dependencies.

  2. When any dependent signal.set() happens, notify the corresponding effects and batch re-runs in a microtask.

  3. Add dispose / onCleanup: before each rerun, remove old dependencies and run cleanup hooks.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Building a Minimal Signal API

Thematisch verwandte Begriffe: Building, Minimal, Signal · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-94493 | A vulnerability was detected in Gigatech PDV5701 1.0.31_240305_112640. T…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick