signalium 0.1.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/CHANGELOG.md +11 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +19 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/scheduling.d.ts +3 -0
- package/dist/scheduling.js +36 -0
- package/dist/signals.d.ts +103 -0
- package/dist/signals.js +474 -0
- package/package.json +31 -0
- package/src/__tests__/async.test.ts +497 -0
- package/src/__tests__/basic.test.ts +378 -0
- package/src/__tests__/subscription.test.ts +646 -0
- package/src/__tests__/utils/instrumented.ts +327 -0
- package/src/config.ts +27 -0
- package/src/index.ts +17 -0
- package/src/scheduling.ts +54 -0
- package/src/signals.ts +689 -0
- package/tsconfig.json +10 -0
package/src/signals.ts
ADDED
@@ -0,0 +1,689 @@
|
|
1
|
+
import { scheduleDisconnect, scheduleWatcher } from './scheduling.js';
|
2
|
+
|
3
|
+
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
|
+
let CURRENT_SEEN: WeakSet<ComputedSignal<any>> | undefined;
|
8
|
+
|
9
|
+
let id = 0;
|
10
|
+
|
11
|
+
const enum SignalType {
|
12
|
+
Computed,
|
13
|
+
Subscription,
|
14
|
+
Async,
|
15
|
+
Watcher,
|
16
|
+
}
|
17
|
+
|
18
|
+
export interface Signal<T = unknown> {
|
19
|
+
get(): T;
|
20
|
+
}
|
21
|
+
|
22
|
+
export interface WriteableSignal<T> extends Signal<T> {
|
23
|
+
set(value: T): void;
|
24
|
+
}
|
25
|
+
|
26
|
+
export type SignalCompute<T> = (prev: T | undefined) => T;
|
27
|
+
|
28
|
+
export type SignalAsyncCompute<T> = (prev: T | undefined) => T | Promise<T>;
|
29
|
+
|
30
|
+
export type SignalWatcherEffect = () => void;
|
31
|
+
|
32
|
+
export type SignalEquals<T> = (prev: T, next: T) => boolean;
|
33
|
+
|
34
|
+
export type SignalSubscription = {
|
35
|
+
update?(): void;
|
36
|
+
unsubscribe?(): void;
|
37
|
+
};
|
38
|
+
|
39
|
+
export type SignalSubscribe<T> = (get: () => T, set: (value: T) => void) => SignalSubscription | undefined | void;
|
40
|
+
|
41
|
+
export interface SignalOptions<T> {
|
42
|
+
equals?: SignalEquals<T>;
|
43
|
+
}
|
44
|
+
|
45
|
+
export interface SignalOptionsWithInit<T> extends SignalOptions<T> {
|
46
|
+
initValue: T;
|
47
|
+
}
|
48
|
+
|
49
|
+
const SUBSCRIPTIONS = new WeakMap<ComputedSignal<any>, SignalSubscription | undefined | void>();
|
50
|
+
|
51
|
+
const enum SignalState {
|
52
|
+
Clean,
|
53
|
+
MaybeDirty,
|
54
|
+
Dirty,
|
55
|
+
}
|
56
|
+
|
57
|
+
interface Link {
|
58
|
+
sub: WeakRef<ComputedSignal<any>>;
|
59
|
+
dep: ComputedSignal<any>;
|
60
|
+
ord: number;
|
61
|
+
version: number;
|
62
|
+
|
63
|
+
nextDep: Link | undefined;
|
64
|
+
nextSub: Link | undefined;
|
65
|
+
prevSub: Link | undefined;
|
66
|
+
|
67
|
+
nextDirty: Link | undefined;
|
68
|
+
}
|
69
|
+
|
70
|
+
let linkPool: Link | undefined;
|
71
|
+
|
72
|
+
function linkNewDep(
|
73
|
+
dep: ComputedSignal<any>,
|
74
|
+
sub: ComputedSignal<any>,
|
75
|
+
nextDep: Link | undefined,
|
76
|
+
depsTail: Link | undefined,
|
77
|
+
ord: number,
|
78
|
+
): Link {
|
79
|
+
let newLink: Link;
|
80
|
+
|
81
|
+
if (linkPool !== undefined) {
|
82
|
+
newLink = linkPool;
|
83
|
+
linkPool = newLink.nextDep;
|
84
|
+
newLink.nextDep = nextDep;
|
85
|
+
newLink.dep = dep;
|
86
|
+
newLink.sub = sub._ref;
|
87
|
+
newLink.ord = ord;
|
88
|
+
} else {
|
89
|
+
newLink = {
|
90
|
+
dep,
|
91
|
+
sub: sub._ref,
|
92
|
+
ord,
|
93
|
+
version: 0,
|
94
|
+
nextDep,
|
95
|
+
nextDirty: undefined,
|
96
|
+
prevSub: undefined,
|
97
|
+
nextSub: undefined,
|
98
|
+
};
|
99
|
+
}
|
100
|
+
|
101
|
+
if (depsTail === undefined) {
|
102
|
+
sub._deps = newLink;
|
103
|
+
} else {
|
104
|
+
depsTail.nextDep = newLink;
|
105
|
+
}
|
106
|
+
|
107
|
+
if (dep._subs === undefined) {
|
108
|
+
dep._subs = newLink;
|
109
|
+
} else {
|
110
|
+
const oldTail = dep._subsTail!;
|
111
|
+
newLink.prevSub = oldTail;
|
112
|
+
oldTail.nextSub = newLink;
|
113
|
+
}
|
114
|
+
|
115
|
+
dep._subsTail = newLink;
|
116
|
+
|
117
|
+
return newLink;
|
118
|
+
}
|
119
|
+
|
120
|
+
function poolLink(link: Link) {
|
121
|
+
const dep = link.dep;
|
122
|
+
const nextSub = link.nextSub;
|
123
|
+
const prevSub = link.prevSub;
|
124
|
+
|
125
|
+
if (nextSub !== undefined) {
|
126
|
+
nextSub.prevSub = prevSub;
|
127
|
+
link.nextSub = undefined;
|
128
|
+
} else {
|
129
|
+
dep._subsTail = prevSub;
|
130
|
+
}
|
131
|
+
|
132
|
+
if (prevSub !== undefined) {
|
133
|
+
prevSub.nextSub = nextSub;
|
134
|
+
link.prevSub = undefined;
|
135
|
+
} else {
|
136
|
+
dep._subs = nextSub;
|
137
|
+
}
|
138
|
+
|
139
|
+
// @ts-expect-error - override to pool the value
|
140
|
+
link.dep = undefined;
|
141
|
+
// @ts-expect-error - override to pool the value
|
142
|
+
link.sub = undefined;
|
143
|
+
link.nextDep = linkPool;
|
144
|
+
linkPool = link;
|
145
|
+
|
146
|
+
link.prevSub = undefined;
|
147
|
+
}
|
148
|
+
|
149
|
+
export function endTrack(sub: ComputedSignal<any>, shouldDisconnect: boolean): void {
|
150
|
+
if (CURRENT_DEP_TAIL !== undefined) {
|
151
|
+
if (CURRENT_DEP_TAIL.nextDep !== undefined) {
|
152
|
+
clearTrack(CURRENT_DEP_TAIL.nextDep, shouldDisconnect);
|
153
|
+
CURRENT_DEP_TAIL.nextDep = undefined;
|
154
|
+
}
|
155
|
+
} else if (sub._deps !== undefined) {
|
156
|
+
clearTrack(sub._deps, shouldDisconnect);
|
157
|
+
sub._deps = undefined;
|
158
|
+
}
|
159
|
+
}
|
160
|
+
|
161
|
+
function clearTrack(link: Link, shouldDisconnect: boolean): void {
|
162
|
+
do {
|
163
|
+
const nextDep = link.nextDep;
|
164
|
+
|
165
|
+
if (shouldDisconnect) {
|
166
|
+
scheduleDisconnect(link.dep);
|
167
|
+
}
|
168
|
+
|
169
|
+
poolLink(link);
|
170
|
+
|
171
|
+
link = nextDep!;
|
172
|
+
} while (link !== undefined);
|
173
|
+
}
|
174
|
+
|
175
|
+
// const registry = new FinalizationRegistry(poolLink);
|
176
|
+
|
177
|
+
export class ComputedSignal<T> {
|
178
|
+
id = id++;
|
179
|
+
_type: SignalType;
|
180
|
+
|
181
|
+
_subs: Link | undefined;
|
182
|
+
_subsTail: Link | undefined;
|
183
|
+
|
184
|
+
_deps: Link | undefined;
|
185
|
+
_dirtyDep: Link | undefined;
|
186
|
+
|
187
|
+
_state: SignalState = SignalState.Dirty;
|
188
|
+
|
189
|
+
_version: number = 0;
|
190
|
+
|
191
|
+
_connectedCount: number;
|
192
|
+
|
193
|
+
_currentValue: T | AsyncResult<T> | undefined;
|
194
|
+
_compute: SignalCompute<T> | SignalAsyncCompute<T> | SignalSubscribe<T> | undefined;
|
195
|
+
_equals: SignalEquals<T>;
|
196
|
+
_ref: WeakRef<ComputedSignal<T>> = new WeakRef(this);
|
197
|
+
|
198
|
+
constructor(
|
199
|
+
type: SignalType,
|
200
|
+
compute: SignalCompute<T> | SignalAsyncCompute<T> | SignalSubscribe<T> | undefined,
|
201
|
+
equals?: SignalEquals<T>,
|
202
|
+
initValue?: T,
|
203
|
+
) {
|
204
|
+
this._type = type;
|
205
|
+
this._compute = compute;
|
206
|
+
this._equals = equals ?? ((a, b) => a === b);
|
207
|
+
this._currentValue = initValue;
|
208
|
+
this._connectedCount = type === SignalType.Watcher ? 1 : 0;
|
209
|
+
}
|
210
|
+
|
211
|
+
get(): T | AsyncResult<T> {
|
212
|
+
let prevTracked = false;
|
213
|
+
|
214
|
+
if (CURRENT_CONSUMER !== undefined && this._type !== SignalType.Watcher && !CURRENT_SEEN!.has(this)) {
|
215
|
+
const ord = CURRENT_ORD++;
|
216
|
+
|
217
|
+
const nextDep = CURRENT_DEP_TAIL === undefined ? CURRENT_CONSUMER._deps : CURRENT_DEP_TAIL.nextDep;
|
218
|
+
let newLink: Link | undefined = nextDep;
|
219
|
+
|
220
|
+
while (newLink !== undefined) {
|
221
|
+
if (newLink.dep === this) {
|
222
|
+
prevTracked = true;
|
223
|
+
|
224
|
+
if (CURRENT_DEP_TAIL === undefined) {
|
225
|
+
CURRENT_CONSUMER._deps = newLink;
|
226
|
+
} else {
|
227
|
+
CURRENT_DEP_TAIL.nextDep = newLink;
|
228
|
+
}
|
229
|
+
|
230
|
+
newLink.ord = ord;
|
231
|
+
newLink.nextDirty = undefined;
|
232
|
+
|
233
|
+
if (this._subs === undefined) {
|
234
|
+
this._subs = newLink;
|
235
|
+
}
|
236
|
+
|
237
|
+
break;
|
238
|
+
}
|
239
|
+
|
240
|
+
newLink = newLink.nextDep;
|
241
|
+
}
|
242
|
+
|
243
|
+
this._check(CURRENT_IS_WATCHED && !prevTracked);
|
244
|
+
|
245
|
+
CURRENT_DEP_TAIL = newLink ?? linkNewDep(this, CURRENT_CONSUMER, nextDep, CURRENT_DEP_TAIL, ord);
|
246
|
+
|
247
|
+
CURRENT_DEP_TAIL.version = this._version;
|
248
|
+
CURRENT_SEEN!.add(this);
|
249
|
+
} else {
|
250
|
+
this._check();
|
251
|
+
}
|
252
|
+
|
253
|
+
return this._currentValue!;
|
254
|
+
}
|
255
|
+
|
256
|
+
_check(shouldWatch = false): number {
|
257
|
+
let state = this._state;
|
258
|
+
let connectedCount = this._connectedCount;
|
259
|
+
|
260
|
+
const wasConnected = connectedCount > 0;
|
261
|
+
const shouldConnect = shouldWatch && !wasConnected;
|
262
|
+
|
263
|
+
if (shouldWatch) {
|
264
|
+
this._connectedCount = connectedCount = connectedCount + 1;
|
265
|
+
}
|
266
|
+
|
267
|
+
if (shouldConnect) {
|
268
|
+
if (this._type === SignalType.Subscription) {
|
269
|
+
state = SignalState.Dirty;
|
270
|
+
} else {
|
271
|
+
let link = this._deps;
|
272
|
+
|
273
|
+
while (link !== undefined) {
|
274
|
+
const dep = link.dep;
|
275
|
+
|
276
|
+
if (link.version !== dep._check(true)) {
|
277
|
+
state = SignalState.Dirty;
|
278
|
+
break;
|
279
|
+
}
|
280
|
+
|
281
|
+
link = link.nextDep;
|
282
|
+
}
|
283
|
+
}
|
284
|
+
|
285
|
+
this._resetDirty();
|
286
|
+
}
|
287
|
+
|
288
|
+
if (state === SignalState.Clean) {
|
289
|
+
return this._version;
|
290
|
+
}
|
291
|
+
|
292
|
+
if (state === SignalState.MaybeDirty) {
|
293
|
+
let dirty = this._dirtyDep;
|
294
|
+
|
295
|
+
while (dirty !== undefined) {
|
296
|
+
const dep = dirty.dep;
|
297
|
+
|
298
|
+
if (dirty.version !== dep._check()) {
|
299
|
+
state = SignalState.Dirty;
|
300
|
+
break;
|
301
|
+
}
|
302
|
+
|
303
|
+
dirty = dirty.nextDirty;
|
304
|
+
}
|
305
|
+
}
|
306
|
+
|
307
|
+
if (state === SignalState.Dirty) {
|
308
|
+
this._run(wasConnected, connectedCount > 0, shouldConnect);
|
309
|
+
} else {
|
310
|
+
this._resetDirty();
|
311
|
+
}
|
312
|
+
|
313
|
+
this._state = SignalState.Clean;
|
314
|
+
this._dirtyDep = undefined;
|
315
|
+
|
316
|
+
return this._version;
|
317
|
+
}
|
318
|
+
|
319
|
+
_run(wasConnected: boolean, isConnected: boolean, shouldConnect: boolean) {
|
320
|
+
const { _type: type } = this;
|
321
|
+
|
322
|
+
const prevConsumer = CURRENT_CONSUMER;
|
323
|
+
const prevOrd = CURRENT_ORD;
|
324
|
+
const prevSeen = CURRENT_SEEN;
|
325
|
+
const prevDepTail = CURRENT_DEP_TAIL;
|
326
|
+
const prevIsWatched = CURRENT_IS_WATCHED;
|
327
|
+
|
328
|
+
try {
|
329
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
330
|
+
CURRENT_CONSUMER = this;
|
331
|
+
CURRENT_ORD = 0;
|
332
|
+
CURRENT_SEEN = new WeakSet();
|
333
|
+
CURRENT_DEP_TAIL = undefined;
|
334
|
+
CURRENT_IS_WATCHED = isConnected;
|
335
|
+
|
336
|
+
switch (type) {
|
337
|
+
case SignalType.Computed: {
|
338
|
+
const prevValue = this._currentValue as T;
|
339
|
+
const nextValue = (this._compute as SignalCompute<T>)(prevValue);
|
340
|
+
|
341
|
+
if (!this._equals(prevValue!, nextValue)) {
|
342
|
+
this._currentValue = nextValue;
|
343
|
+
this._version++;
|
344
|
+
}
|
345
|
+
break;
|
346
|
+
}
|
347
|
+
|
348
|
+
case SignalType.Async: {
|
349
|
+
const value =
|
350
|
+
(this._currentValue as AsyncResult<T>) ??
|
351
|
+
(this._currentValue = {
|
352
|
+
result: undefined,
|
353
|
+
error: undefined,
|
354
|
+
isPending: true,
|
355
|
+
isReady: false,
|
356
|
+
isError: false,
|
357
|
+
isSuccess: false,
|
358
|
+
});
|
359
|
+
|
360
|
+
const nextValue = (this._compute as SignalAsyncCompute<T>)(value?.result);
|
361
|
+
|
362
|
+
if ('then' in (nextValue as Promise<T>)) {
|
363
|
+
const currentVersion = ++this._version;
|
364
|
+
|
365
|
+
(nextValue as Promise<T>).then(
|
366
|
+
result => {
|
367
|
+
if (currentVersion !== this._version) {
|
368
|
+
return;
|
369
|
+
}
|
370
|
+
|
371
|
+
value.result = result;
|
372
|
+
value.isReady = true;
|
373
|
+
|
374
|
+
value.isPending = false;
|
375
|
+
value.isSuccess = true;
|
376
|
+
|
377
|
+
this._version++;
|
378
|
+
this._dirtyConsumers();
|
379
|
+
},
|
380
|
+
error => {
|
381
|
+
if (currentVersion !== this._version) {
|
382
|
+
return;
|
383
|
+
}
|
384
|
+
|
385
|
+
value.error = error;
|
386
|
+
value.isPending = false;
|
387
|
+
value.isError = true;
|
388
|
+
this._version++;
|
389
|
+
this._dirtyConsumers();
|
390
|
+
},
|
391
|
+
);
|
392
|
+
|
393
|
+
value.isPending = true;
|
394
|
+
value.isError = false;
|
395
|
+
value.isSuccess = false;
|
396
|
+
} else {
|
397
|
+
value.result = nextValue as T;
|
398
|
+
value.isReady = true;
|
399
|
+
value.isPending = false;
|
400
|
+
value.isSuccess = true;
|
401
|
+
value.isError = false;
|
402
|
+
|
403
|
+
this._version++;
|
404
|
+
}
|
405
|
+
|
406
|
+
break;
|
407
|
+
}
|
408
|
+
|
409
|
+
case SignalType.Subscription: {
|
410
|
+
if (shouldConnect) {
|
411
|
+
const subscription = (this._compute as SignalSubscribe<T>)(
|
412
|
+
() => this._currentValue as T,
|
413
|
+
value => {
|
414
|
+
if (this._equals(value, this._currentValue as T)) {
|
415
|
+
return;
|
416
|
+
}
|
417
|
+
this._currentValue = value;
|
418
|
+
this._version++;
|
419
|
+
this._dirtyConsumers();
|
420
|
+
},
|
421
|
+
);
|
422
|
+
SUBSCRIPTIONS.set(this, subscription);
|
423
|
+
} else {
|
424
|
+
const subscription = SUBSCRIPTIONS.get(this);
|
425
|
+
|
426
|
+
subscription?.update?.();
|
427
|
+
}
|
428
|
+
|
429
|
+
break;
|
430
|
+
}
|
431
|
+
|
432
|
+
default: {
|
433
|
+
(this._compute as SignalWatcherEffect)!();
|
434
|
+
}
|
435
|
+
}
|
436
|
+
} finally {
|
437
|
+
endTrack(this, wasConnected);
|
438
|
+
|
439
|
+
CURRENT_CONSUMER = prevConsumer;
|
440
|
+
CURRENT_SEEN = prevSeen;
|
441
|
+
CURRENT_DEP_TAIL = prevDepTail;
|
442
|
+
CURRENT_ORD = prevOrd;
|
443
|
+
CURRENT_IS_WATCHED = prevIsWatched;
|
444
|
+
}
|
445
|
+
}
|
446
|
+
|
447
|
+
_resetDirty() {
|
448
|
+
let dirty = this._dirtyDep;
|
449
|
+
|
450
|
+
while (dirty !== undefined) {
|
451
|
+
const dep = dirty.dep;
|
452
|
+
const oldHead = dep._subs;
|
453
|
+
|
454
|
+
if (oldHead === undefined) {
|
455
|
+
dep._subs = dirty;
|
456
|
+
dirty.nextSub = undefined;
|
457
|
+
dirty.prevSub = undefined;
|
458
|
+
} else {
|
459
|
+
dirty.nextSub = oldHead;
|
460
|
+
oldHead.prevSub = dirty;
|
461
|
+
dep._subs = dirty;
|
462
|
+
}
|
463
|
+
|
464
|
+
let nextDirty = dirty.nextDirty;
|
465
|
+
dirty.nextDirty = undefined;
|
466
|
+
dirty = nextDirty;
|
467
|
+
}
|
468
|
+
}
|
469
|
+
|
470
|
+
_dirty() {
|
471
|
+
if (this._type === SignalType.Subscription) {
|
472
|
+
if (this._connectedCount > 0) {
|
473
|
+
scheduleWatcher(this);
|
474
|
+
}
|
475
|
+
|
476
|
+
// else do nothing, only schedule if connected
|
477
|
+
} else if (this._type === SignalType.Watcher) {
|
478
|
+
scheduleWatcher(this);
|
479
|
+
} else {
|
480
|
+
this._dirtyConsumers();
|
481
|
+
}
|
482
|
+
|
483
|
+
this._subs = undefined;
|
484
|
+
}
|
485
|
+
|
486
|
+
_dirtyConsumers() {
|
487
|
+
let link = this._subs;
|
488
|
+
|
489
|
+
while (link !== undefined) {
|
490
|
+
const consumer = link.sub.deref();
|
491
|
+
|
492
|
+
if (consumer === undefined) {
|
493
|
+
const nextSub = link.nextSub;
|
494
|
+
poolLink(link);
|
495
|
+
link = nextSub;
|
496
|
+
continue;
|
497
|
+
}
|
498
|
+
|
499
|
+
const state = consumer._state;
|
500
|
+
|
501
|
+
if (state === SignalState.Dirty) {
|
502
|
+
const nextSub = link.nextSub;
|
503
|
+
link = nextSub;
|
504
|
+
continue;
|
505
|
+
}
|
506
|
+
|
507
|
+
if (state === SignalState.MaybeDirty) {
|
508
|
+
let dirty = consumer._dirtyDep;
|
509
|
+
const ord = link.ord;
|
510
|
+
|
511
|
+
if (dirty!.ord > ord) {
|
512
|
+
consumer._dirtyDep = link;
|
513
|
+
link.nextDirty = dirty;
|
514
|
+
} else {
|
515
|
+
let nextDirty = dirty!.nextDirty;
|
516
|
+
|
517
|
+
while (nextDirty !== undefined && nextDirty!.ord < ord) {
|
518
|
+
dirty = nextDirty;
|
519
|
+
nextDirty = dirty.nextDirty;
|
520
|
+
}
|
521
|
+
|
522
|
+
link.nextDirty = nextDirty;
|
523
|
+
dirty!.nextDirty = link;
|
524
|
+
}
|
525
|
+
} else {
|
526
|
+
// consumer._dirtyQueueLength = dirtyQueueLength + 2;
|
527
|
+
consumer._state = SignalState.MaybeDirty;
|
528
|
+
consumer._dirtyDep = link;
|
529
|
+
link.nextDirty = undefined;
|
530
|
+
consumer._dirty();
|
531
|
+
}
|
532
|
+
|
533
|
+
link = link.nextSub;
|
534
|
+
}
|
535
|
+
}
|
536
|
+
|
537
|
+
_disconnect(count = 1) {
|
538
|
+
this._connectedCount -= count;
|
539
|
+
|
540
|
+
if (this._connectedCount > 0) {
|
541
|
+
return;
|
542
|
+
} else if (this._connectedCount < 0) {
|
543
|
+
throw new Error('Signal disconnect count cannot be negative');
|
544
|
+
}
|
545
|
+
|
546
|
+
if (this._type === SignalType.Subscription) {
|
547
|
+
const subscription = SUBSCRIPTIONS.get(this);
|
548
|
+
|
549
|
+
if (subscription !== undefined) {
|
550
|
+
subscription.unsubscribe?.();
|
551
|
+
SUBSCRIPTIONS.delete(this);
|
552
|
+
}
|
553
|
+
}
|
554
|
+
|
555
|
+
let link = this._deps;
|
556
|
+
|
557
|
+
while (link !== undefined) {
|
558
|
+
const dep = link.dep;
|
559
|
+
|
560
|
+
dep._disconnect();
|
561
|
+
|
562
|
+
link = link.nextDep;
|
563
|
+
}
|
564
|
+
}
|
565
|
+
}
|
566
|
+
|
567
|
+
export interface AsyncPending {
|
568
|
+
result: undefined;
|
569
|
+
error: unknown;
|
570
|
+
isPending: boolean;
|
571
|
+
isReady: false;
|
572
|
+
isError: boolean;
|
573
|
+
isSuccess: boolean;
|
574
|
+
}
|
575
|
+
|
576
|
+
export interface AsyncReady<T> {
|
577
|
+
result: T;
|
578
|
+
error: unknown;
|
579
|
+
isPending: boolean;
|
580
|
+
isReady: true;
|
581
|
+
isError: boolean;
|
582
|
+
isSuccess: boolean;
|
583
|
+
}
|
584
|
+
|
585
|
+
export type AsyncResult<T> = AsyncPending | AsyncReady<T>;
|
586
|
+
|
587
|
+
class StateSignal<T> implements StateSignal<T> {
|
588
|
+
private _consumers: WeakRef<ComputedSignal<unknown>>[] = [];
|
589
|
+
|
590
|
+
constructor(
|
591
|
+
private _value: T,
|
592
|
+
private _equals: SignalEquals<T> = (a, b) => a === b,
|
593
|
+
) {}
|
594
|
+
|
595
|
+
get(): T {
|
596
|
+
if (CURRENT_CONSUMER !== undefined) {
|
597
|
+
this._consumers.push(CURRENT_CONSUMER._ref);
|
598
|
+
}
|
599
|
+
|
600
|
+
return this._value!;
|
601
|
+
}
|
602
|
+
|
603
|
+
set(value: T) {
|
604
|
+
if (this._equals(value, this._value)) {
|
605
|
+
return;
|
606
|
+
}
|
607
|
+
|
608
|
+
this._value = value;
|
609
|
+
|
610
|
+
const { _consumers: consumers } = this;
|
611
|
+
|
612
|
+
for (const consumerRef of consumers) {
|
613
|
+
const consumer = consumerRef.deref();
|
614
|
+
|
615
|
+
if (consumer === undefined) {
|
616
|
+
continue;
|
617
|
+
}
|
618
|
+
|
619
|
+
consumer._state = SignalState.Dirty;
|
620
|
+
consumer._dirty();
|
621
|
+
}
|
622
|
+
|
623
|
+
consumers.length = 0;
|
624
|
+
}
|
625
|
+
}
|
626
|
+
|
627
|
+
export function state<T>(initialValue: T, opts?: SignalOptions<T>): StateSignal<T> {
|
628
|
+
return new StateSignal(initialValue, opts?.equals);
|
629
|
+
}
|
630
|
+
|
631
|
+
export function computed<T>(compute: (prev: T | undefined) => T, opts?: SignalOptions<T>): Signal<T> {
|
632
|
+
return new ComputedSignal(SignalType.Computed, compute, opts?.equals) as Signal<T>;
|
633
|
+
}
|
634
|
+
|
635
|
+
export function asyncComputed<T>(
|
636
|
+
compute: (prev: T | undefined) => Promise<T>,
|
637
|
+
opts?: SignalOptions<T>,
|
638
|
+
): Signal<AsyncResult<T>>;
|
639
|
+
export function asyncComputed<T>(
|
640
|
+
compute: (prev: T | undefined) => Promise<T>,
|
641
|
+
opts: SignalOptionsWithInit<T>,
|
642
|
+
): Signal<AsyncReady<T>>;
|
643
|
+
export function asyncComputed<T>(
|
644
|
+
compute: (prev: T | undefined) => Promise<T>,
|
645
|
+
opts?: Partial<SignalOptionsWithInit<T>>,
|
646
|
+
): Signal<AsyncResult<T>> {
|
647
|
+
return new ComputedSignal(SignalType.Async, compute, opts?.equals, opts?.initValue) as Signal<AsyncResult<T>>;
|
648
|
+
}
|
649
|
+
|
650
|
+
export function subscription<T>(subscribe: SignalSubscribe<T>, opts?: SignalOptions<T>): Signal<T | undefined>;
|
651
|
+
export function subscription<T>(subscribe: SignalSubscribe<T>, opts: SignalOptionsWithInit<T>): Signal<T>;
|
652
|
+
export function subscription<T>(subscribe: SignalSubscribe<T>, opts?: Partial<SignalOptionsWithInit<T>>): Signal<T> {
|
653
|
+
return new ComputedSignal(SignalType.Subscription, subscribe, opts?.equals, opts?.initValue) as Signal<T>;
|
654
|
+
}
|
655
|
+
|
656
|
+
export interface Watcher {
|
657
|
+
disconnect(): void;
|
658
|
+
}
|
659
|
+
|
660
|
+
export function watcher(fn: () => void): Watcher {
|
661
|
+
const watcher = new ComputedSignal(SignalType.Watcher, fn);
|
662
|
+
|
663
|
+
scheduleWatcher(watcher);
|
664
|
+
|
665
|
+
return {
|
666
|
+
disconnect() {
|
667
|
+
scheduleDisconnect(watcher);
|
668
|
+
},
|
669
|
+
};
|
670
|
+
}
|
671
|
+
|
672
|
+
export function untrack<T = void>(fn: () => T): T {
|
673
|
+
const prevConsumer = CURRENT_CONSUMER;
|
674
|
+
const prevOrd = CURRENT_ORD;
|
675
|
+
const prevIsWatched = CURRENT_IS_WATCHED;
|
676
|
+
|
677
|
+
try {
|
678
|
+
CURRENT_CONSUMER = undefined;
|
679
|
+
// LAST_CONSUMED = undefined;
|
680
|
+
CURRENT_ORD = 0;
|
681
|
+
CURRENT_IS_WATCHED = false;
|
682
|
+
|
683
|
+
return fn();
|
684
|
+
} finally {
|
685
|
+
CURRENT_CONSUMER = prevConsumer;
|
686
|
+
CURRENT_ORD = prevOrd;
|
687
|
+
CURRENT_IS_WATCHED = prevIsWatched;
|
688
|
+
}
|
689
|
+
}
|