@valentin30/signal 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.prettierrc +21 -0
  2. package/package.json +86 -8
  3. package/src/core/computed.ts +68 -0
  4. package/src/core/contracts/consumer.ts +3 -0
  5. package/src/core/contracts/source.ts +7 -0
  6. package/src/core/interfaces/consumers.ts +8 -0
  7. package/src/core/interfaces/sources.ts +8 -0
  8. package/src/core/interfaces/value.ts +5 -0
  9. package/src/core/signal.ts +45 -0
  10. package/src/modules/common/contracts/addable.ts +3 -0
  11. package/src/modules/common/contracts/disposable.ts +3 -0
  12. package/src/modules/common/contracts/subscriber.ts +6 -0
  13. package/src/modules/common/types/callable.ts +1 -0
  14. package/src/modules/common/types/callback.ts +1 -0
  15. package/src/modules/common/utils/compare.ts +3 -0
  16. package/src/modules/common/utils/swap.ts +23 -0
  17. package/src/modules/consumers/factory.ts +26 -0
  18. package/src/modules/event/channel.ts +82 -0
  19. package/src/modules/event/effect.ts +45 -0
  20. package/src/modules/event/notifier.ts +38 -0
  21. package/src/modules/node/factory.ts +51 -0
  22. package/src/modules/node/index.ts +13 -0
  23. package/src/modules/node/source.ts +11 -0
  24. package/src/modules/scheduler/dispatch.ts +3 -0
  25. package/src/modules/scheduler/runner.ts +47 -0
  26. package/src/modules/sources/dynamic.ts +73 -0
  27. package/src/modules/sources/static.ts +49 -0
  28. package/src/modules/value/factory.ts +29 -0
  29. package/src/packages/builder/consumers.ts +11 -0
  30. package/src/packages/builder/node.ts +16 -0
  31. package/src/packages/builder/sources.ts +18 -0
  32. package/src/packages/builder/value.ts +11 -0
  33. package/src/packages/builder.ts +4 -0
  34. package/src/packages/core/computed.ts +15 -0
  35. package/src/packages/core/context.ts +5 -0
  36. package/src/packages/core/scheduler.ts +5 -0
  37. package/src/packages/core/signal.ts +12 -0
  38. package/src/packages/core.ts +2 -0
  39. package/src/packages/event/channel.ts +17 -0
  40. package/src/packages/event/effect.ts +15 -0
  41. package/src/packages/event/notifier.ts +14 -0
  42. package/src/packages/event.ts +3 -0
  43. package/src/packages/react/use-computed.ts +9 -0
  44. package/src/packages/react/use-read.ts +23 -0
  45. package/src/packages/react/use-signal.ts +9 -0
  46. package/src/packages/react.ts +3 -0
  47. package/src/runtime/context.ts +36 -0
  48. package/src/runtime/scheduler.ts +75 -0
  49. package/tsconfig.build.json +4 -0
  50. package/tsconfig.json +16 -0
  51. package/tsup.config.ts +53 -0
  52. package/vitest.config.ts +14 -0
  53. package/dist/index.d.mts +0 -194
  54. package/dist/index.d.ts +0 -194
  55. package/dist/index.js +0 -372
  56. package/dist/index.mjs +0 -342
package/.prettierrc ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "singleQuote": true,
3
+ "trailingComma": "all",
4
+ "tabWidth": 4,
5
+ "overrides": [
6
+ {
7
+ "files": ["*.json", "*rc", "*.yml", "*.yaml"],
8
+ "options": {
9
+ "tabWidth": 2,
10
+ "trailingComma": "none"
11
+ }
12
+ }
13
+ ],
14
+ "printWidth": 140,
15
+ "semi": false,
16
+ "arrowParens": "avoid",
17
+ "bracketSpacing": true,
18
+ "jsxSingleQuote": false,
19
+ "singleAttributePerLine": false,
20
+ "bracketSameLine": false
21
+ }
package/package.json CHANGED
@@ -1,13 +1,87 @@
1
1
  {
2
2
  "name": "@valentin30/signal",
3
- "version": "0.1.0",
3
+ "type": "module",
4
+ "version": "1.0.0",
4
5
  "description": "My take on signals - lightweight reactive primitives inspired by Preact Signals, written in TypeScript.",
5
- "main": "dist/index.js",
6
- "module": "dist/index.mjs",
7
- "types": "dist/index.d.ts",
8
- "files": [
9
- "dist"
10
- ],
6
+ "exports": {
7
+ ".": {
8
+ "import": "./dist/core.js",
9
+ "types": "./dist/core.d.ts"
10
+ },
11
+ "./core": {
12
+ "import": "./dist/core.js",
13
+ "types": "./dist/core.d.ts"
14
+ },
15
+ "./core/signal": {
16
+ "import": "./dist/core/signal.js",
17
+ "types": "./dist/core/signal.d.ts"
18
+ },
19
+ "./core/computed": {
20
+ "import": "./dist/core/computed.js",
21
+ "types": "./dist/core/computed.d.ts"
22
+ },
23
+ "./core/context": {
24
+ "import": "./dist/core/context.js",
25
+ "types": "./dist/core/context.d.ts"
26
+ },
27
+ "./core/scheduler": {
28
+ "import": "./dist/core/scheduler.js",
29
+ "types": "./dist/core/scheduler.d.ts"
30
+ },
31
+ "./react": {
32
+ "import": "./dist/react.js",
33
+ "types": "./dist/react.d.ts"
34
+ },
35
+ "./react/use-signal": {
36
+ "import": "./dist/react/use-signal.js",
37
+ "types": "./dist/react/use-signal.d.ts"
38
+ },
39
+ "./react/use-computed": {
40
+ "import": "./dist/react/use-computed.js",
41
+ "types": "./dist/react/use-computed.d.ts"
42
+ },
43
+ "./react/use-read": {
44
+ "import": "./dist/react/use-read.js",
45
+ "types": "./dist/react/use-read.d.ts"
46
+ },
47
+ "./event": {
48
+ "import": "./dist/event.js",
49
+ "types": "./dist/event.d.ts"
50
+ },
51
+ "./event/channel": {
52
+ "import": "./dist/event/channel.js",
53
+ "types": "./dist/event/channel.d.ts"
54
+ },
55
+ "./event/effect": {
56
+ "import": "./dist/event/effect.js",
57
+ "types": "./dist/event/effect.d.ts"
58
+ },
59
+ "./event/notifier": {
60
+ "import": "./dist/event/notifier.js",
61
+ "types": "./dist/event/notifier.d.ts"
62
+ },
63
+ "./builder": {
64
+ "import": "./dist/builder.js",
65
+ "types": "./dist/builder.d.ts"
66
+ },
67
+ "./builder/value": {
68
+ "import": "./dist/builder/value.js",
69
+ "types": "./dist/builder/value.d.ts"
70
+ },
71
+ "./builder/node": {
72
+ "import": "./dist/builder/node.js",
73
+ "types": "./dist/builder/node.d.ts"
74
+ },
75
+ "./builder/sources": {
76
+ "import": "./dist/builder/sources.js",
77
+ "types": "./dist/builder/sources.d.ts"
78
+ },
79
+ "./builder/consumers": {
80
+ "import": "./dist/builder/consumers.js",
81
+ "types": "./dist/builder/consumers.d.ts"
82
+ }
83
+ },
84
+ "sideEffects": false,
11
85
  "scripts": {
12
86
  "clean": "rimraf dist",
13
87
  "build": "npm run clean && tsup",
@@ -20,7 +94,7 @@
20
94
  "type": "git",
21
95
  "url": "git+https://github.com/valentin30/signal.git"
22
96
  },
23
- "author": "Valentin",
97
+ "author": "Valentin Spasov",
24
98
  "license": "MIT",
25
99
  "bugs": {
26
100
  "url": "https://github.com/valentin30/signal/issues"
@@ -28,9 +102,13 @@
28
102
  "homepage": "https://github.com/valentin30/signal#readme",
29
103
  "devDependencies": {
30
104
  "@types/node": "^22.15.29",
105
+ "@types/react": "^19.1.12",
31
106
  "rimraf": "^6.0.1",
32
107
  "tsup": "^8.5.0",
33
108
  "typescript": "^5.8.3",
34
109
  "vitest": "^3.1.4"
110
+ },
111
+ "peerDependencies": {
112
+ "react": "^18 || ^19"
35
113
  }
36
114
  }
@@ -0,0 +1,68 @@
1
+ import { Consumer } from '@valentin30/signal/core/contracts/consumer'
2
+ import { Source } from '@valentin30/signal/core/contracts/source'
3
+ import { Consumers } from '@valentin30/signal/core/interfaces/consumers'
4
+ import { Sources } from '@valentin30/signal/core/interfaces/sources'
5
+ import { Value } from '@valentin30/signal/core/interfaces/value'
6
+
7
+ export class Computed<T> implements Source, Consumer, Value<T> {
8
+ private invalidated: boolean
9
+
10
+ private readonly value: Value<T>
11
+
12
+ private readonly sources: Sources<T>
13
+
14
+ private readonly consumers: Consumers
15
+
16
+ private __v: number
17
+
18
+ constructor(value: Value<T>, sources: Sources<T>, consumers: Consumers) {
19
+ this.invalidated = true
20
+ this.value = value
21
+ this.sources = sources
22
+ this.consumers = consumers
23
+ this.__v = 0
24
+ }
25
+
26
+ public dirty(): boolean {
27
+ if (this.consumers.active()) return this.invalidated
28
+ return this.sources.changed()
29
+ }
30
+
31
+ public read(): T {
32
+ if (this.dirty()) {
33
+ this.invalidated = false
34
+ this.value.write(this.sources.compute())
35
+ }
36
+ return this.value.read()
37
+ }
38
+
39
+ public write(): boolean {
40
+ return false
41
+ }
42
+
43
+ public compare(a: T, b: T): boolean {
44
+ return this.value.compare(a, b)
45
+ }
46
+
47
+ public invalidate(): void {
48
+ if (this.invalidated) return
49
+ this.invalidated = true
50
+ this.__v++
51
+ this.consumers.invalidate()
52
+ }
53
+
54
+ public link(consumer: Consumer): void {
55
+ if (!this.consumers.active()) this.sources.link(this)
56
+ this.consumers.link(consumer)
57
+ }
58
+
59
+ public unlink(consumer: Consumer): void {
60
+ this.consumers.unlink(consumer)
61
+ if (this.consumers.active()) return
62
+ this.sources.unlink(this)
63
+ }
64
+
65
+ public version(): number {
66
+ return this.__v
67
+ }
68
+ }
@@ -0,0 +1,3 @@
1
+ export interface Consumer {
2
+ invalidate(): void
3
+ }
@@ -0,0 +1,7 @@
1
+ import { Consumer } from '@valentin30/signal/core/contracts/consumer'
2
+
3
+ export interface Source {
4
+ link(consumer: Consumer): void
5
+ unlink(consumer: Consumer): void
6
+ version(): number
7
+ }
@@ -0,0 +1,8 @@
1
+ import { Consumer } from '@valentin30/signal/core/contracts/consumer'
2
+
3
+ export interface Consumers {
4
+ active(): boolean
5
+ invalidate(): void
6
+ link(consumer: Consumer): void
7
+ unlink(consumer: Consumer): void
8
+ }
@@ -0,0 +1,8 @@
1
+ import { Consumer } from '@valentin30/signal/core/contracts/consumer'
2
+
3
+ export interface Sources<T> {
4
+ compute(): T
5
+ changed(): boolean
6
+ link(consumer: Consumer): void
7
+ unlink(consumer: Consumer): void
8
+ }
@@ -0,0 +1,5 @@
1
+ export interface Value<T> {
2
+ read(): T
3
+ write(value: T): boolean
4
+ compare(a: T, b: T): boolean
5
+ }
@@ -0,0 +1,45 @@
1
+ import { Consumer } from '@valentin30/signal/core/contracts/consumer'
2
+ import { Source } from '@valentin30/signal/core/contracts/source'
3
+ import { Consumers } from '@valentin30/signal/core/interfaces/consumers'
4
+ import { Value } from '@valentin30/signal/core/interfaces/value'
5
+
6
+ export class Signal<T> implements Source, Value<T> {
7
+ private readonly value: Value<T>
8
+
9
+ private readonly consumers: Consumers
10
+
11
+ private __v: number
12
+
13
+ constructor(value: Value<T>, consumers: Consumers) {
14
+ this.value = value
15
+ this.consumers = consumers
16
+ this.__v = 0
17
+ }
18
+
19
+ public read(): T {
20
+ return this.value.read()
21
+ }
22
+
23
+ public write(value: T): boolean {
24
+ if (!this.value.write(value)) return false
25
+ this.__v++
26
+ this.consumers.invalidate()
27
+ return true
28
+ }
29
+
30
+ public compare(a: T, b: T): boolean {
31
+ return this.value.compare(a, b)
32
+ }
33
+
34
+ public link(consumer: Consumer): void {
35
+ return this.consumers.link(consumer)
36
+ }
37
+
38
+ public unlink(consumer: Consumer): void {
39
+ return this.consumers.unlink(consumer)
40
+ }
41
+
42
+ public version(): number {
43
+ return this.__v
44
+ }
45
+ }
@@ -0,0 +1,3 @@
1
+ export interface Addable<T> {
2
+ add(value: T): void
3
+ }
@@ -0,0 +1,3 @@
1
+ export interface Disposable {
2
+ dispose(): void
3
+ }
@@ -0,0 +1,6 @@
1
+ import { Source } from '@valentin30/signal/core/contracts/source'
2
+
3
+ export interface Subscriber {
4
+ link(source: Source): void
5
+ unlink(source: Source): void
6
+ }
@@ -0,0 +1 @@
1
+ export type Callable<Args extends any[], ReturnType> = (...args: Args) => ReturnType
@@ -0,0 +1 @@
1
+ export type Callback = () => void
@@ -0,0 +1,3 @@
1
+ export function strict_compare<T>(a: T, b: T): boolean {
2
+ return a === b || (a !== a && b !== b)
3
+ }
@@ -0,0 +1,23 @@
1
+ let tmp: any = null
2
+
3
+ export function swap<
4
+ Value extends ASource[AKey],
5
+ ASource extends object,
6
+ AKey extends keyof ASource,
7
+ BSource extends Record<BKey, Value>,
8
+ BKey extends keyof BSource,
9
+ >(a: ASource, aKey: AKey, b: BSource, bKey: BKey): void {
10
+ if (<Value>a[aKey] === <Value>b[bKey]) return
11
+ tmp = a[aKey]
12
+ a[aKey] = <Value>b[bKey]
13
+ b[bKey] = tmp
14
+ tmp = null
15
+ }
16
+
17
+ export function unsafe_swap(a: object, aKey: string | number | symbol, b: object, bKey: string | number | symbol): void {
18
+ if (a[<never>aKey] === b[<never>bKey]) return
19
+ tmp = a[<never>aKey]
20
+ a[<never>aKey] = b[<never>bKey]
21
+ b[<never>bKey] = <never>tmp
22
+ tmp = null
23
+ }
@@ -0,0 +1,26 @@
1
+ import { Consumer } from '@valentin30/signal/core/contracts/consumer'
2
+ import { Consumers } from '@valentin30/signal/core/interfaces/consumers'
3
+
4
+ export function ConsumersFactory() {
5
+ class C extends Set<Consumer> implements Consumers {
6
+ public active(): boolean {
7
+ return this.size > 0
8
+ }
9
+
10
+ public invalidate(): void {
11
+ for (const consumer of this.values()) consumer.invalidate()
12
+ }
13
+
14
+ public link(consumer: Consumer): void {
15
+ this.add(consumer)
16
+ }
17
+
18
+ public unlink(consumer: Consumer): void {
19
+ this.delete(consumer)
20
+ }
21
+ }
22
+
23
+ return function c(): Consumers {
24
+ return new C()
25
+ }
26
+ }
@@ -0,0 +1,82 @@
1
+ import { Consumer } from '@valentin30/signal/core/contracts/consumer'
2
+ import { Source } from '@valentin30/signal/core/contracts/source'
3
+ import { Disposable } from '@valentin30/signal/modules/common/contracts/disposable'
4
+ import { Subscriber } from '@valentin30/signal/modules/common/contracts/subscriber'
5
+ import { Callback } from '@valentin30/signal/modules/common/types/callback'
6
+ import { Dispatch } from '@valentin30/signal/modules/scheduler/dispatch'
7
+ import { Runner } from '@valentin30/signal/modules/scheduler/runner'
8
+
9
+ export interface Channel extends Subscriber, Disposable {
10
+ subscribe(callback: Callback): void
11
+ unsubscribe(callback: Callback): void
12
+ }
13
+
14
+ export function ChannelFactory(enqueue: (runner: Runner) => void, dequeue: (runner: Runner) => void) {
15
+ class Ch implements Channel, Runner, Consumer {
16
+ private queued: boolean
17
+
18
+ private current: Set<Callback>
19
+
20
+ private pending: Set<Callback>
21
+
22
+ private sources: Set<Source>
23
+
24
+ constructor() {
25
+ this.queued = false
26
+ this.pending = new Set()
27
+ this.current = new Set()
28
+ this.sources = new Set()
29
+ }
30
+
31
+ public run(dispatch: Dispatch): void {
32
+ for (const callback of this.current) dispatch(callback)
33
+ this.cleanup()
34
+ }
35
+
36
+ private cleanup(): void {
37
+ this.queued = false
38
+ if (this.pending.size === 0) return
39
+ for (const callback of this.pending) this.current.add(callback)
40
+ this.pending.clear()
41
+ }
42
+
43
+ public subscribe(callback: Callback): void {
44
+ if (this.queued) this.pending.add(callback)
45
+ else this.current.add(callback)
46
+ }
47
+
48
+ public unsubscribe(callback: Callback): void {
49
+ this.current.delete(callback)
50
+ this.pending.delete(callback)
51
+ }
52
+
53
+ public link(source: Source): void {
54
+ source.link(this)
55
+ this.sources.add(source)
56
+ }
57
+
58
+ public unlink(source: Source): void {
59
+ source.unlink(this)
60
+ this.sources.delete(source)
61
+ }
62
+
63
+ public dispose(): void {
64
+ dequeue(this)
65
+ this.cleanup()
66
+ for (const source of this.sources) source.unlink(this)
67
+ this.current.clear()
68
+ this.sources.clear()
69
+ }
70
+
71
+ public invalidate(): void {
72
+ enqueue(this)
73
+ this.queued = true
74
+ }
75
+ }
76
+
77
+ return function ch(...sources: Source[]): Channel {
78
+ const channel = new Ch()
79
+ for (const source of sources) channel.link(source)
80
+ return channel
81
+ }
82
+ }
@@ -0,0 +1,45 @@
1
+ import { Consumer } from '@valentin30/signal/core/contracts/consumer'
2
+ import { Sources } from '@valentin30/signal/core/interfaces/sources'
3
+ import { Disposable } from '@valentin30/signal/modules/common/contracts/disposable'
4
+ import { Callback } from '@valentin30/signal/modules/common/types/callback'
5
+ import { Runner } from '@valentin30/signal/modules/scheduler/runner'
6
+
7
+ export function EffectFactory(enqueue: (runner: Runner) => void, dequeue: (runner: Runner) => void) {
8
+ class Effect implements Runner, Consumer, Disposable {
9
+ private readonly sources: Sources<Callback | void>
10
+
11
+ private cleanup: Callback | void
12
+
13
+ constructor(sources: Sources<Callback | void>) {
14
+ this.sources = sources
15
+ this.cleanup = void 0
16
+ this.sources.link(this)
17
+ enqueue(this)
18
+ }
19
+
20
+ public run(): void {
21
+ try {
22
+ this.cleanup?.()
23
+ this.cleanup = this.sources.compute()
24
+ } catch (error) {
25
+ console.error(error)
26
+ }
27
+ }
28
+
29
+ public invalidate(): void {
30
+ enqueue(this)
31
+ }
32
+
33
+ public dispose(): void {
34
+ dequeue(this)
35
+ this.sources.unlink(this)
36
+ this.cleanup?.()
37
+ this.cleanup = void 0
38
+ }
39
+ }
40
+
41
+ return function effect(sources: Sources<Callback | void>): Callback {
42
+ const o = new Effect(sources)
43
+ return () => o.dispose()
44
+ }
45
+ }
@@ -0,0 +1,38 @@
1
+ import { Consumer } from '@valentin30/signal/core/contracts/consumer'
2
+ import { Source } from '@valentin30/signal/core/contracts/source'
3
+ import { Disposable } from '@valentin30/signal/modules/common/contracts/disposable'
4
+ import { Callback } from '@valentin30/signal/modules/common/types/callback'
5
+ import { Runner } from '@valentin30/signal/modules/scheduler/runner'
6
+
7
+ export function NotifierFactory(enqueue: (runner: Runner) => void, dequeue: (runner: Runner) => void) {
8
+ class Notifier implements Runner, Consumer, Disposable {
9
+ private source: Source | void
10
+
11
+ private callback: Callback | void
12
+
13
+ constructor(source: Source, callback: Callback) {
14
+ this.source = source
15
+ this.callback = callback
16
+ }
17
+
18
+ public invalidate(): void {
19
+ enqueue(this)
20
+ }
21
+
22
+ public run(): void {
23
+ this.callback?.()
24
+ }
25
+
26
+ public dispose(): void {
27
+ dequeue(this)
28
+ this.source?.unlink(this)
29
+ this.source = void 0
30
+ this.callback = void 0
31
+ }
32
+ }
33
+
34
+ return function notifier(source: Source, callback: Callback): Callback {
35
+ const o = new Notifier(source, callback)
36
+ return () => o.dispose()
37
+ }
38
+ }
@@ -0,0 +1,51 @@
1
+ import { Consumer } from '@valentin30/signal/core/contracts/consumer'
2
+ import { Source } from '@valentin30/signal/core/contracts/source'
3
+ import { Node } from '@valentin30/signal/modules/node'
4
+ import { NodeSource } from '@valentin30/signal/modules/node/source'
5
+
6
+ export function NodeFactory(register: (source: Source) => void) {
7
+ class N<T> implements Node<T> {
8
+ private readonly source: NodeSource<T>
9
+
10
+ constructor(source: NodeSource<T>) {
11
+ this.source = source
12
+ }
13
+
14
+ public read(): T {
15
+ register(this.source)
16
+ return this.peek()
17
+ }
18
+
19
+ public peek(): T {
20
+ return this.source.read()
21
+ }
22
+
23
+ public write(value: T): boolean {
24
+ return this.source.write(value)
25
+ }
26
+
27
+ public equals(other: T): boolean {
28
+ return this.compare(this.read(), other)
29
+ }
30
+
31
+ public compare(a: T, b: T): boolean {
32
+ return this.source.compare(a, b)
33
+ }
34
+
35
+ public link(consumer: Consumer): void {
36
+ return this.source.link(consumer)
37
+ }
38
+
39
+ public unlink(consumer: Consumer): void {
40
+ return this.source.unlink(consumer)
41
+ }
42
+
43
+ public version(): number {
44
+ return this.source.version()
45
+ }
46
+ }
47
+
48
+ return function n<T>(source: NodeSource<T>): Node<T> {
49
+ return new N(source)
50
+ }
51
+ }
@@ -0,0 +1,13 @@
1
+ import { Consumer } from '@valentin30/signal/core/contracts/consumer'
2
+ import { Source } from '@valentin30/signal/core/contracts/source'
3
+
4
+ export interface Node<T> extends Source {
5
+ read(): T
6
+ peek(): T
7
+ write(value: T): boolean
8
+ equals(other: T): boolean
9
+ compare(a: T, b: T): boolean
10
+ link(consumer: Consumer): void
11
+ unlink(consumer: Consumer): void
12
+ version(): number
13
+ }
@@ -0,0 +1,11 @@
1
+ import { Consumer } from '@valentin30/signal/core/contracts/consumer'
2
+ import { Source } from '@valentin30/signal/core/contracts/source'
3
+
4
+ export interface NodeSource<T> extends Source {
5
+ read(): T
6
+ write(value: T): boolean
7
+ compare(a: T, b: T): boolean
8
+ link(consumer: Consumer): void
9
+ unlink(consumer: Consumer): void
10
+ version(): number
11
+ }
@@ -0,0 +1,3 @@
1
+ import { Callback } from '@valentin30/signal/modules/common/types/callback'
2
+
3
+ export type Dispatch = (callback: Callback) => void
@@ -0,0 +1,47 @@
1
+ import { Dispatch } from '@valentin30/signal/modules/scheduler/dispatch'
2
+
3
+ /**
4
+ * A schedulable unit of work that can be executed by the scheduler.
5
+ *
6
+ * Runners are queued and executed during a scheduler flush cycle.
7
+ * The scheduler provides a `dispatch` function that enables controlled,
8
+ * deduplicated execution of callbacks.
9
+ */
10
+ export interface Runner {
11
+ /**
12
+ * Execute the runner's work.
13
+ *
14
+ * @param dispatch - A controlled callback executor provided by the scheduler.
15
+ *
16
+ * **Dispatch Behavior:**
17
+ * - Callbacks passed to `dispatch` are deduplicated per flush cycle.
18
+ * - If the same callback reference is dispatched multiple times
19
+ * (by this runner or any other runner), it executes **only once**
20
+ * per scheduler flush across the entire application.
21
+ * - This prevents redundant executions when multiple sources
22
+ * trigger the same callback within a single flush.
23
+ *
24
+ * **When to use dispatch:**
25
+ * - Use `dispatch(callback)` when the callback should be deduplicated
26
+ * (e.g., Channel subscriptions).
27
+ * - Call callbacks directly when deduplication is not desired
28
+ * (e.g., Effect computations).
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * // Channel: uses dispatch for deduplication
33
+ * run(dispatch: Dispatch): void {
34
+ * for (const callback of this.subscribers) {
35
+ * dispatch(callback) // Same callback runs once per flush
36
+ * }
37
+ * }
38
+ *
39
+ * // Effect: runs directly, no deduplication
40
+ * run(dispatch: Dispatch): void {
41
+ * this.cleanup?.()
42
+ * this.cleanup = this.compute()
43
+ * }
44
+ * ```
45
+ */
46
+ run(dispatch: Dispatch): void
47
+ }