@valentin30/signal 0.2.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.
- package/.prettierrc +21 -0
- package/package.json +86 -8
- package/src/core/computed.ts +68 -0
- package/src/core/contracts/consumer.ts +3 -0
- package/src/core/contracts/source.ts +7 -0
- package/src/core/interfaces/consumers.ts +8 -0
- package/src/core/interfaces/sources.ts +8 -0
- package/src/core/interfaces/value.ts +5 -0
- package/src/core/signal.ts +45 -0
- package/src/modules/common/contracts/addable.ts +3 -0
- package/src/modules/common/contracts/disposable.ts +3 -0
- package/src/modules/common/contracts/subscriber.ts +6 -0
- package/src/modules/common/types/callable.ts +1 -0
- package/src/modules/common/types/callback.ts +1 -0
- package/src/modules/common/utils/compare.ts +3 -0
- package/src/modules/common/utils/swap.ts +23 -0
- package/src/modules/consumers/factory.ts +26 -0
- package/src/modules/event/channel.ts +82 -0
- package/src/modules/event/effect.ts +45 -0
- package/src/modules/event/notifier.ts +38 -0
- package/src/modules/node/factory.ts +51 -0
- package/src/modules/node/index.ts +13 -0
- package/src/modules/node/source.ts +11 -0
- package/src/modules/scheduler/dispatch.ts +3 -0
- package/src/modules/scheduler/runner.ts +47 -0
- package/src/modules/sources/dynamic.ts +73 -0
- package/src/modules/sources/static.ts +49 -0
- package/src/modules/value/factory.ts +29 -0
- package/src/packages/builder/consumers.ts +11 -0
- package/src/packages/builder/node.ts +16 -0
- package/src/packages/builder/sources.ts +18 -0
- package/src/packages/builder/value.ts +11 -0
- package/src/packages/builder.ts +4 -0
- package/src/packages/core/computed.ts +15 -0
- package/src/packages/core/context.ts +5 -0
- package/src/packages/core/scheduler.ts +5 -0
- package/src/packages/core/signal.ts +12 -0
- package/src/packages/core.ts +2 -0
- package/src/packages/event/channel.ts +17 -0
- package/src/packages/event/effect.ts +15 -0
- package/src/packages/event/notifier.ts +14 -0
- package/src/packages/event.ts +3 -0
- package/src/packages/react/use-computed.ts +9 -0
- package/src/packages/react/use-read.ts +23 -0
- package/src/packages/react/use-signal.ts +9 -0
- package/src/packages/react.ts +3 -0
- package/src/runtime/context.ts +36 -0
- package/src/runtime/scheduler.ts +75 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +16 -0
- package/tsup.config.ts +53 -0
- package/vitest.config.ts +14 -0
- package/dist/index.d.mts +0 -287
- package/dist/index.d.ts +0 -287
- package/dist/index.js +0 -497
- package/dist/index.mjs +0 -465
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
|
-
"
|
|
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
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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,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 @@
|
|
|
1
|
+
export type Callable<Args extends any[], ReturnType> = (...args: Args) => ReturnType
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type Callback = () => void
|
|
@@ -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,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
|
+
}
|