@esportsplus/reactivity 0.4.7 → 0.5.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/src/signal.ts CHANGED
@@ -1,372 +1,447 @@
1
- import { isArray } from '@esportsplus/utilities';
2
- import { CHECK, CLEAN, COMPUTED, DIRTY, DISPOSED, EFFECT, ROOT, SIGNAL } from './constants';
3
- import { Computed, Changed, Effect, Event, Function, Listener, NeverAsync, Options, Root, Scheduler, Signal, State, Type } from './types';
4
-
5
-
6
- let index = 0,
7
- observer: Reactive<any> | null = null,
8
- observers: Reactive<any>[] | null = null,
9
- scope: Root | null = null;
10
-
11
-
12
- class Reactive<T> {
13
- changed: Changed | null = null;
14
- fn: Computed<T>['fn'] | Effect['fn'] | null = null;
15
- listeners: Record<Event, (Listener<any> | null)[]> | null = null;
16
- observers: Reactive<any>[] | null = null;
17
- root: Root | null;
18
- scheduler: Scheduler | null = null;
19
- sources: Reactive<any>[] | null = null;
20
- state: State;
21
- task: Function | null = null;
22
- tracking: boolean | null = null;
23
- type: Type;
24
- value: T;
25
-
26
-
27
- constructor(state: State, type: Type, value: T) {
28
- let root = null;
29
-
30
- if (type !== ROOT) {
31
- if (scope !== null) {
32
- root = scope;
33
- }
34
- else if (observer !== null) {
35
- root = observer.root;
36
- }
1
+ import { isArray, isObject } from '@esportsplus/utilities';
2
+ import { REACTIVE, STATE_CHECK, STATE_DIRTY, STATE_IN_HEAP, STATE_NONE, STATE_RECOMPUTING } from './constants';
3
+ import { Computed, Link, Signal, } from './types';
37
4
 
38
- if (root == null) {
39
- if (type === EFFECT) {
40
- throw new Error(`@esportsplus/reactivity: 'effect' cannot be created without a reactive root`);
41
- }
42
- }
43
- else if (root.tracking) {
44
- root.on('dispose', () => this.dispose());
45
- }
46
- }
47
5
 
48
- this.root = root;
49
- this.state = state;
50
- this.type = type;
51
- this.value = value;
52
- }
6
+ let context: Computed<unknown> | null = null,
7
+ dirtyHeap: (Computed<unknown> | undefined)[] = new Array(2000),
8
+ maxDirty = 0,
9
+ markedHeap = false,
10
+ minDirty = 0;
53
11
 
54
12
 
55
- dispatch<D>(event: Event, data?: D) {
56
- if (this.listeners === null || this.listeners[event] === undefined) {
57
- return;
13
+ function cleanup(node: Computed<unknown>): void {
14
+ if (!node.cleanup) {
15
+ return;
16
+ }
17
+
18
+ if (isArray(node.cleanup)) {
19
+ for (let i = 0; i < node.cleanup.length; i++) {
20
+ node.cleanup[i]();
58
21
  }
22
+ }
23
+ else {
24
+ node.cleanup();
25
+ }
59
26
 
60
- let listeners = this.listeners[event];
27
+ node.cleanup = null;
28
+ }
61
29
 
62
- for (let i = 0, n = listeners.length; i < n; i++) {
63
- let listener = listeners[i];
30
+ function deleteFromHeap(n: Computed<unknown>) {
31
+ let state = n.state;
64
32
 
65
- if (listener === null) {
66
- continue;
67
- }
33
+ if (!(state & STATE_IN_HEAP)) {
34
+ return;
35
+ }
68
36
 
69
- try {
70
- listener(data, this.value);
37
+ n.state = state & ~STATE_IN_HEAP;
71
38
 
72
- if (listener.once !== undefined) {
73
- listeners[i] = null;
74
- }
75
- }
76
- catch {
77
- listeners[i] = null;
78
- }
79
- }
39
+ let height = n.height;
40
+
41
+ if (n.prevHeap === n) {
42
+ dirtyHeap[height] = undefined;
80
43
  }
44
+ else {
45
+ let next = n.nextHeap,
46
+ dhh = dirtyHeap[height]!,
47
+ end = next ?? dhh;
81
48
 
82
- dispose() {
83
- if (this.state === DISPOSED) {
84
- return;
49
+ if (n === dhh) {
50
+ dirtyHeap[height] = next;
51
+ }
52
+ else {
53
+ n.prevHeap.nextHeap = next;
85
54
  }
86
55
 
87
- this.dispatch('cleanup', this);
88
- this.dispatch('dispose', this);
56
+ end.prevHeap = n.prevHeap;
57
+ }
89
58
 
90
- removeSourceObservers(this, 0);
59
+ n.nextHeap = undefined;
60
+ n.prevHeap = n;
61
+ }
62
+
63
+ function insertIntoHeap(n: Computed<unknown>) {
64
+ let state = n.state;
91
65
 
92
- this.listeners = null;
93
- this.observers = null;
94
- this.sources = null;
95
- this.state = DISPOSED;
66
+ if (state & STATE_IN_HEAP) {
67
+ return;
96
68
  }
97
69
 
98
- get() {
99
- if (this.state === DISPOSED) {
100
- return this.value;
101
- }
70
+ n.state = state | STATE_IN_HEAP;
102
71
 
103
- if (observer !== null) {
104
- if (observers === null) {
105
- if (observer.sources !== null && observer.sources[index] == this) {
106
- index++;
107
- }
108
- else {
109
- observers = [this];
110
- }
111
- }
112
- else {
113
- observers.push(this);
114
- }
115
- }
72
+ let height = n.height,
73
+ heapAtHeight = dirtyHeap[height];
116
74
 
117
- if (this.type === COMPUTED || this.type === EFFECT) {
118
- sync(this);
119
- }
75
+ if (heapAtHeight === undefined) {
76
+ dirtyHeap[height] = n;
77
+ }
78
+ else {
79
+ let tail = heapAtHeight.prevHeap;
120
80
 
121
- return this.value;
81
+ tail.nextHeap = n;
82
+ n.prevHeap = tail;
83
+ heapAtHeight.prevHeap = n;
122
84
  }
123
85
 
124
- on<T>(event: Event, listener: Listener<T>) {
125
- if (this.state === DIRTY) {
126
- if (event !== 'cleanup') {
127
- throw new Error(`@esportsplus/reactivity: events set within computed or effects must use the 'cleanup' event name`);
128
- }
86
+ if (height > maxDirty) {
87
+ maxDirty = height;
129
88
 
130
- listener.once = true;
89
+ // Simple auto adjust to avoid manual management within apps.
90
+ if (height >= dirtyHeap.length) {
91
+ dirtyHeap.length += 250;
131
92
  }
93
+ }
94
+ }
132
95
 
133
- if (this.listeners === null) {
134
- this.listeners = { [event]: [listener] };
135
- }
136
- else {
137
- let listeners = this.listeners[event];
96
+ // https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L52
97
+ function link(dep: Signal<unknown> | Computed<unknown>, sub: Computed<unknown>) {
98
+ let prevDep = sub.depsTail;
138
99
 
139
- if (listeners === undefined) {
140
- this.listeners[event] = [listener];
141
- }
142
- else if (listeners.indexOf(listener) === -1) {
143
- let i = listeners.indexOf(null);
144
-
145
- if (i === -1) {
146
- listeners.push(listener);
147
- }
148
- else {
149
- listeners[i] = listener;
150
- }
151
- }
152
- }
100
+ if (prevDep !== null && prevDep.dep === dep) {
101
+ return;
153
102
  }
154
103
 
155
- once<T>(event: Event, listener: Listener<T>) {
156
- listener.once = true;
157
- this.on(event, listener);
158
- }
104
+ let nextDep: Link | null = null;
159
105
 
160
- set(value: T): T {
161
- if (this.type !== SIGNAL && observer !== this) {
162
- throw new Error(`@esportsplus/reactivity: 'set' method is only available on signals`);
163
- }
106
+ if (sub.state & STATE_RECOMPUTING) {
107
+ nextDep = prevDep !== null ? prevDep.nextDep : sub.deps;
164
108
 
165
- if (this.changed!(this.value, value)) {
166
- this.value = value;
167
- notify(this.observers, DIRTY);
109
+ if (nextDep !== null && nextDep.dep === dep) {
110
+ sub.depsTail = nextDep;
111
+ return;
168
112
  }
169
-
170
- return this.value;
171
113
  }
172
- }
173
114
 
115
+ let prevSub = dep.subsTail,
116
+ newLink =
117
+ sub.depsTail =
118
+ dep.subsTail = {
119
+ dep,
120
+ sub,
121
+ nextDep,
122
+ prevSub,
123
+ nextSub: null,
124
+ };
125
+
126
+ if (prevDep !== null) {
127
+ prevDep.nextDep = newLink;
128
+ }
129
+ else {
130
+ sub.deps = newLink;
131
+ }
174
132
 
175
- function changed(a: unknown, b: unknown) {
176
- return a !== b;
133
+ if (prevSub !== null) {
134
+ prevSub.nextSub = newLink;
135
+ }
136
+ else {
137
+ dep.subs = newLink;
138
+ }
177
139
  }
178
140
 
179
- function notify<T>(nodes: Reactive<T>[] | null, state: typeof CHECK | typeof DIRTY) {
180
- if (nodes === null) {
141
+ function markHeap() {
142
+ if (markedHeap) {
181
143
  return;
182
144
  }
183
145
 
184
- for (let i = 0, n = nodes.length; i < n; i++) {
185
- let node = nodes[i];
146
+ markedHeap = true;
186
147
 
187
- if (node.state < state) {
188
- if (node.type === EFFECT && node.state === CLEAN) {
189
- (node as Effect).root.scheduler((node as Effect).task);
190
- }
191
-
192
- node.state = state;
193
- notify(node.observers, CHECK);
148
+ for (let i = 0; i <= maxDirty; i++) {
149
+ for (let el = dirtyHeap[i]; el !== undefined; el = el.nextHeap) {
150
+ markNode(el);
194
151
  }
195
152
  }
196
153
  }
197
154
 
198
- function removeSourceObservers<T>(node: Reactive<T>, start: number) {
199
- if (node.sources === null) {
155
+ function markNode(el: Computed<unknown>, newState = STATE_DIRTY) {
156
+ let state = el.state;
157
+
158
+ if ((state & (STATE_CHECK | STATE_DIRTY)) >= newState) {
200
159
  return;
201
160
  }
202
161
 
203
- for (let i = start, n = node.sources.length; i < n; i++) {
204
- let observers = node.sources[i].observers;
162
+ el.state = state | newState;
163
+
164
+ for (let link = el.subs; link !== null; link = link.nextSub) {
165
+ markNode(link.sub, STATE_CHECK);
166
+ }
167
+ }
168
+
169
+ function recompute(el: Computed<unknown>, del: boolean) {
170
+ if (del) {
171
+ deleteFromHeap(el);
172
+ }
173
+ else {
174
+ el.nextHeap = undefined;
175
+ el.prevHeap = el;
176
+ }
177
+
178
+ cleanup(el);
179
+
180
+ let oldcontext = context,
181
+ ok = true,
182
+ value;
205
183
 
206
- if (observers === null) {
207
- continue;
184
+ context = el;
185
+ el.depsTail = null;
186
+ el.state = STATE_RECOMPUTING;
187
+
188
+ try {
189
+ value = el.fn(oncleanup);
190
+ }
191
+ catch (e) {
192
+ ok = false;
193
+ }
194
+
195
+ context = oldcontext;
196
+ el.state = STATE_NONE;
197
+
198
+ let depsTail = el.depsTail as Link | null,
199
+ toRemove = depsTail !== null ? depsTail.nextDep : el.deps;
200
+
201
+ if (toRemove !== null) {
202
+ do {
203
+ toRemove = unlink(toRemove);
208
204
  }
205
+ while (toRemove !== null);
209
206
 
210
- observers[observers.indexOf(node)] = observers[observers.length - 1];
211
- observers.pop();
207
+ if (depsTail !== null) {
208
+ depsTail.nextDep = null;
209
+ }
210
+ else {
211
+ el.deps = null;
212
+ }
212
213
  }
213
- }
214
214
 
215
- function sync<T>(node: Reactive<T>) {
216
- if (node.state === CHECK && node.sources !== null) {
217
- for (let i = 0, n = node.sources.length; i < n; i++) {
218
- sync(node.sources[i]);
215
+ if (ok && value !== el.value) {
216
+ el.value = value;
219
217
 
220
- // Stop the loop here so we won't trigger updates on other parents unnecessarily
221
- // If our computation changes to no longer use some sources, we don't
222
- // want to update() a source we used last time, but now don't use.
223
- if ((node.state as State) === DIRTY) {
224
- break;
218
+ for (let s = el.subs; s !== null; s = s.nextSub) {
219
+ let o = s.sub,
220
+ state = o.state;
221
+
222
+ if (state & STATE_CHECK) {
223
+ o.state = state | STATE_DIRTY;
225
224
  }
225
+
226
+ insertIntoHeap(o);
226
227
  }
227
228
  }
229
+ }
228
230
 
229
- if (node.state === DIRTY) {
230
- update(node);
231
+ // https://github.com/stackblitz/alien-signals/blob/v2.0.3/src/system.ts#L100
232
+ function unlink(link: Link): Link | null {
233
+ let dep = link.dep,
234
+ nextDep = link.nextDep,
235
+ nextSub = link.nextSub,
236
+ prevSub = link.prevSub;
237
+
238
+ if (nextSub !== null) {
239
+ nextSub.prevSub = prevSub;
231
240
  }
232
241
  else {
233
- node.state = CLEAN;
242
+ dep.subsTail = prevSub;
234
243
  }
235
- }
236
244
 
237
- function update<T>(node: Reactive<T>) {
238
- let i = index,
239
- o = observer,
240
- os = observers;
241
-
242
- index = 0;
243
- observer = node;
244
- observers = null as typeof observers;
245
-
246
- try {
247
- node.dispatch('cleanup');
248
- node.dispatch('update');
245
+ if (prevSub !== null) {
246
+ prevSub.nextSub = nextSub;
247
+ }
248
+ else {
249
+ dep.subs = nextSub;
249
250
 
250
- // @ts-ignore
251
- let value = node.fn.call(null, node);
251
+ if (nextSub === null && 'fn' in dep) {
252
+ dispose(dep);
253
+ }
254
+ }
252
255
 
253
- if (observers) {
254
- removeSourceObservers(node, index);
256
+ return nextDep;
257
+ }
255
258
 
256
- if (node.sources !== null && index > 0) {
257
- node.sources.length = index + observers.length;
259
+ function update(el: Computed<unknown>): void {
260
+ if (el.state & STATE_CHECK) {
261
+ for (let d = el.deps; d; d = d.nextDep) {
262
+ let dep = d.dep;
258
263
 
259
- for (let i = 0, n = observers.length; i < n; i++) {
260
- node.sources[index + i] = observers[i];
261
- }
264
+ if ('fn' in dep) {
265
+ update(dep);
262
266
  }
263
- else {
264
- node.sources = observers;
267
+
268
+ if (el.state & STATE_DIRTY) {
269
+ break;
265
270
  }
271
+ }
272
+ }
266
273
 
267
- for (let i = index, n = node.sources.length; i < n; i++) {
268
- let source = node.sources[i];
274
+ if (el.state & STATE_DIRTY) {
275
+ recompute(el, true);
276
+ }
269
277
 
270
- if (source.observers === null) {
271
- source.observers = [node];
272
- }
273
- else {
274
- source.observers.push(node);
275
- }
276
- }
278
+ el.state = STATE_NONE;
279
+ }
280
+
281
+
282
+ const computed = <T>(fn: Computed<T>['fn']): Computed<T> => {
283
+ let self: Computed<T> = {
284
+ [REACTIVE]: true,
285
+ cleanup: null,
286
+ deps: null,
287
+ depsTail: null,
288
+ fn: fn,
289
+ height: 0,
290
+ nextHeap: undefined,
291
+ prevHeap: null as any,
292
+ state: STATE_NONE,
293
+ subs: null,
294
+ subsTail: null,
295
+ value: undefined as T,
296
+ };
297
+
298
+ self.prevHeap = self;
299
+
300
+ if (context) {
301
+ if (context.depsTail === null) {
302
+ self.height = context.height;
303
+ recompute(self, false);
277
304
  }
278
- else if (node.sources !== null && index < node.sources.length) {
279
- removeSourceObservers(node, index);
280
- node.sources.length = index;
305
+ else {
306
+ self.height = context.height + 1;
307
+ insertIntoHeap(self);
281
308
  }
282
309
 
283
- if (node.type === COMPUTED) {
284
- node.set(value as T);
285
- }
310
+ link(self, context);
286
311
  }
287
- finally {
288
- index = i;
289
- observer = o;
290
- observers = os;
312
+ else {
313
+ recompute(self, false);
291
314
  }
292
315
 
293
- node.state = CLEAN;
294
- }
316
+ return self;
317
+ };
295
318
 
319
+ const dispose = (el: Computed<unknown>) => {
320
+ deleteFromHeap(el);
296
321
 
297
- const computed = <T>(fn: Computed<T>['fn'], options?: Options) => {
298
- let instance = new Reactive(DIRTY, COMPUTED, undefined as T);
322
+ let dep = el.deps;
299
323
 
300
- instance.changed = options?.changed || changed;
301
- instance.fn = fn;
324
+ while (dep !== null) {
325
+ dep = unlink(dep);
326
+ }
302
327
 
303
- return instance as Computed<T>;
328
+ el.deps = null;
329
+
330
+ cleanup(el);
331
+ }
332
+
333
+ const isComputed = (value: unknown): value is Computed<unknown> => {
334
+ return isObject(value) && REACTIVE in value && 'fn' in value;
304
335
  };
305
336
 
306
- const dispose = <T extends { dispose: VoidFunction }>(dispose?: T[] | T | null) => {
307
- if (dispose == null) {
337
+ const isReactive = (value: unknown): value is Computed<unknown> | Signal<unknown> => {
338
+ return isObject(value) && REACTIVE in value;
339
+ };
340
+
341
+ const isSignal = (value: unknown): value is Signal<unknown> => {
342
+ return isObject(value) && REACTIVE in value && 'fn' in value === false;
343
+ };
344
+
345
+ const oncleanup = (fn: VoidFunction): typeof fn => {
346
+ if (!context) {
347
+ return fn;
308
348
  }
309
- else if (isArray(dispose)) {
310
- for (let i = 0, n = dispose.length; i < n; i++) {
311
- dispose[i].dispose();
312
- }
349
+
350
+ let node = context;
351
+
352
+ if (!node.cleanup) {
353
+ node.cleanup = fn;
354
+ }
355
+ else if (isArray(node.cleanup)) {
356
+ node.cleanup.push(fn);
313
357
  }
314
358
  else {
315
- dispose.dispose();
359
+ node.cleanup = [node.cleanup, fn];
316
360
  }
317
361
 
318
- return dispose;
362
+ return fn;
319
363
  };
320
364
 
321
- const effect = (fn: Effect['fn']) => {
322
- let instance = new Reactive(DIRTY, EFFECT, null);
365
+ const read = <T>(el: Signal<T> | Computed<T>): T => {
366
+ if (context) {
367
+ link(el, context);
323
368
 
324
- instance.fn = fn;
325
- instance.task = () => instance.get();
369
+ if ('fn' in el) {
370
+ let height = el.height;
326
371
 
327
- update(instance);
372
+ if (height >= context.height) {
373
+ context.height = height + 1;
374
+ }
328
375
 
329
- return instance as Effect;
376
+ if (
377
+ height >= minDirty ||
378
+ el.state & (STATE_DIRTY | STATE_CHECK)
379
+ ) {
380
+ markHeap();
381
+ update(el);
382
+ }
383
+ }
384
+ }
385
+
386
+ return el.value;
330
387
  };
331
388
 
332
- const root = <T>(fn: NeverAsync<(instance: Root) => T>, scheduler?: Scheduler) => {
333
- let o = observer,
334
- s = scope;
389
+ const root = <T>(fn: () => T) => {
390
+ let c = context;
335
391
 
336
- if (scheduler === undefined) {
337
- if (o?.type === EFFECT) {
338
- scope = o.root;
339
- }
392
+ context = null;
340
393
 
341
- if (scope === null) {
342
- throw new Error('@esportsplus/reactivity: `root` cannot be created without a task scheduler');
343
- }
394
+ let value = fn();
344
395
 
345
- scheduler = scope.scheduler;
346
- }
396
+ context = c;
347
397
 
348
- observer = null;
398
+ return value;
399
+ };
349
400
 
350
- scope = new Reactive(CLEAN, ROOT, null) as any as Root;
351
- scope.scheduler = scheduler;
352
- scope.tracking = fn.length > 0;
401
+ const signal = <T>(value: T): Signal<T> => {
402
+ return {
403
+ [REACTIVE]: true,
404
+ subs: null,
405
+ subsTail: null,
406
+ value,
407
+ };
408
+ };
353
409
 
354
- let result = fn.call(null, scope);
410
+ signal.set = (el: Signal<unknown>, v: unknown) => {
411
+ if (el.value === v) {
412
+ return;
413
+ }
355
414
 
356
- observer = o;
357
- scope = s;
415
+ el.value = v;
358
416
 
359
- return result;
417
+ for (let link = el.subs; link !== null; link = link.nextSub) {
418
+ markedHeap = false;
419
+ insertIntoHeap(link.sub);
420
+ }
360
421
  };
361
422
 
362
- const signal = <T>(value: T, options?: Options) => {
363
- let instance = new Reactive(CLEAN, SIGNAL, value);
423
+ const stabilize = () => {
424
+ for (minDirty = 0; minDirty <= maxDirty; minDirty++) {
425
+ let el = dirtyHeap[minDirty];
364
426
 
365
- instance.changed = options?.changed || changed;
427
+ dirtyHeap[minDirty] = undefined;
366
428
 
367
- return instance as Signal<T>;
429
+ while (el !== undefined) {
430
+ let next = el.nextHeap;
431
+
432
+ recompute(el, false);
433
+
434
+ el = next;
435
+ }
436
+ }
368
437
  };
369
438
 
370
439
 
371
- export { computed, dispose, effect, root, signal };
372
- export { Reactive };
440
+ export {
441
+ computed,
442
+ dispose,
443
+ isComputed, isReactive, isSignal,
444
+ oncleanup,
445
+ read, root,
446
+ signal, stabilize
447
+ };