@planet-matrix/mobius-model 0.1.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/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./signal-core/index";
2
+ export * from "./signal-operators";
@@ -0,0 +1,4 @@
1
+ # Signals
2
+
3
+ - https://github.com/stackblitz/alien-signals
4
+ - https://github.com/johnsoncodehk/alien-signals-starter
@@ -0,0 +1,275 @@
1
+ export const FLAG_DICT = {
2
+ /**
3
+ * 当前节点没有任何特性。
4
+ */
5
+ None: 0,
6
+ /**
7
+ * 当前节点的值是可变的。值的变化可以通过此节点继续传递。
8
+ */
9
+ Mutable: 1,
10
+ /**
11
+ * 当前节点的值可能发生了变化,待确定。
12
+ */
13
+ Pending: 2,
14
+ /**
15
+ * 当前节点的值发生了变化。
16
+ */
17
+ Dirty: 4,
18
+ /**
19
+ * 当前节点正在进行观察。当上游节点的值发生变化时应该通知当前节点。
20
+ */
21
+ Watching: 8,
22
+
23
+ /**
24
+ * 当前节点正在进行依赖收集。
25
+ *
26
+ * 目前只考虑到以下用途:
27
+ *
28
+ * 1. 用于防止在某节点依赖收集过程中依赖发生变化导致该节点重复运行。
29
+ * ```
30
+ * const count = signal(0);
31
+ * effect(() => {
32
+ * count(count() + 1);
33
+ * })
34
+ * ```
35
+ */
36
+ Tracking: 16,
37
+ /**
38
+ * 当前节点正在进行依赖收集,且正在尝试复用现存连接。
39
+ */
40
+ TrackingReusing: 32,
41
+ } as const;
42
+
43
+ export const hasMutable = (flags: number): boolean => {
44
+ return (flags & FLAG_DICT.Mutable) !== 0;
45
+ }
46
+ export const setMutable = (flags?: number | undefined): number => {
47
+ return flags === undefined ? FLAG_DICT.Mutable : (flags | FLAG_DICT.Mutable);
48
+ }
49
+ export const unsetMutable = (flags: number): number => {
50
+ return flags & ~FLAG_DICT.Mutable;
51
+ }
52
+ export const toggleMutable = (flags: number): number => {
53
+ return flags ^ FLAG_DICT.Mutable;
54
+ }
55
+ export const hasDirty = (flags: number): boolean => {
56
+ return (flags & FLAG_DICT.Dirty) !== 0;
57
+ }
58
+ export const setDirty = (flags?: number | undefined): number => {
59
+ return flags === undefined ? FLAG_DICT.Dirty : (flags | FLAG_DICT.Dirty);
60
+ }
61
+ export const unsetDirty = (flags: number): number => {
62
+ return flags & ~FLAG_DICT.Dirty;
63
+ }
64
+ export const toggleDirty = (flags: number): number => {
65
+ return flags ^ FLAG_DICT.Dirty;
66
+ }
67
+ export const hasPending = (flags: number): boolean => {
68
+ return (flags & FLAG_DICT.Pending) !== 0;
69
+ }
70
+ export const setPending = (flags?: number | undefined): number => {
71
+ return flags === undefined ? FLAG_DICT.Pending : (flags | FLAG_DICT.Pending);
72
+ }
73
+ export const unsetPending = (flags: number): number => {
74
+ return flags & ~FLAG_DICT.Pending;
75
+ }
76
+ export const togglePending = (flags: number): number => {
77
+ return flags ^ FLAG_DICT.Pending;
78
+ }
79
+ export const hasWatching = (flags: number): boolean => {
80
+ return (flags & FLAG_DICT.Watching) !== 0;
81
+ }
82
+ export const setWatching = (flags?: number | undefined): number => {
83
+ return flags === undefined ? FLAG_DICT.Watching : (flags | FLAG_DICT.Watching);
84
+ }
85
+ export const unsetWatching = (flags: number): number => {
86
+ return flags & ~FLAG_DICT.Watching;
87
+ }
88
+ export const toggleWatching = (flags: number): number => {
89
+ return flags ^ FLAG_DICT.Watching;
90
+ }
91
+ export const hasTracking = (flags: number): boolean => {
92
+ return (flags & FLAG_DICT.Tracking) !== 0;
93
+ }
94
+ export const setTracking = (flags?: number | undefined): number => {
95
+ return flags === undefined ? FLAG_DICT.Tracking : (flags | FLAG_DICT.Tracking);
96
+ }
97
+ export const unsetTracking = (flags: number): number => {
98
+ return flags & ~FLAG_DICT.Tracking;
99
+ }
100
+ export const toggleTracking = (flags: number): number => {
101
+ return flags ^ FLAG_DICT.Tracking;
102
+ }
103
+ export const hasTrackingReusing = (flags: number): boolean => {
104
+ return (flags & FLAG_DICT.TrackingReusing) !== 0;
105
+ }
106
+ export const setTrackingReusing = (flags?: number): number => {
107
+ return flags === undefined ? FLAG_DICT.TrackingReusing : (flags | FLAG_DICT.TrackingReusing);
108
+ }
109
+ export const unsetTrackingReusing = (flags: number): number => {
110
+ return flags & ~FLAG_DICT.TrackingReusing;
111
+ }
112
+ export const toggleTrackingReusing = (flags: number): number => {
113
+ return flags ^ FLAG_DICT.TrackingReusing;
114
+ }
115
+
116
+ export class Flags {
117
+ private flags: number;
118
+
119
+ constructor() {
120
+ this.flags = FLAG_DICT.None;
121
+ }
122
+
123
+ static clone(flags: Flags): Flags {
124
+ const newFlags = new Flags();
125
+ newFlags.set(flags.get());
126
+ return newFlags;
127
+ }
128
+
129
+ get(): number {
130
+ return this.flags;
131
+ }
132
+
133
+ set(flags: number): this {
134
+ this.flags = flags;
135
+ return this;
136
+ }
137
+
138
+ clear(): this {
139
+ this.flags = FLAG_DICT.None;
140
+ return this;
141
+ }
142
+
143
+ hasMutable(): boolean {
144
+ return hasMutable(this.flags);
145
+ }
146
+ setMutable(): this {
147
+ this.flags = setMutable(this.flags);
148
+ return this;
149
+ }
150
+ unsetMutable(): this {
151
+ this.flags = unsetMutable(this.flags);
152
+ return this;
153
+ }
154
+ toggleMutable(): this {
155
+ this.flags = toggleMutable(this.flags);
156
+ return this;
157
+ }
158
+
159
+ hasDirty(): boolean {
160
+ return hasDirty(this.flags);
161
+ }
162
+ setDirty(): this {
163
+ this.flags = setDirty(this.flags);
164
+ return this;
165
+ }
166
+ unsetDirty(): this {
167
+ this.flags = unsetDirty(this.flags);
168
+ return this;
169
+ }
170
+ toggleDirty(): this {
171
+ this.flags = toggleDirty(this.flags);
172
+ return this;
173
+ }
174
+
175
+ hasPending(): boolean {
176
+ return hasPending(this.flags);
177
+ }
178
+ setPending(): this {
179
+ this.flags = setPending(this.flags);
180
+ return this;
181
+ }
182
+ unsetPending(): this {
183
+ this.flags = unsetPending(this.flags);
184
+ return this;
185
+ }
186
+ togglePending(): this {
187
+ this.flags = togglePending(this.flags);
188
+ return this;
189
+ }
190
+
191
+ hasWatching(): boolean {
192
+ return hasWatching(this.flags);
193
+ }
194
+ setWatching(): this {
195
+ this.flags = setWatching(this.flags);
196
+ return this;
197
+ }
198
+ unsetWatching(): this {
199
+ this.flags = unsetWatching(this.flags);
200
+ return this;
201
+ }
202
+ toggleWatching(): this {
203
+ this.flags = toggleWatching(this.flags);
204
+ return this;
205
+ }
206
+
207
+ hasTracking(): boolean {
208
+ return hasTracking(this.flags);
209
+ }
210
+ setTracking(): this {
211
+ this.flags = setTracking(this.flags);
212
+ return this;
213
+ }
214
+ unsetTracking(): this {
215
+ this.flags = unsetTracking(this.flags);
216
+ return this;
217
+ }
218
+ toggleTracking(): this {
219
+ this.flags = toggleTracking(this.flags);
220
+ return this;
221
+ }
222
+
223
+ hasTrackingReusing(): boolean {
224
+ return hasTrackingReusing(this.flags);
225
+ }
226
+ setTrackingReusing(): this {
227
+ this.flags = setTrackingReusing(this.flags);
228
+ return this;
229
+ }
230
+ unsetTrackingReusing(): this {
231
+ this.flags = unsetTrackingReusing(this.flags);
232
+ return this;
233
+ }
234
+ toggleTrackingReusing(): this {
235
+ this.flags = toggleTrackingReusing(this.flags);
236
+ return this;
237
+ }
238
+
239
+ toString(): string {
240
+ if (this.flags === FLAG_DICT.None) {
241
+ return 'Flags(None)';
242
+ }
243
+
244
+ const activeFlags: string[] = [];
245
+
246
+ if (this.hasMutable()) {
247
+ activeFlags.push('Mutable')
248
+ };
249
+ if (this.hasPending()) {
250
+ activeFlags.push('Pending')
251
+ };
252
+ if (this.hasDirty()) {
253
+ activeFlags.push('Dirty')
254
+ };
255
+ if (this.hasWatching()) {
256
+ activeFlags.push('Watching')
257
+ };
258
+ if (this.hasTracking()) {
259
+ activeFlags.push('Tracking')
260
+ };
261
+ if (this.hasTrackingReusing()) {
262
+ activeFlags.push('TrackingReusing')
263
+ };
264
+
265
+ return `Flags(${activeFlags.join(' | ')})`;
266
+ }
267
+ }
268
+
269
+ export const flags = (flags?: number | undefined): Flags => {
270
+ const f = new Flags();
271
+ if (flags !== undefined) {
272
+ f.set(flags);
273
+ }
274
+ return f;
275
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./flags"
2
+ export * from "./reactive-system"
3
+ export * from "./primitive"
@@ -0,0 +1,275 @@
1
+ import type { Flags } from "./flags"
2
+ import { createReactiveSystem } from "./reactive-system"
3
+ import { flags } from "./flags"
4
+ import type { Link, Node } from "./reactive-system"
5
+
6
+ const VOID = Symbol("void") as unknown;
7
+
8
+ const queue: Effect[] = [];
9
+ const flush = (): void => {
10
+ while (queue.length !== 0) {
11
+ queue.shift()!.run();
12
+ }
13
+ }
14
+
15
+ let batchDepth = 0;
16
+ export const startBatch = (): void => {
17
+ batchDepth = batchDepth + 1;
18
+ }
19
+ export const endBatch = (): void => {
20
+ batchDepth = batchDepth - 1;
21
+ if (batchDepth === 0) {
22
+ flush();
23
+ }
24
+ }
25
+
26
+ const reactiveSystem = createReactiveSystem({
27
+ update(node: Signal | Computed) {
28
+ return node.update();
29
+ },
30
+ notify(node: Effect) {
31
+ const nodes = []
32
+ let currentNode: Effect | undefined = node
33
+ while (currentNode !== undefined) {
34
+ if (currentNode.flags.hasWatching() === false) {
35
+ break
36
+ }
37
+ node.notify();
38
+ nodes.push(currentNode);
39
+ const nextNode: Node | undefined = currentNode?.headSubLink?.sub
40
+ currentNode = nextNode instanceof Effect ? nextNode : undefined
41
+ }
42
+ queue.push(...nodes.toReversed());
43
+ },
44
+ unwatched(node: Signal | Computed | Effect) {
45
+ return node.stop();
46
+ }
47
+ });
48
+ const {
49
+ unlinkAllDepLinksOfNode,
50
+
51
+ withTracking,
52
+ track,
53
+
54
+ shallowPropagate,
55
+ deeeeepPropagate,
56
+ resolvePending,
57
+ } = reactiveSystem
58
+
59
+ /**
60
+ * Signal 的值只有在被获取的时候才会更新。
61
+ */
62
+ export class Signal<T = unknown> implements Node {
63
+ headSubLink: Link | undefined;
64
+ tailSubLink: Link | undefined;
65
+ flags: Flags;
66
+
67
+ value: T;
68
+ pendingValue: T;
69
+
70
+ constructor(value: T) {
71
+ this.headSubLink = undefined;
72
+ this.tailSubLink = undefined;
73
+ this.flags = flags().setMutable();
74
+
75
+ this.pendingValue = value;
76
+ this.value = value;
77
+ }
78
+
79
+ get(): T {
80
+ let valueChanged = false
81
+
82
+ // 如果包含 Dirty 标记,则进行更新
83
+ const shouldUpdate = this.flags.hasDirty();
84
+ if (shouldUpdate === true) {
85
+ valueChanged = this.update()
86
+ }
87
+
88
+ // 如果值发生变化,则更新直接下游节点的标记
89
+ if (valueChanged === true) {
90
+ shallowPropagate(this);
91
+ }
92
+
93
+ // 自动进行依赖收集
94
+ track(this);
95
+
96
+ return this.value;
97
+ }
98
+
99
+ set(value: T): void {
100
+ // 将新值暂存起来,等用到的时候再执行更新
101
+ this.pendingValue = value;
102
+ // 为当前节点添加 Dirty 标记
103
+ this.flags.setDirty();
104
+
105
+ // 更新所有下游节点的标记
106
+ deeeeepPropagate(this);
107
+
108
+ // 执行更新队列
109
+ if (batchDepth === 0) {
110
+ flush();
111
+ }
112
+ }
113
+
114
+ /**
115
+ * 返回值表示更新前后值是否变化。
116
+ */
117
+ update(): boolean {
118
+ this.flags.unsetDirty();
119
+
120
+ const oldValue = this.value;
121
+ const newValue = this.pendingValue;
122
+ this.value = newValue;
123
+
124
+ return oldValue !== newValue;
125
+ }
126
+
127
+ stop(): void {
128
+ // do nothing
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Computed 的值只有在被获取的时候才会更新。
134
+ * Computed 的值只有在上游节点的值发生变化时才会更新。
135
+ * Computed 的下游节点全部解除连接时会自动与所有上游节点解除连接,并回到初始状态。
136
+ */
137
+ export class Computed<T = unknown> implements Node {
138
+ headDepLink: Link | undefined;
139
+ tailDepLink: Link | undefined;
140
+ headSubLink: Link | undefined;
141
+ tailSubLink: Link | undefined;
142
+ flags: Flags;
143
+
144
+ value: T;
145
+ getValue: () => T;
146
+
147
+ constructor(getValue: () => T) {
148
+ this.headDepLink = undefined;
149
+ this.tailDepLink = undefined;
150
+ this.headSubLink = undefined;
151
+ this.tailSubLink = undefined;
152
+ // 值在创建时尚未计算,因此初始时添加 Dirty 标记
153
+ this.flags = flags().setMutable().setDirty();
154
+
155
+ // undefined 可能是使用者期待的合法值,因此这里使用 VOID 占位
156
+ // oxlint-disable-next-line no-unsafe-type-assertion
157
+ this.value = VOID as T;
158
+ this.getValue = getValue;
159
+ }
160
+
161
+ get(): T {
162
+ let valueChanged = false;
163
+
164
+ // 如果包含 Pending 标记,则进行解决
165
+ const shouldResolve = this.flags.hasPending();
166
+ if (shouldResolve === true) {
167
+ resolvePending(this);
168
+ }
169
+
170
+ // 如果包含 Dirty 标记,则进行更新
171
+ const shouldUpdate = this.flags.hasDirty();
172
+ if (shouldUpdate === true) {
173
+ valueChanged = this.update()
174
+ }
175
+
176
+ // 如果值变化,则通知下游(仅通知直接下游)
177
+ if (valueChanged === true) {
178
+ shallowPropagate(this);
179
+ }
180
+
181
+ // 自动进行依赖收集
182
+ track(this);
183
+
184
+ return this.value;
185
+ }
186
+
187
+ /**
188
+ * 返回值表示更新前后值是否变化。
189
+ */
190
+ update(): boolean {
191
+ const result = withTracking(this, () => {
192
+ const oldValue = this.value;
193
+ const newValue = this.getValue();
194
+ this.value = newValue;
195
+ return oldValue !== newValue;
196
+ })
197
+ return result;
198
+ }
199
+
200
+ stop(): void {
201
+ unlinkAllDepLinksOfNode(this);
202
+ this.flags = flags().setMutable().setDirty();
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Effect 会在创建时立即执行一次传入的函数。
208
+ * Effect 会在每次上游节点的值发生变化时重新执行传入的函数。
209
+ * Effect 在任意时刻最多只能一个待运行。
210
+ * Effect 可以与 Effect 建立连接。
211
+ * 作为上游节点的 Effect 在没有下游节点时会自动与所有上游节点解除连接,并将标记清空。
212
+ */
213
+ export class Effect implements Node {
214
+ headDepLink: Link | undefined;
215
+ tailDepLink: Link | undefined;
216
+ headSubLink: Link | undefined;
217
+ tailSubLink: Link | undefined;
218
+ flags: Flags;
219
+
220
+ fn: () => void;
221
+
222
+ constructor(fn: () => void) {
223
+ this.headDepLink = undefined;
224
+ this.tailDepLink = undefined;
225
+ this.headSubLink = undefined;
226
+ this.tailSubLink = undefined;
227
+ // 在创建完成后即进入观察状态,因此初始时添加 Watching 标记
228
+ this.flags = flags().setWatching();
229
+
230
+ this.fn = fn;
231
+ this.internalRun();
232
+ }
233
+
234
+ notify(): void {
235
+ this.flags.unsetWatching();
236
+ }
237
+
238
+ run(): void {
239
+ this.flags.setWatching();
240
+
241
+ // 如果包含 Pending 标记,则进行解决
242
+ const shouldResolve = this.flags.hasPending();
243
+ if (shouldResolve === true) {
244
+ resolvePending(this);
245
+ }
246
+
247
+ // 如果包含 Dirty 标记,则进行更新
248
+ const shouldRun = this.flags.hasDirty();
249
+ if (shouldRun === true) {
250
+ this.internalRun();
251
+ }
252
+ }
253
+
254
+ private internalRun(): void {
255
+ track(this);
256
+ withTracking(this, () => {
257
+ return this.fn();
258
+ })
259
+ }
260
+
261
+ stop(): void {
262
+ unlinkAllDepLinksOfNode(this);
263
+ this.flags.clear();
264
+ }
265
+ }
266
+
267
+ export const signal = <T>(initialValue: T): Signal<T> => {
268
+ return new Signal(initialValue);
269
+ }
270
+ export const computed = <T>(getter: () => T): Computed<T> => {
271
+ return new Computed<T>(getter);
272
+ }
273
+ export const effect = (fn: () => void): Effect => {
274
+ return new Effect(fn);
275
+ }