# TSignal
A topological order respecting signals library inspired by [SolidJS](https://www.solidjs.com/).
What's topological ordering you ask? Check out the readme to understand the problem with most signals libraries. <br>
Wait, this is the readme file... _uhmm_ <br>

## Non-mandatory examples:
### Build an SVG clock
```tsx
// the example is also available at "https://github.com/omar-azmi/tsignal_ts/blob/main/examples/3/index.tsx"
/** @jsx h */
/** @jsxFrag hf */

import { createHyperScript } from "jsr:@oazmi/tsignal/jsx-runtime"
import { Context, MemoSignal_Factory, StateSignal_Factory } from "jsr:@oazmi/tsignal"

const
	ctx = new Context(),
	createState = ctx.addClass(StateSignal_Factory),
	createMemo = ctx.addClass(MemoSignal_Factory)

/** in the esbuild build options (`BuildOptions`), you must set `jsxFactory = "h"` and `jsxFragment = "hf"` */
export const [h, hf, namespaceStack] = createHyperScript(ctx)

type TimeState = {
	hour: number
	minute: number
	second: number
}

const App = () => {
	const
		now_time = new Date(),
		today_midnight_epochtime = new Date(now_time.getFullYear(), now_time.getMonth(), now_time.getDate(), 0, 0, 0).getTime(),
		[, getEpochTime, setEpochTime] = createState<number>(0),
		[, seconds_since_midnight] = createMemo((id) => {
			return ((getEpochTime(id) - today_midnight_epochtime) / 1000) | 0
		})
	const [, currentTime] = createMemo<TimeState>((id) => {
		const s = seconds_since_midnight(id)
		return {
			second: s % (60),
			minute: ((s / 60) % 60) | 0,
			hour: ((s / (60 * 60)) % 12) | 0,
		}
	}, { equals: false })


	setInterval(() => setEpochTime(Date.now()), 500)

	// we must change the namespace to `svg`, so that hypescript picks up on it, and handles the newly created svg nodes appropriately.
	namespaceStack.push("svg")
	const svg_dom = <svg style="user-select: none;" width="200px" height="200px" viewbox="0 0 200 200">
		<g transform="translate(100, 100)">
			<circle r="100" fill="lightgrey" stroke="black" />
			<text text-anchor="middle" y="-25">Apple Watch XVII</text>
			<line transform={createMemo((id) => `rotate(${currentTime(id).second * 6})`)[1]} class="hand-seconds" y1="0" y2="-100" stroke="red" stroke-width={4} />
			<line transform={createMemo((id) => `rotate(${currentTime(id).minute * 6})`)[1]} class="hand-minutes" y1="0" y2="-100" stroke="green" stroke-width={4} />
			<line transform={createMemo((id) => `rotate(${currentTime(id).hour * 5 * 6})`)[1]} class="hand-hours" y1="0" y2="-100" stroke="blue" stroke-width={4} />
		</g>
	</svg>
	// declare that the svg namespace is now over, and switch back to html namespace
	namespaceStack.pop()
	return svg_dom
}

document.body.append(App())
```

### Reactively compute bounding-boxes of many nested rectangles
```ts
// reactively compute the bounding-boxes of many nested rectangles.
// 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.
// if the top-most rectangle's `right` or `top` bounding-box's sides exceed `600`, then we should log `"overflow"` in the console.
// at the end of every reaction cycle, log the number of computations done in the console.

import { Context } from "jsr:@oazmi/tsignal/context"
import { StateSignal_Factory, MemoSignal_Factory, EffectSignal_Factory } from "jsr:@oazmi/tsignal/signal"
import type { Accessor, Setter } from "jsr:@oazmi/tsignal/typedefs"

/** `x` and `y` are relative to the parent-rectangle's top-left corner (which is their (x, y) position). */
interface Rect { x: number, y: number, width: number, height: number }

/** the bounding box of a `Rect` */
interface Box { left: number, top: number, right: number, bottom: number }

const signal_ctx = new Context()
const createState = signal_ctx.addClass(StateSignal_Factory)
const createMemo = signal_ctx.addClass(MemoSignal_Factory)
const createEffect = signal_ctx.addClass(EffectSignal_Factory)
const rectEqualityFn = (rect1: Rect | undefined, rect2: Rect): boolean => {
	return rect1 === undefined ? false :
		rect1.x === rect2.x &&
		rect1.y === rect2.y &&
		rect1.width === rect2.width &&
		rect1.height === rect2.height
}
const boxEqualityFn = (box1: Box | undefined, box2: Box): boolean => {
	return box1 === undefined ? false :
		box1.top === box2.top &&
		box1.left === box2.left &&
		box1.bottom === box2.bottom &&
		box1.right === box2.right
}

type RectangleObject = Rect & { children?: RectangleObject[] }

class Rectangle {
	rect: Accessor<Rect>
	setRect: Setter<Rect>
	box: Accessor<Box>
	children: Rectangle[] = []

	constructor(initial_rect: Rect) {
		const [idRect, getRect, setRect] = createState(initial_rect, { equals: rectEqualityFn })
		const [idBox, getBox] = createMemo<Box>((id) => {
			const { x, y, width, height } = getRect(id)
			const children_boxes = this.children.map((child) => child.box(idBox))
			const
				top = Math.min(y, ...children_boxes.map((child_box) => child_box.top)),
				left = Math.min(x, ...children_boxes.map((child_box) => child_box.left)),
				bottom = Math.max(y + height, ...children_boxes.map((child_box) => y + child_box.bottom)),
				right = Math.max(x + width, ...children_boxes.map((child_box) => x + child_box.right))
			return { top, left, bottom, right }
		}, { equals: boxEqualityFn })

		this.rect = getRect
		this.setRect = setRect
		this.box = getBox
	}

	render(ctx: CanvasRenderingContext2D) {
		const { x, y, width, height } = this.rect()
		const { top, left, bottom, right } = this.box()
		const children = this.children
		ctx.fillStyle = get_next_fillcolor()
		ctx.strokeStyle = get_next_strokecolor()
		ctx.fillRect(x, y, width, height)
		ctx.strokeRect(left, top, right - left, bottom - top)
		ctx.translate(x, y)
		for (const child of children) {
			child.render(ctx)
		}
		ctx.translate(-x, -y)
	}

	static fromObject(object: RectangleObject): Rectangle {
		const { children, ...rect } = object
		const instance = new Rectangle(rect)
		const children_instances = children?.map(Rectangle.fromObject) ?? []
		instance.children.push(...children_instances)
		return instance
	}
}

let color_idx = 0
const fillcolors = ["blue", "cyan", "goldenrod", "gray", "green", "khaki", "magenta", "orange", "orchid", "red", "salmon", "seagreen", "turquoise", "violet",]
const strokecolors = fillcolors.map((color) => "dark" + color)
const get_next_fillcolor = () => { return fillcolors[color_idx++ % fillcolors.length] }
const get_next_strokecolor = () => { return strokecolors[color_idx++ % strokecolors.length] }

const all_rectangles = Rectangle.fromObject({
	x: 10, y: 20, width: 70, height: 80, children: [
		{
			x: 15, y: 25, width: 50, height: 60, children: [
				{
					x: 20, y: 30, width: 40, height: 50, children: [
						{
							x: 25, y: 35, width: 30, height: 40, children: [
								{ x: 30, y: 40, width: 20, height: 30 },
								{ x: 35, y: 45, width: 10, height: 20 }
							]
						},
						{ x: 40, y: 50, width: 10, height: 20 }
					]
				},
				{
					x: 45, y: 55, width: 10, height: 15, children: [
						{ x: 0, y: 40, width: 30, height: 50 },
						{ x: 70, y: 80, width: 50, height: 20 }
					]
				}
			]
		},
		{ x: 50, y: 60, width: 40, height: 30 }
	]
})

const canvas = document.createElement("canvas")
const ctx = canvas.getContext("2d")!
document.body.appendChild(canvas)

const [idRedraw, awaitRedraw, fireRedraw] = createEffect((id) => {
	// add dependence on all rectangles
	const add_dependence = (rect_object: Rectangle) => {
		rect_object.rect(id)
		rect_object.box(id)
		rect_object.children.forEach(add_dependence)
	}
	add_dependence(all_rectangles)

	color_idx = 0
	ctx.reset()
	ctx.scale(2, 2)
	all_rectangles.render(ctx)
})

fireRedraw()

// try the following lines in your console (one by one) to witness reactivity:
// all_rectangles.children[0].children[0].children[0].setRect({x:10, y:10, width: 20, height: 50})
// all_rectangles.children[0].children[1].setRect({x:50, y:50, width: 100, height: 50})
```

## Signal Classes

here is a list of all signal classes that are currently available:
- [`StateSignal`](./src/signal.ts#L108)
- [`MemoSignal`](./src/signal.ts#L146)
- [`LazySignal`](./src/signal.ts#L187)
- [`EffectSignal`](./src/signal.ts#L241)
- [`RecordSignal`](./src/record_signal.ts#L44)
- [`RecordStateSignal`](./src/record_signal.ts#L132)
- [`MemoRecordSignal`](./src/record_signal.ts#L171)


## Theory

### What is Topological Ordering

a signal-based reactivity system can be thought of as a **DAG** (directed acyclic graph) - which is something that resembles a dependency graph. <br>
in 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.

we use the following notation to describe one relation in our graph:

> SourceSignal -> [DestinationSignal_1, DestinationSignal_2, DestinationSignal_3, ...]

which is basically saying that: updating `SourceSignal` should result in an update in `DestinationSignal_1`, `DestinationSignal_2`, etc... . <br>
or 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`).

a DAG graph can then be described as a series/array of relations:
```txt
MyGraph = [
	Signal_A -> [Signal_B, Signal_C],
	Signal_B -> [Signal_D, Signal_F],
	Signal_C -> [Signal_E, Signal_F, Signal_H],
	...
]
```

a 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. <br>
so, for example the following DAG graph:

```mermaid
---
title: "graph"
---
flowchart LR
	332796(("A")) --> 590207(("B"))
	332796 --> 763885(("C"))
	590207 --> 562158(("D"))
	763885 --> 932932(("E"))
	763885 --> 940461(("H"))
	562158 --> 831043(("G"))
	831043 --> 450102(("I"))
	932932 --> 940461
	940461 --> 450102
	590207 --> 270457(("F"))
	562158 --> 270457
	763885 --> 270457
	932932 --> 270457
	450102 --> 770803(("J"))
```

```txt
graph = [ A->[B, C] , B->[D, F] , C->[E, F, H] , D->[F, G] , E->[F, H] , F->[I] , G->[I] , H->[I] , I->[J] ]
```

has the following (non-unique) topologically sorted array:
```txt
nodes_sorted = [ A, B, D, G, C, E, H, F, I, J ]
```


### Update State of each Signal

in 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:
- ` 1` or `SignalUpdateStatus.UPDATED`: this signal's value has been updated, and therefore its observers should be updated too.
- ` 0` or `SignalUpdateStatus.UNCHANGED`: this signal's value has not changed, and therefore its observers should be _not_ be updated by this signal.
do 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`).
- `-1` or `SignalUpdateStatus.ABORTED`: this signal has been aborted, and therefore its observers must abort execution as well.
the observers will abort _even_ if they had a dependency that _did_ update (had a status value of `1`).
should the aborted observer signal also abort its own observers? that's a thing that is open to debate.
currently, it does not abort its own observers.


### The Topological Update Cycle Algorithm

#### Algorithm:

given an array of `source_ids` to initiate the signal from (simultaneously),
the `Context`'s [update cycle](./src/context.ts#L148) ({@link context!Context.fireUpdateCycle}) works by following the steps below:
- 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:
  - `source_ids` are **always** at the beginning of the `topological_ids` array.
- 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`).
- 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.
- fill the set `next_to_visit` with our initial `source_ids`
- now, in an ordered loop, for each `id` inside of `topological_ids` (starting with the source ids), do:
  - check that `id` exists in `next_to_visit` AND `id` does not exist inside of `not_to_visit`. if true, then:
    - delete `id` from `next_to_visit`
    - run the signal associated with the `id`, and save its execution's returned `SignalUpdateStatus` to `status`.
    - is `status == SignalUpdateStatus.UPDATED` (i.e. `status == 1`)? if true, then:
      - for every `observer_id` of this signal's `id`, add `observer_id` to `next_to_visit`
    - is `status == SignalUpdateStatus.ABORTED` (i.e. `status == -1`)? if true, then:
      - for every `observer_id` of this signal's `id`, add `observer_id` to `not_to_visit`
  - is `next_to_visit` set now empty? if true, then:
    - terminate/break the loop early, since there is no way any more ids will ever be added to `next_to_visit`


#### Example:

starting with the following `graph`, and its `topological_ids` sorted array, and the initial `source_ids`:
```txt
graph = [ A->[B, C] , B->[D, F] , C->[E, F, H] , D->[F, G] , E->[F, H] , F->[I] , G->[I] , H->[I] , I->[J] ]
topological_ids = [ A, B, D, G, C, E, H, F, I, J ]
source_ids = [ A, ]
```
and assuming that signal `B` does not change when executed (i.e. `status = SignalUpdateStatus.UNCHANGED`), we get the following update cycle:
- assign `not_to_visit = { }`
- assign `next_to_visit = {A}`
- `id` = A:
  - `next_to_visit == {}`
  - `status = run(A)` == 1
    - add each of `graph[A]` = [B, C] to `next_to_visit`
- `next_to_visit == {B, C}` and is not empty
- `id` = B:
  - `next_to_visit == {C}`
  - `status = run(B)` == 0
- `next_to_visit == {C}` and is not empty
- `id` = D: skipped because it is not in `next_to_visit`
- `next_to_visit == {C}` and is not empty
- `id` = G: skipped because it is not in `next_to_visit`
- `next_to_visit == {C}` and is not empty
- `id` = C:
  - `next_to_visit == {}`
  - `status = run(C)` == 1
    - add each of `graph[C]` = [E, F, H] to `next_to_visit`
- `next_to_visit == {E, F, H}` and is not empty
- `id` = E:
  - `next_to_visit == {F, H}`
  - `status = run(E)` == 1
    - add each of `graph[E]` = [F, H] to `next_to_visit`
- `next_to_visit == {F, H}` and is not empty
- `id` = H:
  - `next_to_visit == {F}`
  - `status = run(H)` == 1
    - add each of `graph[H]` = [I] to `next_to_visit`
- `next_to_visit == {F, I}` and is not empty
- `id` = F:
  - `next_to_visit == {I}`
  - `status = run(F)` == 1
    - add each of `graph[F]` = [I] to `next_to_visit`
- `next_to_visit == {I}` and is not empty
- `id` = I:
  - `next_to_visit == {}`
  - `status = run(I)` == 1
    - add each of `graph[I]` = [J] to `next_to_visit`
- `next_to_visit == {J}` and is not empty
- `id` = I:
  - `next_to_visit == {}`
  - `status = run(I)` == 1
    - add each of `graph[J]` = [ ] to `next_to_visit`
- `next_to_visit == { }` and **is** empty, so terminate


### Caching Mechanism

recomputing the topologically ordered signal ids at the beginning of every update cycle is wasteful,
so instead, we memorize the result of a topological ordering when certain the update is initiated from a certain `source_ids`. <br>
in 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`. <br>
the hashing function is defined in [`hash_ids`](./src/funcdefs.ts#L46) ({@link funcdefs!hash_ids}),
and the memorization/caching function is defined in [`get_ids_to_visit`](./src/context.ts#L96) ({@link context!get_ids_to_visit}).

the cache is only valid if no mutations to the DAG graph (addition or deletion of nodes or edges) have been done. <br>
that's why we clear the cache whenever a mutative action is taken within the `Context`'s graph, such as:
- introducing a new signal
- a signal declares a new dependency or observer
- deleting a signal


### Observation Detection Mechanism

TODO: explain the difference between a signal's `id` and its `rid`. then explain how non-zero `rid` are an idication for a new observation. 
