{"id":17452372,"url":"https://github.com/omar-azmi/tsignal_ts","last_synced_at":"2026-02-17T15:03:01.068Z","repository":{"id":198297438,"uuid":"695305091","full_name":"omar-azmi/tsignal_ts","owner":"omar-azmi","description":"A topological order respecting signals library inspired by SolidJS. What's topological ordering you ask? Check out the readme to understand the problem with most signal libraries.","archived":false,"fork":false,"pushed_at":"2024-07-05T07:40:03.000Z","size":702,"stargazers_count":1,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-09-22T15:23:29.900Z","etag":null,"topics":["dag","data-flow","deno","dependency-free","effects","es6","events","modular","reactive","reactive-programming","reactivity","signals","solidjs","state-management","tiny","topological-sort","typescript"],"latest_commit_sha":null,"homepage":"https://omar-azmi.github.io/tsignal_ts/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/omar-azmi.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"license.md","code_of_conduct":".github/code_of_conduct.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-09-22T20:19:06.000Z","updated_at":"2024-07-05T07:40:08.000Z","dependencies_parsed_at":"2023-10-05T07:33:39.587Z","dependency_job_id":"e7e43fcc-235a-4724-bdb9-471104927c34","html_url":"https://github.com/omar-azmi/tsignal_ts","commit_stats":null,"previous_names":["omar-azmi/tsignal_ts"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/omar-azmi/tsignal_ts","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omar-azmi%2Ftsignal_ts","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omar-azmi%2Ftsignal_ts/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omar-azmi%2Ftsignal_ts/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omar-azmi%2Ftsignal_ts/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/omar-azmi","download_url":"https://codeload.github.com/omar-azmi/tsignal_ts/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omar-azmi%2Ftsignal_ts/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279001118,"owners_count":26083022,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-09T02:00:07.460Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dag","data-flow","deno","dependency-free","effects","es6","events","modular","reactive","reactive-programming","reactivity","signals","solidjs","state-management","tiny","topological-sort","typescript"],"created_at":"2024-10-17T23:06:18.222Z","updated_at":"2025-10-09T09:42:53.983Z","avatar_url":"https://github.com/omar-azmi.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TSignal\nA topological order respecting signals library inspired by [SolidJS](https://www.solidjs.com/).\nWhat's topological ordering you ask? Check out the readme to understand the problem with most signals libraries. \u003cbr\u003e\nWait, this is the readme file... _uhmm_ \u003cbr\u003e\n\n## Non-mandatory examples:\n### Build an SVG clock\n```tsx\n// the example is also available at \"https://github.com/omar-azmi/tsignal_ts/blob/main/examples/3/index.tsx\"\n/** @jsx h */\n/** @jsxFrag hf */\n\nimport { createHyperScript } from \"jsr:@oazmi/tsignal/jsx-runtime\"\nimport { Context, MemoSignal_Factory, StateSignal_Factory } from \"jsr:@oazmi/tsignal\"\n\nconst\n\tctx = new Context(),\n\tcreateState = ctx.addClass(StateSignal_Factory),\n\tcreateMemo = ctx.addClass(MemoSignal_Factory)\n\n/** in the esbuild build options (`BuildOptions`), you must set `jsxFactory = \"h\"` and `jsxFragment = \"hf\"` */\nexport const [h, hf, namespaceStack] = createHyperScript(ctx)\n\ntype TimeState = {\n\thour: number\n\tminute: number\n\tsecond: number\n}\n\nconst App = () =\u003e {\n\tconst\n\t\tnow_time = new Date(),\n\t\ttoday_midnight_epochtime = new Date(now_time.getFullYear(), now_time.getMonth(), now_time.getDate(), 0, 0, 0).getTime(),\n\t\t[, getEpochTime, setEpochTime] = createState\u003cnumber\u003e(0),\n\t\t[, seconds_since_midnight] = createMemo((id) =\u003e {\n\t\t\treturn ((getEpochTime(id) - today_midnight_epochtime) / 1000) | 0\n\t\t})\n\tconst [, currentTime] = createMemo\u003cTimeState\u003e((id) =\u003e {\n\t\tconst s = seconds_since_midnight(id)\n\t\treturn {\n\t\t\tsecond: s % (60),\n\t\t\tminute: ((s / 60) % 60) | 0,\n\t\t\thour: ((s / (60 * 60)) % 12) | 0,\n\t\t}\n\t}, { equals: false })\n\n\n\tsetInterval(() =\u003e setEpochTime(Date.now()), 500)\n\n\t// we must change the namespace to `svg`, so that hypescript picks up on it, and handles the newly created svg nodes appropriately.\n\tnamespaceStack.push(\"svg\")\n\tconst svg_dom = \u003csvg style=\"user-select: none;\" width=\"200px\" height=\"200px\" viewbox=\"0 0 200 200\"\u003e\n\t\t\u003cg transform=\"translate(100, 100)\"\u003e\n\t\t\t\u003ccircle r=\"100\" fill=\"lightgrey\" stroke=\"black\" /\u003e\n\t\t\t\u003ctext text-anchor=\"middle\" y=\"-25\"\u003eApple Watch XVII\u003c/text\u003e\n\t\t\t\u003cline transform={createMemo((id) =\u003e `rotate(${currentTime(id).second * 6})`)[1]} class=\"hand-seconds\" y1=\"0\" y2=\"-100\" stroke=\"red\" stroke-width={4} /\u003e\n\t\t\t\u003cline transform={createMemo((id) =\u003e `rotate(${currentTime(id).minute * 6})`)[1]} class=\"hand-minutes\" y1=\"0\" y2=\"-100\" stroke=\"green\" stroke-width={4} /\u003e\n\t\t\t\u003cline transform={createMemo((id) =\u003e `rotate(${currentTime(id).hour * 5 * 6})`)[1]} class=\"hand-hours\" y1=\"0\" y2=\"-100\" stroke=\"blue\" stroke-width={4} /\u003e\n\t\t\u003c/g\u003e\n\t\u003c/svg\u003e\n\t// declare that the svg namespace is now over, and switch back to html namespace\n\tnamespaceStack.pop()\n\treturn svg_dom\n}\n\ndocument.body.append(App())\n```\n\n### Reactively compute bounding-boxes of many nested rectangles\n```ts\n// reactively compute the bounding-boxes of many nested rectangles.\n// the computation should be lazy: if the bounding-box of a child-rectangle hasn't changed, then it shouldn't invoke an update in its parent-rectangle.\n// if the top-most rectangle's `right` or `top` bounding-box's sides exceed `600`, then we should log `\"overflow\"` in the console.\n// at the end of every reaction cycle, log the number of computations done in the console.\n\nimport { Context } from \"jsr:@oazmi/tsignal/context\"\nimport { StateSignal_Factory, MemoSignal_Factory, EffectSignal_Factory } from \"jsr:@oazmi/tsignal/signal\"\nimport type { Accessor, Setter } from \"jsr:@oazmi/tsignal/typedefs\"\n\n/** `x` and `y` are relative to the parent-rectangle's top-left corner (which is their (x, y) position). */\ninterface Rect { x: number, y: number, width: number, height: number }\n\n/** the bounding box of a `Rect` */\ninterface Box { left: number, top: number, right: number, bottom: number }\n\nconst signal_ctx = new Context()\nconst createState = signal_ctx.addClass(StateSignal_Factory)\nconst createMemo = signal_ctx.addClass(MemoSignal_Factory)\nconst createEffect = signal_ctx.addClass(EffectSignal_Factory)\nconst rectEqualityFn = (rect1: Rect | undefined, rect2: Rect): boolean =\u003e {\n\treturn rect1 === undefined ? false :\n\t\trect1.x === rect2.x \u0026\u0026\n\t\trect1.y === rect2.y \u0026\u0026\n\t\trect1.width === rect2.width \u0026\u0026\n\t\trect1.height === rect2.height\n}\nconst boxEqualityFn = (box1: Box | undefined, box2: Box): boolean =\u003e {\n\treturn box1 === undefined ? false :\n\t\tbox1.top === box2.top \u0026\u0026\n\t\tbox1.left === box2.left \u0026\u0026\n\t\tbox1.bottom === box2.bottom \u0026\u0026\n\t\tbox1.right === box2.right\n}\n\ntype RectangleObject = Rect \u0026 { children?: RectangleObject[] }\n\nclass Rectangle {\n\trect: Accessor\u003cRect\u003e\n\tsetRect: Setter\u003cRect\u003e\n\tbox: Accessor\u003cBox\u003e\n\tchildren: Rectangle[] = []\n\n\tconstructor(initial_rect: Rect) {\n\t\tconst [idRect, getRect, setRect] = createState(initial_rect, { equals: rectEqualityFn })\n\t\tconst [idBox, getBox] = createMemo\u003cBox\u003e((id) =\u003e {\n\t\t\tconst { x, y, width, height } = getRect(id)\n\t\t\tconst children_boxes = this.children.map((child) =\u003e child.box(idBox))\n\t\t\tconst\n\t\t\t\ttop = Math.min(y, ...children_boxes.map((child_box) =\u003e child_box.top)),\n\t\t\t\tleft = Math.min(x, ...children_boxes.map((child_box) =\u003e child_box.left)),\n\t\t\t\tbottom = Math.max(y + height, ...children_boxes.map((child_box) =\u003e y + child_box.bottom)),\n\t\t\t\tright = Math.max(x + width, ...children_boxes.map((child_box) =\u003e x + child_box.right))\n\t\t\treturn { top, left, bottom, right }\n\t\t}, { equals: boxEqualityFn })\n\n\t\tthis.rect = getRect\n\t\tthis.setRect = setRect\n\t\tthis.box = getBox\n\t}\n\n\trender(ctx: CanvasRenderingContext2D) {\n\t\tconst { x, y, width, height } = this.rect()\n\t\tconst { top, left, bottom, right } = this.box()\n\t\tconst children = this.children\n\t\tctx.fillStyle = get_next_fillcolor()\n\t\tctx.strokeStyle = get_next_strokecolor()\n\t\tctx.fillRect(x, y, width, height)\n\t\tctx.strokeRect(left, top, right - left, bottom - top)\n\t\tctx.translate(x, y)\n\t\tfor (const child of children) {\n\t\t\tchild.render(ctx)\n\t\t}\n\t\tctx.translate(-x, -y)\n\t}\n\n\tstatic fromObject(object: RectangleObject): Rectangle {\n\t\tconst { children, ...rect } = object\n\t\tconst instance = new Rectangle(rect)\n\t\tconst children_instances = children?.map(Rectangle.fromObject) ?? []\n\t\tinstance.children.push(...children_instances)\n\t\treturn instance\n\t}\n}\n\nlet color_idx = 0\nconst fillcolors = [\"blue\", \"cyan\", \"goldenrod\", \"gray\", \"green\", \"khaki\", \"magenta\", \"orange\", \"orchid\", \"red\", \"salmon\", \"seagreen\", \"turquoise\", \"violet\",]\nconst strokecolors = fillcolors.map((color) =\u003e \"dark\" + color)\nconst get_next_fillcolor = () =\u003e { return fillcolors[color_idx++ % fillcolors.length] }\nconst get_next_strokecolor = () =\u003e { return strokecolors[color_idx++ % strokecolors.length] }\n\nconst all_rectangles = Rectangle.fromObject({\n\tx: 10, y: 20, width: 70, height: 80, children: [\n\t\t{\n\t\t\tx: 15, y: 25, width: 50, height: 60, children: [\n\t\t\t\t{\n\t\t\t\t\tx: 20, y: 30, width: 40, height: 50, children: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tx: 25, y: 35, width: 30, height: 40, children: [\n\t\t\t\t\t\t\t\t{ x: 30, y: 40, width: 20, height: 30 },\n\t\t\t\t\t\t\t\t{ x: 35, y: 45, width: 10, height: 20 }\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{ x: 40, y: 50, width: 10, height: 20 }\n\t\t\t\t\t]\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tx: 45, y: 55, width: 10, height: 15, children: [\n\t\t\t\t\t\t{ x: 0, y: 40, width: 30, height: 50 },\n\t\t\t\t\t\t{ x: 70, y: 80, width: 50, height: 20 }\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{ x: 50, y: 60, width: 40, height: 30 }\n\t]\n})\n\nconst canvas = document.createElement(\"canvas\")\nconst ctx = canvas.getContext(\"2d\")!\ndocument.body.appendChild(canvas)\n\nconst [idRedraw, awaitRedraw, fireRedraw] = createEffect((id) =\u003e {\n\t// add dependence on all rectangles\n\tconst add_dependence = (rect_object: Rectangle) =\u003e {\n\t\trect_object.rect(id)\n\t\trect_object.box(id)\n\t\trect_object.children.forEach(add_dependence)\n\t}\n\tadd_dependence(all_rectangles)\n\n\tcolor_idx = 0\n\tctx.reset()\n\tctx.scale(2, 2)\n\tall_rectangles.render(ctx)\n})\n\nfireRedraw()\n\n// try the following lines in your console (one by one) to witness reactivity:\n// all_rectangles.children[0].children[0].children[0].setRect({x:10, y:10, width: 20, height: 50})\n// all_rectangles.children[0].children[1].setRect({x:50, y:50, width: 100, height: 50})\n```\n\n## Signal Classes\n\nhere is a list of all signal classes that are currently available:\n- [`StateSignal`](./src/signal.ts#L108)\n- [`MemoSignal`](./src/signal.ts#L146)\n- [`LazySignal`](./src/signal.ts#L187)\n- [`EffectSignal`](./src/signal.ts#L241)\n- [`RecordSignal`](./src/record_signal.ts#L44)\n- [`RecordStateSignal`](./src/record_signal.ts#L132)\n- [`MemoRecordSignal`](./src/record_signal.ts#L171)\n\n\n## Theory\n\n### What is Topological Ordering\n\na signal-based reactivity system can be thought of as a **DAG** (directed acyclic graph) - which is something that resembles a dependency graph. \u003cbr\u003e\nin a DAG, each signal object is a *node/vertex* of the graph, and each directed-relation (or dependency) is described as an *edge* of the graph.\n\nwe use the following notation to describe one relation in our graph:\n\n\u003e SourceSignal -\u003e [DestinationSignal_1, DestinationSignal_2, DestinationSignal_3, ...]\n\nwhich is basically saying that: updating `SourceSignal` should result in an update in `DestinationSignal_1`, `DestinationSignal_2`, etc... . \u003cbr\u003e\nor another way of reading it is: each of `DestinationSignal_1`, `DestinationSignal_2`, and etc... depend on `SourceSignal` (i.e. the destination signals are observers of `SourceSignal`).\n\na DAG graph can then be described as a series/array of relations:\n```txt\nMyGraph = [\n\tSignal_A -\u003e [Signal_B, Signal_C],\n\tSignal_B -\u003e [Signal_D, Signal_F],\n\tSignal_C -\u003e [Signal_E, Signal_F, Signal_H],\n\t...\n]\n```\n\na topologically sorted array of the nodes of a DAG is basically an array of the nodes, where every dependency node appears before the node which depends on it. \u003cbr\u003e\nso, for example the following DAG graph:\n\n```mermaid\n---\ntitle: \"graph\"\n---\nflowchart LR\n\t332796((\"A\")) --\u003e 590207((\"B\"))\n\t332796 --\u003e 763885((\"C\"))\n\t590207 --\u003e 562158((\"D\"))\n\t763885 --\u003e 932932((\"E\"))\n\t763885 --\u003e 940461((\"H\"))\n\t562158 --\u003e 831043((\"G\"))\n\t831043 --\u003e 450102((\"I\"))\n\t932932 --\u003e 940461\n\t940461 --\u003e 450102\n\t590207 --\u003e 270457((\"F\"))\n\t562158 --\u003e 270457\n\t763885 --\u003e 270457\n\t932932 --\u003e 270457\n\t270457 --\u003e 450102\n\t450102 --\u003e 770803((\"J\"))\n```\n\n```txt\ngraph = [ A-\u003e[B, C] , B-\u003e[D, F] , C-\u003e[E, F, H] , D-\u003e[F, G] , E-\u003e[F, H] , F-\u003e[I] , G-\u003e[I] , H-\u003e[I] , I-\u003e[J] ]\n```\n\nhas the following (non-unique) topologically sorted array:\n```txt\nnodes_sorted = [ A, B, D, G, C, E, H, F, I, J ]\n```\n\n\n### Update State of each Signal\n\nin this library, during an update cycle, when a signal is executed to update (through its [`run method`](./src/typedefs.ts#L136) in {@link typedefs!Signal.run}), it returns the numeric enum [`SignalUpdateStatus`](./src/typedefs.ts#L176), which conveys a specific instruction to the `Context`'s update loop:\n- ` 1` or `SignalUpdateStatus.UPDATED`: this signal's value has been updated, and therefore its observers should be updated too.\n- ` 0` or `SignalUpdateStatus.UNCHANGED`: this signal's value has not changed, and therefore its observers should be _not_ be updated by this signal.\ndo note that an observer signal will still run if some _other_ of its dependency signal did update this cycle (i.e. had a status value of `1`).\n- `-1` or `SignalUpdateStatus.ABORTED`: this signal has been aborted, and therefore its observers must abort execution as well.\nthe observers will abort _even_ if they had a dependency that _did_ update (had a status value of `1`).\nshould the aborted observer signal also abort its own observers? that's a thing that is open to debate.\ncurrently, it does not abort its own observers.\n\n\n### The Topological Update Cycle Algorithm\n\n#### Algorithm:\n\ngiven an array of `source_ids` to initiate the signal from (simultaneously),\nthe `Context`'s [update cycle](./src/context.ts#L148) ({@link context!Context.fireUpdateCycle}) works by following the steps below:\n- starting with the `source_ids`, sort the DAG graph into a topologically ordered array `topological_ids` (via [DFS](https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search)), where:\n  - `source_ids` are **always** at the beginning of the `topological_ids` array.\n- create an empty set of signal ids called `not_to_visit`, which will contain the signals whose dependencies (at least one) have declared an aborted status (`SignalUpdateStatus.ABORTED`).\n- create an empty set of signal ids called `next_to_visit`, which will contain the signals that are awaiting/in-queue to be executed next.\n- fill the set `next_to_visit` with our initial `source_ids`\n- now, in an ordered loop, for each `id` inside of `topological_ids` (starting with the source ids), do:\n  - check that `id` exists in `next_to_visit` AND `id` does not exist inside of `not_to_visit`. if true, then:\n    - delete `id` from `next_to_visit`\n    - run the signal associated with the `id`, and save its execution's returned `SignalUpdateStatus` to `status`.\n    - is `status == SignalUpdateStatus.UPDATED` (i.e. `status == 1`)? if true, then:\n      - for every `observer_id` of this signal's `id`, add `observer_id` to `next_to_visit`\n    - is `status == SignalUpdateStatus.ABORTED` (i.e. `status == -1`)? if true, then:\n      - for every `observer_id` of this signal's `id`, add `observer_id` to `not_to_visit`\n  - is `next_to_visit` set now empty? if true, then:\n    - terminate/break the loop early, since there is no way any more ids will ever be added to `next_to_visit`\n\n\n#### Example:\n\nstarting with the following `graph`, and its `topological_ids` sorted array, and the initial `source_ids`:\n```txt\ngraph = [ A-\u003e[B, C] , B-\u003e[D, F] , C-\u003e[E, F, H] , D-\u003e[F, G] , E-\u003e[F, H] , F-\u003e[I] , G-\u003e[I] , H-\u003e[I] , I-\u003e[J] ]\ntopological_ids = [ A, B, D, G, C, E, H, F, I, J ]\nsource_ids = [ A, ]\n```\nand assuming that signal `B` does not change when executed (i.e. `status = SignalUpdateStatus.UNCHANGED`), we get the following update cycle:\n- assign `not_to_visit = { }`\n- assign `next_to_visit = {A}`\n- `id` = A:\n  - `next_to_visit == {}`\n  - `status = run(A)` == 1\n    - add each of `graph[A]` = [B, C] to `next_to_visit`\n- `next_to_visit == {B, C}` and is not empty\n- `id` = B:\n  - `next_to_visit == {C}`\n  - `status = run(B)` == 0\n- `next_to_visit == {C}` and is not empty\n- `id` = D: skipped because it is not in `next_to_visit`\n- `next_to_visit == {C}` and is not empty\n- `id` = G: skipped because it is not in `next_to_visit`\n- `next_to_visit == {C}` and is not empty\n- `id` = C:\n  - `next_to_visit == {}`\n  - `status = run(C)` == 1\n    - add each of `graph[C]` = [E, F, H] to `next_to_visit`\n- `next_to_visit == {E, F, H}` and is not empty\n- `id` = E:\n  - `next_to_visit == {F, H}`\n  - `status = run(E)` == 1\n    - add each of `graph[E]` = [F, H] to `next_to_visit`\n- `next_to_visit == {F, H}` and is not empty\n- `id` = H:\n  - `next_to_visit == {F}`\n  - `status = run(H)` == 1\n    - add each of `graph[H]` = [I] to `next_to_visit`\n- `next_to_visit == {F, I}` and is not empty\n- `id` = F:\n  - `next_to_visit == {I}`\n  - `status = run(F)` == 1\n    - add each of `graph[F]` = [I] to `next_to_visit`\n- `next_to_visit == {I}` and is not empty\n- `id` = I:\n  - `next_to_visit == {}`\n  - `status = run(I)` == 1\n    - add each of `graph[I]` = [J] to `next_to_visit`\n- `next_to_visit == {J}` and is not empty\n- `id` = I:\n  - `next_to_visit == {}`\n  - `status = run(I)` == 1\n    - add each of `graph[J]` = [ ] to `next_to_visit`\n- `next_to_visit == { }` and **is** empty, so terminate\n\n\n### Caching Mechanism\n\nrecomputing the topologically ordered signal ids at the beginning of every update cycle is wasteful,\nso instead, we memorize the result of a topological ordering when certain the update is initiated from a certain `source_ids`. \u003cbr\u003e\nin order to memorize the result, we first hash the array `source_ids` to a `number` that is invariant to the positional ordering of the ids inside of `source_ids`. \u003cbr\u003e\nthe hashing function is defined in [`hash_ids`](./src/funcdefs.ts#L46) ({@link funcdefs!hash_ids}),\nand the memorization/caching function is defined in [`get_ids_to_visit`](./src/context.ts#L96) ({@link context!get_ids_to_visit}).\n\nthe cache is only valid if no mutations to the DAG graph (addition or deletion of nodes or edges) have been done. \u003cbr\u003e\nthat's why we clear the cache whenever a mutative action is taken within the `Context`'s graph, such as:\n- introducing a new signal\n- a signal declares a new dependency or observer\n- deleting a signal\n\n\n### Observation Detection Mechanism\n\nTODO: explain the difference between a signal's `id` and its `rid`. then explain how non-zero `rid` are an idication for a new observation. \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fomar-azmi%2Ftsignal_ts","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fomar-azmi%2Ftsignal_ts","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fomar-azmi%2Ftsignal_ts/lists"}