signalium 0.2.2 → 0.2.3

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/signals.ts CHANGED
@@ -1,13 +1,10 @@
1
1
  import { scheduleDisconnect, scheduleWatcher } from './scheduling.js';
2
2
 
3
+ let CURRENT_ORD = 0;
3
4
  let CURRENT_CONSUMER: ComputedSignal<any> | undefined;
4
- let CURRENT_DEP_TAIL: Link | undefined;
5
- let CURRENT_ORD: number = 0;
6
- let CURRENT_IS_WATCHED: boolean = false;
7
5
  let CURRENT_IS_WAITING: boolean = false;
8
- let CURRENT_SEEN: WeakSet<ComputedSignal<any>> | undefined;
9
6
 
10
- let id = 0;
7
+ let ID = 0;
11
8
 
12
9
  const enum SignalType {
13
10
  Computed,
@@ -61,188 +58,30 @@ const enum SignalState {
61
58
  const WAITING = Symbol();
62
59
 
63
60
  interface Link {
64
- id: number;
65
- sub: WeakRef<ComputedSignal<any>>;
66
61
  dep: ComputedSignal<any>;
62
+ sub: WeakRef<ComputedSignal<any>>;
67
63
  ord: number;
68
64
  version: number;
69
-
70
- nextDep: Link | undefined;
71
- nextSub: Link | undefined;
72
- prevSub: Link | undefined;
65
+ consumedAt: number;
73
66
 
74
67
  nextDirty: Link | undefined;
75
68
  }
76
69
 
77
- let linkPool: Link | undefined;
78
-
79
- const checkForCircularLinks = (link: Link | undefined) => {
80
- if (!link) return;
81
-
82
- for (const key of ['nextDep', 'nextSub', 'prevSub', 'nextDirty'] as const) {
83
- let currentLink: Link | undefined = link?.[key];
84
-
85
- while (currentLink !== undefined) {
86
- if (currentLink === link) {
87
- throw new Error(
88
- `Circular link detected via ${key}. This is a bug, please report it to the Signalium maintainers.`,
89
- );
90
- }
91
-
92
- currentLink = currentLink[key];
93
- }
94
- }
95
- };
96
-
97
- const typeToString = (type: SignalType) => {
98
- switch (type) {
99
- case SignalType.Computed:
100
- return 'Computed';
101
- case SignalType.Subscription:
102
- return 'Subscription';
103
- case SignalType.Async:
104
- return 'Async';
105
- case SignalType.Watcher:
106
- return 'Watcher';
107
- }
108
- };
109
-
110
- const printComputed = (computed: ComputedSignal<any>) => {
111
- const type = typeToString(computed._type);
112
-
113
- return `ComputedSignal<${type}:${computed.id}>`;
114
- };
115
-
116
- const printLink = (link: Link) => {
117
- const sub = link.sub.deref();
118
- const subStr = sub === undefined ? 'undefined' : printComputed(sub);
119
- const depStr = printComputed(link.dep);
120
-
121
- return `Link<${link.id}> sub(${subStr}) -> dep(${depStr})`;
122
- };
123
-
124
- function linkNewDep(
125
- dep: ComputedSignal<any>,
126
- sub: ComputedSignal<any>,
127
- nextDep: Link | undefined,
128
- depsTail: Link | undefined,
129
- ord: number,
130
- ): Link {
131
- let newLink: Link;
132
-
133
- if (linkPool !== undefined) {
134
- newLink = linkPool;
135
- linkPool = newLink.nextDep;
136
- newLink.nextDep = nextDep;
137
- newLink.dep = dep;
138
- newLink.sub = sub._ref;
139
- newLink.ord = ord;
140
- } else {
141
- newLink = {
142
- id: id++,
143
- dep,
144
- sub: sub._ref,
145
- ord,
146
- version: 0,
147
- nextDep,
148
- nextDirty: undefined,
149
- prevSub: undefined,
150
- nextSub: undefined,
151
- };
152
- }
153
-
154
- if (depsTail === undefined) {
155
- sub._deps = newLink;
156
- } else {
157
- depsTail.nextDep = newLink;
158
- }
159
-
160
- if (dep._subs === undefined) {
161
- dep._subs = newLink;
162
- } else {
163
- const oldTail = dep._subsTail!;
164
- newLink.prevSub = oldTail;
165
- oldTail.nextSub = newLink;
166
- }
167
-
168
- dep._subsTail = newLink;
169
-
170
- return newLink;
171
- }
172
-
173
- function poolLink(link: Link) {
174
- const dep = link.dep;
175
- const nextSub = link.nextSub;
176
- const prevSub = link.prevSub;
177
-
178
- if (nextSub !== undefined) {
179
- nextSub.prevSub = prevSub;
180
- link.nextSub = undefined;
181
- } else {
182
- dep._subsTail = prevSub;
183
- }
184
-
185
- if (prevSub !== undefined) {
186
- prevSub.nextSub = nextSub;
187
- link.prevSub = undefined;
188
- } else {
189
- dep._subs = nextSub;
190
- }
191
-
192
- // @ts-expect-error - override to pool the value
193
- link.dep = undefined;
194
- // @ts-expect-error - override to pool the value
195
- link.sub = undefined;
196
- link.nextDep = linkPool;
197
- linkPool = link;
198
-
199
- link.prevSub = undefined;
200
- }
201
-
202
- export function endTrack(sub: ComputedSignal<any>, shouldDisconnect: boolean): void {
203
- if (CURRENT_DEP_TAIL !== undefined) {
204
- if (CURRENT_DEP_TAIL.nextDep !== undefined) {
205
- clearTrack(CURRENT_DEP_TAIL.nextDep, shouldDisconnect);
206
- CURRENT_DEP_TAIL.nextDep = undefined;
207
- }
208
- } else if (sub._deps !== undefined) {
209
- clearTrack(sub._deps, shouldDisconnect);
210
- sub._deps = undefined;
211
- }
212
- }
213
-
214
- function clearTrack(link: Link, shouldDisconnect: boolean): void {
215
- do {
216
- const nextDep = link.nextDep;
217
-
218
- if (shouldDisconnect) {
219
- scheduleDisconnect(link.dep);
220
- }
221
-
222
- poolLink(link);
223
-
224
- link = nextDep!;
225
- } while (link !== undefined);
226
- }
227
-
228
70
  export class ComputedSignal<T> {
229
- id = id++;
71
+ _id = ID++;
230
72
  _type: SignalType;
231
73
 
232
- _subs: Link | undefined;
233
- _subsTail: Link | undefined;
234
-
235
- _deps: Link | undefined;
236
- _dirtyDep: Link | undefined;
74
+ _deps = new Map<ComputedSignal<any>, Link>();
237
75
 
76
+ _dirtyDep: Link | undefined = undefined;
77
+ _subs = new Set<Link>();
238
78
  _state: SignalState = SignalState.Dirty;
239
-
240
79
  _version: number = 0;
241
-
242
- _connectedCount: number;
243
-
80
+ _computedCount: number = 0;
81
+ _connectedCount: number = 0;
244
82
  _currentValue: T | AsyncResult<T> | undefined;
245
83
  _compute: SignalCompute<T> | SignalAsyncCompute<T> | SignalSubscribe<T> | undefined;
84
+
246
85
  _equals: SignalEquals<T>;
247
86
  _ref: WeakRef<ComputedSignal<T>> = new WeakRef(this);
248
87
 
@@ -300,45 +139,33 @@ export class ComputedSignal<T> {
300
139
  }
301
140
 
302
141
  get(): T | AsyncResult<T> {
303
- let prevTracked = false;
142
+ if (CURRENT_CONSUMER !== undefined) {
143
+ const { _deps: deps, _computedCount: computedCount, _connectedCount: connectedCount } = CURRENT_CONSUMER;
144
+ const prevLink = deps.get(this);
304
145
 
305
- if (CURRENT_CONSUMER !== undefined && this._type !== SignalType.Watcher && !CURRENT_SEEN!.has(this)) {
306
146
  const ord = CURRENT_ORD++;
307
147
 
308
- const nextDep = CURRENT_DEP_TAIL === undefined ? CURRENT_CONSUMER._deps : CURRENT_DEP_TAIL.nextDep;
309
- let newLink: Link | undefined = nextDep;
310
-
311
- while (newLink !== undefined) {
312
- if (newLink.dep === this) {
313
- prevTracked = true;
314
-
315
- if (CURRENT_DEP_TAIL === undefined) {
316
- CURRENT_CONSUMER._deps = newLink;
317
- } else {
318
- CURRENT_DEP_TAIL.nextDep = newLink;
319
- }
320
-
321
- newLink.ord = ord;
322
- newLink.nextDirty = undefined;
323
-
324
- if (this._subs === undefined) {
325
- this._subs = newLink;
326
- }
327
-
328
- break;
329
- }
330
-
331
- newLink = newLink.nextDep;
148
+ this._check(!prevLink && connectedCount > 0);
149
+
150
+ if (prevLink === undefined) {
151
+ const newLink = {
152
+ dep: this,
153
+ sub: CURRENT_CONSUMER._ref,
154
+ ord,
155
+ version: this._version,
156
+ consumedAt: CURRENT_CONSUMER._computedCount,
157
+ nextDirty: undefined,
158
+ };
159
+
160
+ deps.set(this, newLink);
161
+ this._subs.add(newLink);
162
+ } else if (prevLink.consumedAt !== computedCount) {
163
+ prevLink.ord = ord;
164
+ prevLink.version = this._version;
165
+ prevLink.consumedAt = computedCount;
166
+ // prevLink.nextDirty = undefined;
167
+ this._subs.add(prevLink);
332
168
  }
333
-
334
- this._check(CURRENT_IS_WATCHED && !prevTracked);
335
-
336
- CURRENT_DEP_TAIL = newLink ?? linkNewDep(this, CURRENT_CONSUMER, nextDep, CURRENT_DEP_TAIL, ord);
337
-
338
- if (process.env.NODE_ENV !== 'production') checkForCircularLinks(CURRENT_DEP_TAIL);
339
-
340
- CURRENT_DEP_TAIL.version = this._version;
341
- CURRENT_SEEN!.add(this);
342
169
  } else {
343
170
  this._check();
344
171
  }
@@ -347,6 +174,7 @@ export class ComputedSignal<T> {
347
174
  }
348
175
 
349
176
  _check(shouldWatch = false): number {
177
+ // COUNTS.checks++;
350
178
  let state = this._state;
351
179
  let connectedCount = this._connectedCount;
352
180
 
@@ -361,19 +189,11 @@ export class ComputedSignal<T> {
361
189
  if (this._type === SignalType.Subscription) {
362
190
  state = SignalState.Dirty;
363
191
  } else {
364
- let link = this._deps;
365
-
366
- if (process.env.NODE_ENV !== 'production') checkForCircularLinks(link);
367
-
368
- while (link !== undefined) {
369
- const dep = link.dep;
370
-
192
+ for (const [dep, link] of this._deps) {
371
193
  if (link.version !== dep._check(true)) {
372
194
  state = SignalState.Dirty;
373
195
  break;
374
196
  }
375
-
376
- link = link.nextDep;
377
197
  }
378
198
  }
379
199
  }
@@ -398,7 +218,7 @@ export class ComputedSignal<T> {
398
218
  }
399
219
 
400
220
  if (state === SignalState.Dirty) {
401
- this._run(wasConnected, connectedCount > 0, shouldConnect);
221
+ this._run(wasConnected, shouldConnect);
402
222
  } else {
403
223
  this._resetDirty();
404
224
  }
@@ -409,22 +229,16 @@ export class ComputedSignal<T> {
409
229
  return this._version;
410
230
  }
411
231
 
412
- _run(wasConnected: boolean, isConnected: boolean, shouldConnect: boolean) {
232
+ _run(wasConnected: boolean, shouldConnect: boolean) {
413
233
  const { _type: type } = this;
414
234
 
415
235
  const prevConsumer = CURRENT_CONSUMER;
416
- const prevOrd = CURRENT_ORD;
417
- const prevSeen = CURRENT_SEEN;
418
- const prevDepTail = CURRENT_DEP_TAIL;
419
- const prevIsWatched = CURRENT_IS_WATCHED;
420
236
 
421
237
  try {
422
238
  // eslint-disable-next-line @typescript-eslint/no-this-alias
423
239
  CURRENT_CONSUMER = this;
424
- CURRENT_ORD = 0;
425
- CURRENT_SEEN = new WeakSet();
426
- CURRENT_DEP_TAIL = undefined;
427
- CURRENT_IS_WATCHED = isConnected;
240
+
241
+ this._computedCount++;
428
242
 
429
243
  switch (type) {
430
244
  case SignalType.Computed: {
@@ -552,44 +366,37 @@ export class ComputedSignal<T> {
552
366
  }
553
367
  }
554
368
  } finally {
555
- endTrack(this, wasConnected);
369
+ const deps = this._deps;
370
+
371
+ for (const link of deps.values()) {
372
+ if (link.consumedAt === this._computedCount) continue;
373
+
374
+ const dep = link.dep;
375
+
376
+ if (wasConnected) {
377
+ scheduleDisconnect(dep);
378
+ }
379
+
380
+ deps.delete(dep);
381
+ dep._subs.delete(link);
382
+ }
556
383
 
557
384
  CURRENT_CONSUMER = prevConsumer;
558
- CURRENT_SEEN = prevSeen;
559
- CURRENT_DEP_TAIL = prevDepTail;
560
- CURRENT_ORD = prevOrd;
561
- CURRENT_IS_WATCHED = prevIsWatched;
562
385
  }
563
386
  }
564
387
 
565
388
  _resetDirty() {
566
389
  let dirty = this._dirtyDep;
390
+ // COUNTS.dirtyResetIterations++;
567
391
 
568
392
  while (dirty !== undefined) {
569
- const dep = dirty.dep;
570
- const oldHead = dep._subs;
571
-
572
- if (oldHead === undefined) {
573
- dep._subs = dirty;
574
- dirty.nextSub = undefined;
575
- dirty.prevSub = undefined;
576
- } else {
577
- dirty.nextSub = oldHead;
578
- dirty.prevSub = undefined;
579
- oldHead.prevSub = dirty;
580
- dep._subs = dirty;
581
- }
582
-
583
- if (process.env.NODE_ENV !== 'production') {
584
- checkForCircularLinks(this._dirtyDep);
585
- }
393
+ // COUNTS.dirtyResetIterations++;
394
+ dirty.dep._subs.add(dirty);
586
395
 
587
396
  let nextDirty = dirty.nextDirty;
588
397
  dirty.nextDirty = undefined;
589
398
  dirty = nextDirty;
590
399
  }
591
-
592
- if (process.env.NODE_ENV !== 'production') checkForCircularLinks(this._dirtyDep);
593
400
  }
594
401
 
595
402
  _dirty() {
@@ -604,61 +411,43 @@ export class ComputedSignal<T> {
604
411
  } else {
605
412
  this._dirtyConsumers();
606
413
  }
607
-
608
- this._subs = undefined;
609
414
  }
610
415
 
611
416
  _dirtyConsumers() {
612
- let link = this._subs;
613
-
614
- if (process.env.NODE_ENV !== 'production') checkForCircularLinks(link);
615
-
616
- while (link !== undefined) {
617
- const consumer = link.sub.deref();
618
-
619
- if (consumer === undefined) {
620
- const nextSub = link.nextSub;
621
- poolLink(link);
622
- link = nextSub;
623
- continue;
624
- }
625
-
626
- const state = consumer._state;
627
-
628
- if (state === SignalState.Dirty) {
629
- const nextSub = link.nextSub;
630
- link = nextSub;
631
- continue;
632
- }
633
-
634
- if (state === SignalState.MaybeDirty) {
635
- let dirty = consumer._dirtyDep;
636
- const ord = link.ord;
637
-
638
- if (dirty!.ord > ord) {
639
- consumer._dirtyDep = link;
640
- link.nextDirty = dirty;
641
- } else {
642
- let nextDirty = dirty!.nextDirty;
643
-
644
- while (nextDirty !== undefined && nextDirty!.ord < ord) {
645
- dirty = nextDirty;
646
- nextDirty = dirty.nextDirty;
417
+ for (const link of this._subs.values()) {
418
+ const sub = link.sub.deref();
419
+
420
+ if (sub === undefined) continue;
421
+
422
+ switch (sub._state) {
423
+ case SignalState.MaybeDirty: {
424
+ let dirty = sub._dirtyDep;
425
+ const ord = link.ord;
426
+ if (dirty!.ord > ord) {
427
+ sub._dirtyDep = link;
428
+ link.nextDirty = dirty;
429
+ } else {
430
+ let nextDirty = dirty!.nextDirty;
431
+ while (nextDirty !== undefined && nextDirty!.ord < ord) {
432
+ // COUNTS.dirtyInsertIterations++;
433
+ dirty = nextDirty;
434
+ nextDirty = dirty.nextDirty;
435
+ }
436
+ link.nextDirty = nextDirty;
437
+ dirty!.nextDirty = link;
647
438
  }
648
-
649
- link.nextDirty = nextDirty;
650
- dirty!.nextDirty = link;
439
+ break;
440
+ }
441
+ case SignalState.Clean: {
442
+ sub._state = SignalState.MaybeDirty;
443
+ sub._dirtyDep = link;
444
+ link.nextDirty = undefined;
445
+ sub._dirty();
651
446
  }
652
- } else {
653
- // consumer._dirtyQueueLength = dirtyQueueLength + 2;
654
- consumer._state = SignalState.MaybeDirty;
655
- consumer._dirtyDep = link;
656
- link.nextDirty = undefined;
657
- consumer._dirty();
658
447
  }
659
-
660
- link = link.nextSub;
661
448
  }
449
+
450
+ this._subs = new Set();
662
451
  }
663
452
 
664
453
  _disconnect(count = 1) {
@@ -679,14 +468,10 @@ export class ComputedSignal<T> {
679
468
  }
680
469
  }
681
470
 
682
- let link = this._deps;
683
-
684
- while (link !== undefined) {
471
+ for (const link of this._deps.values()) {
685
472
  const dep = link.dep;
686
473
 
687
474
  dep._disconnect();
688
-
689
- link = link.nextDep;
690
475
  }
691
476
  }
692
477
  }
@@ -719,7 +504,7 @@ export interface AsyncReady<T> extends AsyncBaseResult<T> {
719
504
  export type AsyncResult<T> = AsyncPending<T> | AsyncReady<T>;
720
505
 
721
506
  class StateSignal<T> implements StateSignal<T> {
722
- private _consumers: WeakRef<ComputedSignal<unknown>>[] = [];
507
+ private _subs: WeakRef<ComputedSignal<unknown>>[] = [];
723
508
 
724
509
  constructor(
725
510
  private _value: T,
@@ -728,7 +513,7 @@ class StateSignal<T> implements StateSignal<T> {
728
513
 
729
514
  get(): T {
730
515
  if (CURRENT_CONSUMER !== undefined) {
731
- this._consumers.push(CURRENT_CONSUMER._ref);
516
+ this._subs.push(CURRENT_CONSUMER._ref);
732
517
  }
733
518
 
734
519
  return this._value!;
@@ -740,21 +525,28 @@ class StateSignal<T> implements StateSignal<T> {
740
525
  }
741
526
 
742
527
  this._value = value;
528
+ const subs = this._subs;
529
+ const subsLength = subs.length;
743
530
 
744
- const { _consumers: consumers } = this;
745
-
746
- for (const consumerRef of consumers) {
747
- const consumer = consumerRef.deref();
531
+ for (let i = 0; i < subsLength; i++) {
532
+ const sub = subs[i].deref();
748
533
 
749
- if (consumer === undefined) {
534
+ if (sub === undefined) {
750
535
  continue;
751
536
  }
752
537
 
753
- consumer._state = SignalState.Dirty;
754
- consumer._dirty();
538
+ switch (sub._state) {
539
+ case SignalState.Clean:
540
+ sub._state = SignalState.Dirty;
541
+ sub._dirty();
542
+ break;
543
+ case SignalState.MaybeDirty:
544
+ sub._state = SignalState.Dirty;
545
+ break;
546
+ }
755
547
  }
756
548
 
757
- consumers.length = 0;
549
+ this._subs = [];
758
550
  }
759
551
  }
760
552
 
@@ -790,7 +582,7 @@ export interface Watcher {
790
582
  }
791
583
 
792
584
  export function watcher(fn: () => void): Watcher {
793
- const subscribers = new Set<() => void>();
585
+ const subscribers: (() => void)[] = [];
794
586
  const watcher = new ComputedSignal(SignalType.Watcher, () => {
795
587
  fn();
796
588
 
@@ -809,10 +601,10 @@ export function watcher(fn: () => void): Watcher {
809
601
  },
810
602
 
811
603
  subscribe(subscriber: () => void) {
812
- subscribers.add(subscriber);
604
+ subscribers.push(subscriber);
813
605
 
814
606
  return () => {
815
- subscribers.delete(subscriber);
607
+ subscribers.splice(subscribers.indexOf(subscriber), 1);
816
608
  };
817
609
  },
818
610
  };
@@ -824,19 +616,13 @@ export function isTracking(): boolean {
824
616
 
825
617
  export function untrack<T = void>(fn: () => T): T {
826
618
  const prevConsumer = CURRENT_CONSUMER;
827
- const prevOrd = CURRENT_ORD;
828
- const prevIsWatched = CURRENT_IS_WATCHED;
829
619
 
830
620
  try {
831
621
  CURRENT_CONSUMER = undefined;
832
622
  // LAST_CONSUMED = undefined;
833
- CURRENT_ORD = 0;
834
- CURRENT_IS_WATCHED = false;
835
623
 
836
624
  return fn();
837
625
  } finally {
838
626
  CURRENT_CONSUMER = prevConsumer;
839
- CURRENT_ORD = prevOrd;
840
- CURRENT_IS_WATCHED = prevIsWatched;
841
627
  }
842
628
  }