signalium 0.2.2 → 0.2.4

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,11 @@
1
- import { scheduleDisconnect, scheduleWatcher } from './scheduling.js';
1
+ import { scheduleDirty, scheduleDisconnect, schedulePull, scheduleWatcher } from './scheduling.js';
2
+ import WeakRef from './weakref.js';
2
3
 
4
+ let CURRENT_ORD = 0;
3
5
  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
6
  let CURRENT_IS_WAITING: boolean = false;
8
- let CURRENT_SEEN: WeakSet<ComputedSignal<any>> | undefined;
9
7
 
10
- let id = 0;
8
+ let ID = 0;
11
9
 
12
10
  const enum SignalType {
13
11
  Computed,
@@ -61,188 +59,30 @@ const enum SignalState {
61
59
  const WAITING = Symbol();
62
60
 
63
61
  interface Link {
64
- id: number;
65
- sub: WeakRef<ComputedSignal<any>>;
66
62
  dep: ComputedSignal<any>;
63
+ sub: WeakRef<ComputedSignal<any>>;
67
64
  ord: number;
68
65
  version: number;
69
-
70
- nextDep: Link | undefined;
71
- nextSub: Link | undefined;
72
- prevSub: Link | undefined;
66
+ consumedAt: number;
73
67
 
74
68
  nextDirty: Link | undefined;
75
69
  }
76
70
 
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
71
  export class ComputedSignal<T> {
229
- id = id++;
72
+ _id = ID++;
230
73
  _type: SignalType;
231
74
 
232
- _subs: Link | undefined;
233
- _subsTail: Link | undefined;
234
-
235
- _deps: Link | undefined;
236
- _dirtyDep: Link | undefined;
75
+ _deps = new Map<ComputedSignal<any>, Link>();
237
76
 
77
+ _dirtyDep: Link | undefined = undefined;
78
+ _subs = new Set<Link>();
238
79
  _state: SignalState = SignalState.Dirty;
239
-
240
80
  _version: number = 0;
241
-
242
- _connectedCount: number;
243
-
81
+ _computedCount: number = 0;
82
+ _connectedCount: number = 0;
244
83
  _currentValue: T | AsyncResult<T> | undefined;
245
84
  _compute: SignalCompute<T> | SignalAsyncCompute<T> | SignalSubscribe<T> | undefined;
85
+
246
86
  _equals: SignalEquals<T>;
247
87
  _ref: WeakRef<ComputedSignal<T>> = new WeakRef(this);
248
88
 
@@ -286,7 +126,7 @@ export class ComputedSignal<T> {
286
126
 
287
127
  if (value.isPending) {
288
128
  const currentConsumer = CURRENT_CONSUMER;
289
- ACTIVE_ASYNCS.get(this)?.finally(() => currentConsumer._check());
129
+ ACTIVE_ASYNCS.get(this)?.finally(() => schedulePull(currentConsumer));
290
130
 
291
131
  CURRENT_IS_WAITING = true;
292
132
  throw WAITING;
@@ -300,45 +140,33 @@ export class ComputedSignal<T> {
300
140
  }
301
141
 
302
142
  get(): T | AsyncResult<T> {
303
- let prevTracked = false;
143
+ if (CURRENT_CONSUMER !== undefined) {
144
+ const { _deps: deps, _computedCount: computedCount, _connectedCount: connectedCount } = CURRENT_CONSUMER;
145
+ const prevLink = deps.get(this);
304
146
 
305
- if (CURRENT_CONSUMER !== undefined && this._type !== SignalType.Watcher && !CURRENT_SEEN!.has(this)) {
306
147
  const ord = CURRENT_ORD++;
307
148
 
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;
149
+ this._check(!prevLink && connectedCount > 0);
150
+
151
+ if (prevLink === undefined) {
152
+ const newLink = {
153
+ dep: this,
154
+ sub: CURRENT_CONSUMER._ref,
155
+ ord,
156
+ version: this._version,
157
+ consumedAt: CURRENT_CONSUMER._computedCount,
158
+ nextDirty: undefined,
159
+ };
160
+
161
+ deps.set(this, newLink);
162
+ this._subs.add(newLink);
163
+ } else if (prevLink.consumedAt !== computedCount) {
164
+ prevLink.ord = ord;
165
+ prevLink.version = this._version;
166
+ prevLink.consumedAt = computedCount;
167
+ // prevLink.nextDirty = undefined;
168
+ this._subs.add(prevLink);
332
169
  }
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
170
  } else {
343
171
  this._check();
344
172
  }
@@ -347,6 +175,7 @@ export class ComputedSignal<T> {
347
175
  }
348
176
 
349
177
  _check(shouldWatch = false): number {
178
+ // COUNTS.checks++;
350
179
  let state = this._state;
351
180
  let connectedCount = this._connectedCount;
352
181
 
@@ -361,19 +190,11 @@ export class ComputedSignal<T> {
361
190
  if (this._type === SignalType.Subscription) {
362
191
  state = SignalState.Dirty;
363
192
  } 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
-
193
+ for (const [dep, link] of this._deps) {
371
194
  if (link.version !== dep._check(true)) {
372
195
  state = SignalState.Dirty;
373
196
  break;
374
197
  }
375
-
376
- link = link.nextDep;
377
198
  }
378
199
  }
379
200
  }
@@ -398,7 +219,7 @@ export class ComputedSignal<T> {
398
219
  }
399
220
 
400
221
  if (state === SignalState.Dirty) {
401
- this._run(wasConnected, connectedCount > 0, shouldConnect);
222
+ this._run(wasConnected, shouldConnect);
402
223
  } else {
403
224
  this._resetDirty();
404
225
  }
@@ -409,22 +230,16 @@ export class ComputedSignal<T> {
409
230
  return this._version;
410
231
  }
411
232
 
412
- _run(wasConnected: boolean, isConnected: boolean, shouldConnect: boolean) {
233
+ _run(wasConnected: boolean, shouldConnect: boolean) {
413
234
  const { _type: type } = this;
414
235
 
415
236
  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
237
 
421
238
  try {
422
239
  // eslint-disable-next-line @typescript-eslint/no-this-alias
423
240
  CURRENT_CONSUMER = this;
424
- CURRENT_ORD = 0;
425
- CURRENT_SEEN = new WeakSet();
426
- CURRENT_DEP_TAIL = undefined;
427
- CURRENT_IS_WATCHED = isConnected;
241
+
242
+ this._computedCount++;
428
243
 
429
244
  switch (type) {
430
245
  case SignalType.Computed: {
@@ -491,7 +306,7 @@ export class ComputedSignal<T> {
491
306
  value.isSuccess = true;
492
307
 
493
308
  this._version++;
494
- this._dirtyConsumers();
309
+ scheduleDirty(this);
495
310
  },
496
311
  error => {
497
312
  if (currentVersion !== this._version || error === WAITING) {
@@ -502,7 +317,7 @@ export class ComputedSignal<T> {
502
317
  value.isPending = false;
503
318
  value.isError = true;
504
319
  this._version++;
505
- this._dirtyConsumers();
320
+ scheduleDirty(this);
506
321
  },
507
322
  );
508
323
 
@@ -552,44 +367,37 @@ export class ComputedSignal<T> {
552
367
  }
553
368
  }
554
369
  } finally {
555
- endTrack(this, wasConnected);
370
+ const deps = this._deps;
371
+
372
+ for (const link of deps.values()) {
373
+ if (link.consumedAt === this._computedCount) continue;
374
+
375
+ const dep = link.dep;
376
+
377
+ if (wasConnected) {
378
+ scheduleDisconnect(dep);
379
+ }
380
+
381
+ deps.delete(dep);
382
+ dep._subs.delete(link);
383
+ }
556
384
 
557
385
  CURRENT_CONSUMER = prevConsumer;
558
- CURRENT_SEEN = prevSeen;
559
- CURRENT_DEP_TAIL = prevDepTail;
560
- CURRENT_ORD = prevOrd;
561
- CURRENT_IS_WATCHED = prevIsWatched;
562
386
  }
563
387
  }
564
388
 
565
389
  _resetDirty() {
566
390
  let dirty = this._dirtyDep;
391
+ // COUNTS.dirtyResetIterations++;
567
392
 
568
393
  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
- }
394
+ // COUNTS.dirtyResetIterations++;
395
+ dirty.dep._subs.add(dirty);
586
396
 
587
397
  let nextDirty = dirty.nextDirty;
588
398
  dirty.nextDirty = undefined;
589
399
  dirty = nextDirty;
590
400
  }
591
-
592
- if (process.env.NODE_ENV !== 'production') checkForCircularLinks(this._dirtyDep);
593
401
  }
594
402
 
595
403
  _dirty() {
@@ -604,61 +412,43 @@ export class ComputedSignal<T> {
604
412
  } else {
605
413
  this._dirtyConsumers();
606
414
  }
607
-
608
- this._subs = undefined;
609
415
  }
610
416
 
611
417
  _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;
418
+ for (const link of this._subs.values()) {
419
+ const sub = link.sub.deref();
420
+
421
+ if (sub === undefined) continue;
422
+
423
+ switch (sub._state) {
424
+ case SignalState.MaybeDirty: {
425
+ let dirty = sub._dirtyDep;
426
+ const ord = link.ord;
427
+ if (dirty!.ord > ord) {
428
+ sub._dirtyDep = link;
429
+ link.nextDirty = dirty;
430
+ } else {
431
+ let nextDirty = dirty!.nextDirty;
432
+ while (nextDirty !== undefined && nextDirty!.ord < ord) {
433
+ // COUNTS.dirtyInsertIterations++;
434
+ dirty = nextDirty;
435
+ nextDirty = dirty.nextDirty;
436
+ }
437
+ link.nextDirty = nextDirty;
438
+ dirty!.nextDirty = link;
647
439
  }
648
-
649
- link.nextDirty = nextDirty;
650
- dirty!.nextDirty = link;
440
+ break;
441
+ }
442
+ case SignalState.Clean: {
443
+ sub._state = SignalState.MaybeDirty;
444
+ sub._dirtyDep = link;
445
+ link.nextDirty = undefined;
446
+ sub._dirty();
651
447
  }
652
- } else {
653
- // consumer._dirtyQueueLength = dirtyQueueLength + 2;
654
- consumer._state = SignalState.MaybeDirty;
655
- consumer._dirtyDep = link;
656
- link.nextDirty = undefined;
657
- consumer._dirty();
658
448
  }
659
-
660
- link = link.nextSub;
661
449
  }
450
+
451
+ this._subs = new Set();
662
452
  }
663
453
 
664
454
  _disconnect(count = 1) {
@@ -679,14 +469,10 @@ export class ComputedSignal<T> {
679
469
  }
680
470
  }
681
471
 
682
- let link = this._deps;
683
-
684
- while (link !== undefined) {
472
+ for (const link of this._deps.values()) {
685
473
  const dep = link.dep;
686
474
 
687
475
  dep._disconnect();
688
-
689
- link = link.nextDep;
690
476
  }
691
477
  }
692
478
  }
@@ -719,7 +505,7 @@ export interface AsyncReady<T> extends AsyncBaseResult<T> {
719
505
  export type AsyncResult<T> = AsyncPending<T> | AsyncReady<T>;
720
506
 
721
507
  class StateSignal<T> implements StateSignal<T> {
722
- private _consumers: WeakRef<ComputedSignal<unknown>>[] = [];
508
+ private _subs: WeakRef<ComputedSignal<unknown>>[] = [];
723
509
 
724
510
  constructor(
725
511
  private _value: T,
@@ -728,7 +514,7 @@ class StateSignal<T> implements StateSignal<T> {
728
514
 
729
515
  get(): T {
730
516
  if (CURRENT_CONSUMER !== undefined) {
731
- this._consumers.push(CURRENT_CONSUMER._ref);
517
+ this._subs.push(CURRENT_CONSUMER._ref);
732
518
  }
733
519
 
734
520
  return this._value!;
@@ -740,21 +526,28 @@ class StateSignal<T> implements StateSignal<T> {
740
526
  }
741
527
 
742
528
  this._value = value;
529
+ const subs = this._subs;
530
+ const subsLength = subs.length;
743
531
 
744
- const { _consumers: consumers } = this;
745
-
746
- for (const consumerRef of consumers) {
747
- const consumer = consumerRef.deref();
532
+ for (let i = 0; i < subsLength; i++) {
533
+ const sub = subs[i].deref();
748
534
 
749
- if (consumer === undefined) {
535
+ if (sub === undefined) {
750
536
  continue;
751
537
  }
752
538
 
753
- consumer._state = SignalState.Dirty;
754
- consumer._dirty();
539
+ switch (sub._state) {
540
+ case SignalState.Clean:
541
+ sub._state = SignalState.Dirty;
542
+ sub._dirty();
543
+ break;
544
+ case SignalState.MaybeDirty:
545
+ sub._state = SignalState.Dirty;
546
+ break;
547
+ }
755
548
  }
756
549
 
757
- consumers.length = 0;
550
+ this._subs = [];
758
551
  }
759
552
  }
760
553
 
@@ -790,7 +583,7 @@ export interface Watcher {
790
583
  }
791
584
 
792
585
  export function watcher(fn: () => void): Watcher {
793
- const subscribers = new Set<() => void>();
586
+ const subscribers: (() => void)[] = [];
794
587
  const watcher = new ComputedSignal(SignalType.Watcher, () => {
795
588
  fn();
796
589
 
@@ -809,10 +602,10 @@ export function watcher(fn: () => void): Watcher {
809
602
  },
810
603
 
811
604
  subscribe(subscriber: () => void) {
812
- subscribers.add(subscriber);
605
+ subscribers.push(subscriber);
813
606
 
814
607
  return () => {
815
- subscribers.delete(subscriber);
608
+ subscribers.splice(subscribers.indexOf(subscriber), 1);
816
609
  };
817
610
  },
818
611
  };
@@ -824,19 +617,13 @@ export function isTracking(): boolean {
824
617
 
825
618
  export function untrack<T = void>(fn: () => T): T {
826
619
  const prevConsumer = CURRENT_CONSUMER;
827
- const prevOrd = CURRENT_ORD;
828
- const prevIsWatched = CURRENT_IS_WATCHED;
829
620
 
830
621
  try {
831
622
  CURRENT_CONSUMER = undefined;
832
623
  // LAST_CONSUMED = undefined;
833
- CURRENT_ORD = 0;
834
- CURRENT_IS_WATCHED = false;
835
624
 
836
625
  return fn();
837
626
  } finally {
838
627
  CURRENT_CONSUMER = prevConsumer;
839
- CURRENT_ORD = prevOrd;
840
- CURRENT_IS_WATCHED = prevIsWatched;
841
628
  }
842
629
  }
package/src/weakref.ts ADDED
@@ -0,0 +1,9 @@
1
+ class WeakRefPolyfill<T extends WeakKey> {
2
+ constructor(private value: T) {}
3
+
4
+ deref(): T {
5
+ return this.value;
6
+ }
7
+ }
8
+
9
+ export default typeof WeakRef === 'function' ? WeakRef : (WeakRefPolyfill as unknown as WeakRefConstructor);