@bindtty/signal 0.1.0-alpha.1

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/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @bindtty/signal
2
+
3
+ BindTTY 响应式内核。提供 signal、computed、effect 与订阅清理,供 runtime binding 与 widget 内部状态使用。
4
+
5
+ ## API
6
+
7
+ ```ts
8
+ import { createSignal, computed, effect } from "@bindtty/signal";
9
+
10
+ const count = createSignal(0);
11
+ const label = computed(() => `Count: ${count.get()}`);
12
+
13
+ effect(() => {
14
+ console.log(label.get());
15
+ });
16
+
17
+ count.set(1);
18
+ count.subscribe((value) => { /* ... */ });
19
+ ```
20
+
21
+ - `createSignal(initial)` — 可写 signal
22
+ - `computed(fn)` — 派生只读 signal
23
+ - `effect(fn)` — 副作用,返回 dispose
24
+ - `ReadableSignal.subscribe(listener)` — binding 层建立订阅
25
+
26
+ ## Counter 示例
27
+
28
+ ```ts
29
+ import { createSignal, computed } from "@bindtty/signal";
30
+
31
+ class CounterVM {
32
+ count = createSignal(0);
33
+ countLabel = computed(() => `Count: ${this.count.get()}`);
34
+ inc = () => this.count.set(this.count.get() + 1);
35
+ }
36
+ ```
37
+
38
+ View 中绑定 `vm.countLabel`,signal 更新后由 runtime binding 驱动局部 repaint,无需整树重渲染。
39
+
40
+ ## 单实例要求
41
+
42
+ `@bindtty/signal` 在模块内维护 `computationStack` 与订阅图,**全应用只能有一份物理拷贝**。若应用与 `@bindtty/widgets` 各解析到不同版本的 signal,computed 与 binding 可能异常。
43
+
44
+ - 推荐从 `bindtty` 导入(re-export 同源 signal)
45
+ - 若单独安装本包,版本须与 `bindtty` / `@bindtty/widgets` 的 peer 声明一致
46
+ - 检查:`npm ls @bindtty/signal` 应只有一棵树、一个版本
47
+
48
+ ## 文档
49
+
50
+ - [doc/architecture/ROADMAP.md](../../doc/architecture/ROADMAP.md) — 实现计划
51
+ - [doc/architecture/DESIGN.md](../../doc/architecture/DESIGN.md) — MVVM binding 模型
@@ -0,0 +1,13 @@
1
+ export type Dispose = () => void;
2
+ export type SignalListener<T> = (value: T, previousValue: T) => void;
3
+ export type EffectCleanup = void | Dispose;
4
+ export interface ReadableSignal<T> {
5
+ get(): T;
6
+ subscribe(listener: SignalListener<T>): Dispose;
7
+ }
8
+ export interface Signal<T> extends ReadableSignal<T> {
9
+ set(value: T): void;
10
+ }
11
+ export declare function createSignal<T>(initialValue: T): Signal<T>;
12
+ export declare function computed<T>(derive: () => T): ReadableSignal<T>;
13
+ export declare function effect(runEffect: () => EffectCleanup): Dispose;
package/dist/index.js ADDED
@@ -0,0 +1,160 @@
1
+ const computationStack = [];
2
+ function getActiveComputation() {
3
+ return computationStack[computationStack.length - 1];
4
+ }
5
+ function cleanupDependencies(computation) {
6
+ // 重新运行前先解除旧依赖,否则条件分支切换后会继续响应已经不再读取的 signal。
7
+ for (const dependency of computation.dependencies) {
8
+ dependency.removeSubscriber(computation.run);
9
+ }
10
+ computation.dependencies.clear();
11
+ }
12
+ function trackDependency(source) {
13
+ const computation = getActiveComputation();
14
+ if (!computation || computation.disposed || computation.dependencies.has(source)) {
15
+ return;
16
+ }
17
+ computation.dependencies.add(source);
18
+ source.addSubscriber(computation.run);
19
+ }
20
+ function createComputation(runBody) {
21
+ const computation = {
22
+ dependencies: new Set(),
23
+ disposed: false,
24
+ run: () => {
25
+ if (computation.disposed) {
26
+ return;
27
+ }
28
+ cleanupDependencies(computation);
29
+ // 用栈而不是单个全局变量,是为了支持 computed/effect 嵌套执行。
30
+ // 栈顶永远是当前正在收集依赖的 computation。
31
+ computationStack.push(computation);
32
+ try {
33
+ runBody();
34
+ }
35
+ finally {
36
+ // 即使 runBody 抛错,也必须弹栈,否则后续 get() 会被错误地追踪到旧 computation。
37
+ computationStack.pop();
38
+ }
39
+ }
40
+ };
41
+ return computation;
42
+ }
43
+ function notifySubscribers(subscribers) {
44
+ // 复制一份再遍历,避免 subscriber 执行时增删订阅者导致本轮通知顺序变得不可预测。
45
+ for (const subscriber of [...subscribers]) {
46
+ subscriber();
47
+ }
48
+ }
49
+ function notifyListeners(listeners, value, previousValue) {
50
+ for (const listener of [...listeners]) {
51
+ listener(value, previousValue);
52
+ }
53
+ }
54
+ export function createSignal(initialValue) {
55
+ let currentValue = initialValue;
56
+ const subscribers = new Set();
57
+ const listeners = new Set();
58
+ const source = {
59
+ addSubscriber(subscriber) {
60
+ subscribers.add(subscriber);
61
+ },
62
+ removeSubscriber(subscriber) {
63
+ subscribers.delete(subscriber);
64
+ }
65
+ };
66
+ return {
67
+ get() {
68
+ // 如果当前处在 computed/effect 执行中,这次读取会把当前 signal 记录为依赖。
69
+ trackDependency(source);
70
+ return currentValue;
71
+ },
72
+ set(value) {
73
+ if (Object.is(currentValue, value)) {
74
+ return;
75
+ }
76
+ const previousValue = currentValue;
77
+ currentValue = value;
78
+ // 先通知响应式订阅者,让 computed/effect 重新计算;
79
+ // 再通知显式 subscribe(listener),让用户拿到稳定的新旧值。
80
+ notifySubscribers(subscribers);
81
+ notifyListeners(listeners, currentValue, previousValue);
82
+ },
83
+ subscribe(listener) {
84
+ listeners.add(listener);
85
+ return () => {
86
+ listeners.delete(listener);
87
+ };
88
+ }
89
+ };
90
+ }
91
+ export function computed(derive) {
92
+ let initialized = false;
93
+ let currentValue;
94
+ const subscribers = new Set();
95
+ const listeners = new Set();
96
+ const source = {
97
+ addSubscriber(subscriber) {
98
+ subscribers.add(subscriber);
99
+ },
100
+ removeSubscriber(subscriber) {
101
+ subscribers.delete(subscriber);
102
+ }
103
+ };
104
+ const computation = createComputation(() => {
105
+ const nextValue = derive();
106
+ if (initialized && Object.is(currentValue, nextValue)) {
107
+ return;
108
+ }
109
+ const previousValue = currentValue;
110
+ currentValue = nextValue;
111
+ const hadPreviousValue = initialized;
112
+ initialized = true;
113
+ if (hadPreviousValue) {
114
+ // computed 初次求值只是建立缓存和依赖,不应触发订阅者。
115
+ // 只有后续依赖变化且派生值真的改变时,才向下游传播。
116
+ notifySubscribers(subscribers);
117
+ notifyListeners(listeners, currentValue, previousValue);
118
+ }
119
+ });
120
+ // 创建 computed 时立即求值,这样它会马上收集依赖并拥有可同步读取的缓存值。
121
+ computation.run();
122
+ return {
123
+ get() {
124
+ // computed 本身也可以作为另一个 computed/effect 的依赖源。
125
+ trackDependency(source);
126
+ return currentValue;
127
+ },
128
+ subscribe(listener) {
129
+ listeners.add(listener);
130
+ return () => {
131
+ listeners.delete(listener);
132
+ };
133
+ }
134
+ };
135
+ }
136
+ export function effect(runEffect) {
137
+ let cleanup;
138
+ const computation = createComputation(() => {
139
+ if (cleanup) {
140
+ // effect 重新运行前先清理上一次运行创建的外部资源。
141
+ cleanup();
142
+ cleanup = undefined;
143
+ }
144
+ cleanup = runEffect();
145
+ });
146
+ // effect 的语义是“立即运行一次”,并在运行期间收集依赖。
147
+ computation.run();
148
+ return () => {
149
+ if (computation.disposed) {
150
+ return;
151
+ }
152
+ computation.disposed = true;
153
+ cleanupDependencies(computation);
154
+ if (cleanup) {
155
+ // dispose 时也要执行最后一次 cleanup,防止事件监听、定时器等资源泄漏。
156
+ cleanup();
157
+ cleanup = undefined;
158
+ }
159
+ };
160
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@bindtty/signal",
3
+ "version": "0.1.0-alpha.1",
4
+ "type": "module",
5
+ "description": "Signal primitives for BindTTY.",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "prepublishOnly": "npm run build",
21
+ "test": "npm run build && node --test test/*.test.js"
22
+ },
23
+ "devDependencies": {
24
+ "typescript": "^5.5.0"
25
+ },
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/lithdoo/BindTTY.git",
36
+ "directory": "packages/signal"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/lithdoo/BindTTY/issues"
40
+ },
41
+ "homepage": "https://github.com/lithdoo/BindTTY/tree/main/packages/signal#readme"
42
+ }