signalium 0.2.0 → 0.2.2
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 +12 -0
- package/dist/signals.d.ts +3 -0
- package/dist/signals.js +83 -31
- package/package.json +1 -1
- package/src/__tests__/async.test.ts +65 -25
- package/src/signals.ts +106 -40
package/CHANGELOG.md
CHANGED
package/dist/signals.d.ts
CHANGED
@@ -32,6 +32,7 @@ declare const enum SignalState {
|
|
32
32
|
Dirty = 2
|
33
33
|
}
|
34
34
|
interface Link {
|
35
|
+
id: number;
|
35
36
|
sub: WeakRef<ComputedSignal<any>>;
|
36
37
|
dep: ComputedSignal<any>;
|
37
38
|
ord: number;
|
@@ -76,6 +77,7 @@ export interface AsyncPending<T> extends AsyncBaseResult<T> {
|
|
76
77
|
isReady: false;
|
77
78
|
isError: boolean;
|
78
79
|
isSuccess: boolean;
|
80
|
+
didResolve: boolean;
|
79
81
|
}
|
80
82
|
export interface AsyncReady<T> extends AsyncBaseResult<T> {
|
81
83
|
result: T;
|
@@ -84,6 +86,7 @@ export interface AsyncReady<T> extends AsyncBaseResult<T> {
|
|
84
86
|
isReady: true;
|
85
87
|
isError: boolean;
|
86
88
|
isSuccess: boolean;
|
89
|
+
didResolve: boolean;
|
87
90
|
}
|
88
91
|
export type AsyncResult<T> = AsyncPending<T> | AsyncReady<T>;
|
89
92
|
declare class StateSignal<T> implements StateSignal<T> {
|
package/dist/signals.js
CHANGED
@@ -10,6 +10,41 @@ const SUBSCRIPTIONS = new WeakMap();
|
|
10
10
|
const ACTIVE_ASYNCS = new WeakMap();
|
11
11
|
const WAITING = Symbol();
|
12
12
|
let linkPool;
|
13
|
+
const checkForCircularLinks = (link) => {
|
14
|
+
if (!link)
|
15
|
+
return;
|
16
|
+
for (const key of ['nextDep', 'nextSub', 'prevSub', 'nextDirty']) {
|
17
|
+
let currentLink = link?.[key];
|
18
|
+
while (currentLink !== undefined) {
|
19
|
+
if (currentLink === link) {
|
20
|
+
throw new Error(`Circular link detected via ${key}. This is a bug, please report it to the Signalium maintainers.`);
|
21
|
+
}
|
22
|
+
currentLink = currentLink[key];
|
23
|
+
}
|
24
|
+
}
|
25
|
+
};
|
26
|
+
const typeToString = (type) => {
|
27
|
+
switch (type) {
|
28
|
+
case 0 /* SignalType.Computed */:
|
29
|
+
return 'Computed';
|
30
|
+
case 1 /* SignalType.Subscription */:
|
31
|
+
return 'Subscription';
|
32
|
+
case 2 /* SignalType.Async */:
|
33
|
+
return 'Async';
|
34
|
+
case 3 /* SignalType.Watcher */:
|
35
|
+
return 'Watcher';
|
36
|
+
}
|
37
|
+
};
|
38
|
+
const printComputed = (computed) => {
|
39
|
+
const type = typeToString(computed._type);
|
40
|
+
return `ComputedSignal<${type}:${computed.id}>`;
|
41
|
+
};
|
42
|
+
const printLink = (link) => {
|
43
|
+
const sub = link.sub.deref();
|
44
|
+
const subStr = sub === undefined ? 'undefined' : printComputed(sub);
|
45
|
+
const depStr = printComputed(link.dep);
|
46
|
+
return `Link<${link.id}> sub(${subStr}) -> dep(${depStr})`;
|
47
|
+
};
|
13
48
|
function linkNewDep(dep, sub, nextDep, depsTail, ord) {
|
14
49
|
let newLink;
|
15
50
|
if (linkPool !== undefined) {
|
@@ -22,6 +57,7 @@ function linkNewDep(dep, sub, nextDep, depsTail, ord) {
|
|
22
57
|
}
|
23
58
|
else {
|
24
59
|
newLink = {
|
60
|
+
id: id++,
|
25
61
|
dep,
|
26
62
|
sub: sub._ref,
|
27
63
|
ord,
|
@@ -115,8 +151,39 @@ export class ComputedSignal {
|
|
115
151
|
this._type = type;
|
116
152
|
this._compute = compute;
|
117
153
|
this._equals = equals ?? ((a, b) => a === b);
|
118
|
-
this._currentValue = initValue;
|
119
154
|
this._connectedCount = type === 3 /* SignalType.Watcher */ ? 1 : 0;
|
155
|
+
this._currentValue =
|
156
|
+
type !== 2 /* SignalType.Async */
|
157
|
+
? initValue
|
158
|
+
: {
|
159
|
+
result: initValue,
|
160
|
+
error: undefined,
|
161
|
+
isReady: initValue !== undefined,
|
162
|
+
isPending: true,
|
163
|
+
isError: false,
|
164
|
+
isSuccess: false,
|
165
|
+
didResolve: false,
|
166
|
+
invalidate: () => {
|
167
|
+
this._state = 2 /* SignalState.Dirty */;
|
168
|
+
this._dirty();
|
169
|
+
},
|
170
|
+
await: () => {
|
171
|
+
if (CURRENT_CONSUMER === undefined || CURRENT_CONSUMER._type !== 2 /* SignalType.Async */) {
|
172
|
+
throw new Error('Cannot await an async signal outside of an async signal. If you are using an async function, you must use signal.await() for all async signals _before_ the first language-level `await` keyword statement (e.g. it must be synchronous).');
|
173
|
+
}
|
174
|
+
const value = this._currentValue;
|
175
|
+
if (value.isPending) {
|
176
|
+
const currentConsumer = CURRENT_CONSUMER;
|
177
|
+
ACTIVE_ASYNCS.get(this)?.finally(() => currentConsumer._check());
|
178
|
+
CURRENT_IS_WAITING = true;
|
179
|
+
throw WAITING;
|
180
|
+
}
|
181
|
+
else if (value.isError) {
|
182
|
+
throw value.error;
|
183
|
+
}
|
184
|
+
return value.result;
|
185
|
+
},
|
186
|
+
};
|
120
187
|
}
|
121
188
|
get() {
|
122
189
|
let prevTracked = false;
|
@@ -144,6 +211,8 @@ export class ComputedSignal {
|
|
144
211
|
}
|
145
212
|
this._check(CURRENT_IS_WATCHED && !prevTracked);
|
146
213
|
CURRENT_DEP_TAIL = newLink ?? linkNewDep(this, CURRENT_CONSUMER, nextDep, CURRENT_DEP_TAIL, ord);
|
214
|
+
if (process.env.NODE_ENV !== 'production')
|
215
|
+
checkForCircularLinks(CURRENT_DEP_TAIL);
|
147
216
|
CURRENT_DEP_TAIL.version = this._version;
|
148
217
|
CURRENT_SEEN.add(this);
|
149
218
|
}
|
@@ -166,6 +235,8 @@ export class ComputedSignal {
|
|
166
235
|
}
|
167
236
|
else {
|
168
237
|
let link = this._deps;
|
238
|
+
if (process.env.NODE_ENV !== 'production')
|
239
|
+
checkForCircularLinks(link);
|
169
240
|
while (link !== undefined) {
|
170
241
|
const dep = link.dep;
|
171
242
|
if (link.version !== dep._check(true)) {
|
@@ -175,7 +246,6 @@ export class ComputedSignal {
|
|
175
246
|
link = link.nextDep;
|
176
247
|
}
|
177
248
|
}
|
178
|
-
this._resetDirty();
|
179
249
|
}
|
180
250
|
if (state === 0 /* SignalState.Clean */) {
|
181
251
|
return this._version;
|
@@ -226,34 +296,7 @@ export class ComputedSignal {
|
|
226
296
|
break;
|
227
297
|
}
|
228
298
|
case 2 /* SignalType.Async */: {
|
229
|
-
const value = this._currentValue
|
230
|
-
(this._currentValue = {
|
231
|
-
result: undefined,
|
232
|
-
error: undefined,
|
233
|
-
isPending: true,
|
234
|
-
isReady: false,
|
235
|
-
isError: false,
|
236
|
-
isSuccess: false,
|
237
|
-
invalidate: () => {
|
238
|
-
this._state = 2 /* SignalState.Dirty */;
|
239
|
-
this._dirty();
|
240
|
-
},
|
241
|
-
await: () => {
|
242
|
-
if (CURRENT_CONSUMER === undefined || CURRENT_CONSUMER._type !== 2 /* SignalType.Async */) {
|
243
|
-
throw new Error('Cannot await an async signal outside of an async signal. If you are using an async function, you must use signal.await() for all async signals _before_ the first language-level `await` keyword statement (e.g. it must be synchronous).');
|
244
|
-
}
|
245
|
-
if (value.isPending) {
|
246
|
-
const currentConsumer = CURRENT_CONSUMER;
|
247
|
-
ACTIVE_ASYNCS.get(this)?.finally(() => currentConsumer._check());
|
248
|
-
CURRENT_IS_WAITING = true;
|
249
|
-
throw WAITING;
|
250
|
-
}
|
251
|
-
else if (value.isError) {
|
252
|
-
throw value.error;
|
253
|
-
}
|
254
|
-
return value.result;
|
255
|
-
},
|
256
|
-
});
|
299
|
+
const value = this._currentValue;
|
257
300
|
let nextValue;
|
258
301
|
try {
|
259
302
|
CURRENT_IS_WAITING = false;
|
@@ -265,8 +308,8 @@ export class ComputedSignal {
|
|
265
308
|
value.isPending = false;
|
266
309
|
value.isError = true;
|
267
310
|
this._version++;
|
311
|
+
break;
|
268
312
|
}
|
269
|
-
break;
|
270
313
|
}
|
271
314
|
if (CURRENT_IS_WAITING) {
|
272
315
|
if (!value.isPending) {
|
@@ -294,6 +337,7 @@ export class ComputedSignal {
|
|
294
337
|
}
|
295
338
|
value.result = result;
|
296
339
|
value.isReady = true;
|
340
|
+
value.didResolve = true;
|
297
341
|
value.isPending = false;
|
298
342
|
value.isSuccess = true;
|
299
343
|
this._version++;
|
@@ -367,13 +411,19 @@ export class ComputedSignal {
|
|
367
411
|
}
|
368
412
|
else {
|
369
413
|
dirty.nextSub = oldHead;
|
414
|
+
dirty.prevSub = undefined;
|
370
415
|
oldHead.prevSub = dirty;
|
371
416
|
dep._subs = dirty;
|
372
417
|
}
|
418
|
+
if (process.env.NODE_ENV !== 'production') {
|
419
|
+
checkForCircularLinks(this._dirtyDep);
|
420
|
+
}
|
373
421
|
let nextDirty = dirty.nextDirty;
|
374
422
|
dirty.nextDirty = undefined;
|
375
423
|
dirty = nextDirty;
|
376
424
|
}
|
425
|
+
if (process.env.NODE_ENV !== 'production')
|
426
|
+
checkForCircularLinks(this._dirtyDep);
|
377
427
|
}
|
378
428
|
_dirty() {
|
379
429
|
if (this._type === 1 /* SignalType.Subscription */) {
|
@@ -392,6 +442,8 @@ export class ComputedSignal {
|
|
392
442
|
}
|
393
443
|
_dirtyConsumers() {
|
394
444
|
let link = this._subs;
|
445
|
+
if (process.env.NODE_ENV !== 'production')
|
446
|
+
checkForCircularLinks(link);
|
395
447
|
while (link !== undefined) {
|
396
448
|
const consumer = link.sub.deref();
|
397
449
|
if (consumer === undefined) {
|
package/package.json
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
2
|
-
import { state,
|
2
|
+
import { state, asyncComputed } from './utils/instrumented.js';
|
3
3
|
import { AsyncResult } from '../signals';
|
4
4
|
|
5
5
|
const sleep = (ms = 0) => new Promise(r => setTimeout(r, ms));
|
@@ -8,16 +8,19 @@ const nextTick = () => new Promise(r => setTimeout(r, 0));
|
|
8
8
|
const result = <T>(
|
9
9
|
value: T | undefined,
|
10
10
|
promiseState: 'pending' | 'error' | 'success',
|
11
|
-
|
11
|
+
readyState: 'initial' | 'ready' | 'resolved',
|
12
12
|
error?: any,
|
13
13
|
): AsyncResult<T> =>
|
14
14
|
({
|
15
15
|
result: value,
|
16
16
|
error,
|
17
17
|
isPending: promiseState === 'pending',
|
18
|
-
isReady,
|
19
18
|
isError: promiseState === 'error',
|
20
19
|
isSuccess: promiseState === 'success',
|
20
|
+
|
21
|
+
isReady: readyState === 'ready' || readyState === 'resolved',
|
22
|
+
didResolve: readyState === 'resolved',
|
23
|
+
|
21
24
|
await: expect.any(Function),
|
22
25
|
invalidate: expect.any(Function),
|
23
26
|
}) as AsyncResult<T>;
|
@@ -31,20 +34,20 @@ describe('Async Signal functionality', () => {
|
|
31
34
|
return a.get() + b.get();
|
32
35
|
});
|
33
36
|
|
34
|
-
expect(c).toHaveValueAndCounts(result(undefined, 'pending',
|
37
|
+
expect(c).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
35
38
|
compute: 1,
|
36
39
|
resolve: 0,
|
37
40
|
});
|
38
41
|
|
39
42
|
await nextTick();
|
40
43
|
|
41
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
44
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
42
45
|
compute: 1,
|
43
46
|
resolve: 1,
|
44
47
|
});
|
45
48
|
|
46
49
|
// stability
|
47
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
50
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
48
51
|
compute: 1,
|
49
52
|
resolve: 1,
|
50
53
|
});
|
@@ -58,28 +61,28 @@ describe('Async Signal functionality', () => {
|
|
58
61
|
return a.get() + b.get();
|
59
62
|
});
|
60
63
|
|
61
|
-
expect(c).toHaveValueAndCounts(result(undefined, 'pending',
|
64
|
+
expect(c).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
62
65
|
compute: 1,
|
63
66
|
resolve: 0,
|
64
67
|
});
|
65
68
|
|
66
69
|
await nextTick();
|
67
70
|
|
68
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
71
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
69
72
|
compute: 1,
|
70
73
|
resolve: 1,
|
71
74
|
});
|
72
75
|
|
73
76
|
a.set(2);
|
74
77
|
|
75
|
-
expect(c).toHaveValueAndCounts(result(3, 'pending',
|
78
|
+
expect(c).toHaveValueAndCounts(result(3, 'pending', 'resolved'), {
|
76
79
|
compute: 2,
|
77
80
|
resolve: 1,
|
78
81
|
});
|
79
82
|
|
80
83
|
await nextTick();
|
81
84
|
|
82
|
-
expect(c).toHaveValueAndCounts(result(4, 'success',
|
85
|
+
expect(c).toHaveValueAndCounts(result(4, 'success', 'resolved'), {
|
83
86
|
compute: 2,
|
84
87
|
resolve: 2,
|
85
88
|
});
|
@@ -93,28 +96,28 @@ describe('Async Signal functionality', () => {
|
|
93
96
|
return a.get() + b.get();
|
94
97
|
});
|
95
98
|
|
96
|
-
expect(c).toHaveValueAndCounts(result(undefined, 'pending',
|
99
|
+
expect(c).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
97
100
|
compute: 1,
|
98
101
|
resolve: 0,
|
99
102
|
});
|
100
103
|
|
101
104
|
await nextTick();
|
102
105
|
|
103
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
106
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
104
107
|
compute: 1,
|
105
108
|
resolve: 1,
|
106
109
|
});
|
107
110
|
|
108
111
|
a.set(1);
|
109
112
|
|
110
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
113
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
111
114
|
compute: 1,
|
112
115
|
resolve: 1,
|
113
116
|
});
|
114
117
|
|
115
118
|
await nextTick();
|
116
119
|
|
117
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
120
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
118
121
|
compute: 1,
|
119
122
|
resolve: 1,
|
120
123
|
});
|
@@ -134,40 +137,77 @@ describe('Async Signal functionality', () => {
|
|
134
137
|
return result;
|
135
138
|
});
|
136
139
|
|
137
|
-
expect(c).toHaveValueAndCounts(result(undefined, 'pending',
|
140
|
+
expect(c).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
138
141
|
compute: 1,
|
139
142
|
resolve: 0,
|
140
143
|
});
|
141
144
|
|
142
145
|
await nextTick();
|
143
146
|
|
144
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
147
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
145
148
|
compute: 1,
|
146
149
|
resolve: 1,
|
147
150
|
});
|
148
151
|
|
149
152
|
a.set(2);
|
150
153
|
|
151
|
-
expect(c).toHaveValueAndCounts(result(3, 'pending',
|
154
|
+
expect(c).toHaveValueAndCounts(result(3, 'pending', 'resolved'), {
|
152
155
|
compute: 2,
|
153
156
|
resolve: 1,
|
154
157
|
});
|
155
158
|
|
156
159
|
a.set(3);
|
157
160
|
|
158
|
-
expect(c).toHaveValueAndCounts(result(3, 'pending',
|
161
|
+
expect(c).toHaveValueAndCounts(result(3, 'pending', 'resolved'), {
|
159
162
|
compute: 3,
|
160
163
|
resolve: 1,
|
161
164
|
});
|
162
165
|
|
163
166
|
await sleep(200);
|
164
167
|
|
165
|
-
expect(c).toHaveValueAndCounts(result(5, 'success',
|
168
|
+
expect(c).toHaveValueAndCounts(result(5, 'success', 'resolved'), {
|
166
169
|
compute: 3,
|
167
170
|
resolve: 3,
|
168
171
|
});
|
169
172
|
});
|
170
173
|
|
174
|
+
test('Can have initial value', async () => {
|
175
|
+
const a = state(1);
|
176
|
+
const b = state(2);
|
177
|
+
|
178
|
+
const c = asyncComputed(
|
179
|
+
async () => {
|
180
|
+
const result = a.get() + b.get();
|
181
|
+
|
182
|
+
await sleep(50);
|
183
|
+
|
184
|
+
return result;
|
185
|
+
},
|
186
|
+
{
|
187
|
+
initValue: 5,
|
188
|
+
},
|
189
|
+
);
|
190
|
+
|
191
|
+
expect(c).toHaveValueAndCounts(result(5, 'pending', 'ready'), {
|
192
|
+
compute: 1,
|
193
|
+
resolve: 0,
|
194
|
+
});
|
195
|
+
|
196
|
+
await nextTick();
|
197
|
+
|
198
|
+
expect(c).toHaveValueAndCounts(result(5, 'pending', 'ready'), {
|
199
|
+
compute: 1,
|
200
|
+
resolve: 0,
|
201
|
+
});
|
202
|
+
|
203
|
+
await sleep(60);
|
204
|
+
|
205
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
206
|
+
compute: 1,
|
207
|
+
resolve: 1,
|
208
|
+
});
|
209
|
+
});
|
210
|
+
|
171
211
|
describe('Awaiting', () => {
|
172
212
|
test('Awaiting a computed will resolve the value', async () => {
|
173
213
|
const compA = asyncComputed(async () => {
|
@@ -190,7 +230,7 @@ describe('Async Signal functionality', () => {
|
|
190
230
|
});
|
191
231
|
|
192
232
|
// Pull once to start the computation, trigger the computation
|
193
|
-
expect(compC).toHaveValueAndCounts(result(undefined, 'pending',
|
233
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
194
234
|
compute: 1,
|
195
235
|
resolve: 0,
|
196
236
|
});
|
@@ -198,7 +238,7 @@ describe('Async Signal functionality', () => {
|
|
198
238
|
await nextTick();
|
199
239
|
|
200
240
|
// Check after a tick to make sure we didn't resolve early
|
201
|
-
expect(compC).toHaveValueAndCounts(result(undefined, 'pending',
|
241
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
202
242
|
compute: 1,
|
203
243
|
resolve: 0,
|
204
244
|
});
|
@@ -206,14 +246,14 @@ describe('Async Signal functionality', () => {
|
|
206
246
|
await sleep(30);
|
207
247
|
|
208
248
|
// Check to make sure we don't resolve early after the first task completes
|
209
|
-
expect(compC).toHaveValueAndCounts(result(undefined, 'pending',
|
249
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
210
250
|
compute: 2,
|
211
251
|
resolve: 0,
|
212
252
|
});
|
213
253
|
|
214
254
|
await sleep(30);
|
215
255
|
|
216
|
-
expect(compC).toHaveValueAndCounts(result(3, 'success',
|
256
|
+
expect(compC).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
217
257
|
compute: 3,
|
218
258
|
resolve: 1,
|
219
259
|
});
|
@@ -240,14 +280,14 @@ describe('Async Signal functionality', () => {
|
|
240
280
|
});
|
241
281
|
|
242
282
|
// Pull once to start the computation, trigger the computation
|
243
|
-
expect(compC).toHaveValueAndCounts(result(undefined, 'pending',
|
283
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
244
284
|
compute: 1,
|
245
285
|
resolve: 0,
|
246
286
|
});
|
247
287
|
|
248
288
|
await sleep(50);
|
249
289
|
|
250
|
-
expect(compC).toHaveValueAndCounts(result(undefined, 'error',
|
290
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'error', 'initial', 'error'), {
|
251
291
|
compute: 2,
|
252
292
|
resolve: 0,
|
253
293
|
});
|
package/src/signals.ts
CHANGED
@@ -61,6 +61,7 @@ const enum SignalState {
|
|
61
61
|
const WAITING = Symbol();
|
62
62
|
|
63
63
|
interface Link {
|
64
|
+
id: number;
|
64
65
|
sub: WeakRef<ComputedSignal<any>>;
|
65
66
|
dep: ComputedSignal<any>;
|
66
67
|
ord: number;
|
@@ -75,6 +76,51 @@ interface Link {
|
|
75
76
|
|
76
77
|
let linkPool: Link | undefined;
|
77
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
|
+
|
78
124
|
function linkNewDep(
|
79
125
|
dep: ComputedSignal<any>,
|
80
126
|
sub: ComputedSignal<any>,
|
@@ -93,6 +139,7 @@ function linkNewDep(
|
|
93
139
|
newLink.ord = ord;
|
94
140
|
} else {
|
95
141
|
newLink = {
|
142
|
+
id: id++,
|
96
143
|
dep,
|
97
144
|
sub: sub._ref,
|
98
145
|
ord,
|
@@ -208,8 +255,48 @@ export class ComputedSignal<T> {
|
|
208
255
|
this._type = type;
|
209
256
|
this._compute = compute;
|
210
257
|
this._equals = equals ?? ((a, b) => a === b);
|
211
|
-
this._currentValue = initValue;
|
212
258
|
this._connectedCount = type === SignalType.Watcher ? 1 : 0;
|
259
|
+
|
260
|
+
this._currentValue =
|
261
|
+
type !== SignalType.Async
|
262
|
+
? initValue
|
263
|
+
: ({
|
264
|
+
result: initValue,
|
265
|
+
error: undefined,
|
266
|
+
isReady: initValue !== undefined,
|
267
|
+
|
268
|
+
isPending: true,
|
269
|
+
isError: false,
|
270
|
+
isSuccess: false,
|
271
|
+
didResolve: false,
|
272
|
+
|
273
|
+
invalidate: () => {
|
274
|
+
this._state = SignalState.Dirty;
|
275
|
+
this._dirty();
|
276
|
+
},
|
277
|
+
|
278
|
+
await: () => {
|
279
|
+
if (CURRENT_CONSUMER === undefined || CURRENT_CONSUMER._type !== SignalType.Async) {
|
280
|
+
throw new Error(
|
281
|
+
'Cannot await an async signal outside of an async signal. If you are using an async function, you must use signal.await() for all async signals _before_ the first language-level `await` keyword statement (e.g. it must be synchronous).',
|
282
|
+
);
|
283
|
+
}
|
284
|
+
|
285
|
+
const value = this._currentValue as AsyncResult<T>;
|
286
|
+
|
287
|
+
if (value.isPending) {
|
288
|
+
const currentConsumer = CURRENT_CONSUMER;
|
289
|
+
ACTIVE_ASYNCS.get(this)?.finally(() => currentConsumer._check());
|
290
|
+
|
291
|
+
CURRENT_IS_WAITING = true;
|
292
|
+
throw WAITING;
|
293
|
+
} else if (value.isError) {
|
294
|
+
throw value.error;
|
295
|
+
}
|
296
|
+
|
297
|
+
return value.result as T;
|
298
|
+
},
|
299
|
+
} as AsyncResult<T>);
|
213
300
|
}
|
214
301
|
|
215
302
|
get(): T | AsyncResult<T> {
|
@@ -248,6 +335,8 @@ export class ComputedSignal<T> {
|
|
248
335
|
|
249
336
|
CURRENT_DEP_TAIL = newLink ?? linkNewDep(this, CURRENT_CONSUMER, nextDep, CURRENT_DEP_TAIL, ord);
|
250
337
|
|
338
|
+
if (process.env.NODE_ENV !== 'production') checkForCircularLinks(CURRENT_DEP_TAIL);
|
339
|
+
|
251
340
|
CURRENT_DEP_TAIL.version = this._version;
|
252
341
|
CURRENT_SEEN!.add(this);
|
253
342
|
} else {
|
@@ -274,6 +363,8 @@ export class ComputedSignal<T> {
|
|
274
363
|
} else {
|
275
364
|
let link = this._deps;
|
276
365
|
|
366
|
+
if (process.env.NODE_ENV !== 'production') checkForCircularLinks(link);
|
367
|
+
|
277
368
|
while (link !== undefined) {
|
278
369
|
const dep = link.dep;
|
279
370
|
|
@@ -285,8 +376,6 @@ export class ComputedSignal<T> {
|
|
285
376
|
link = link.nextDep;
|
286
377
|
}
|
287
378
|
}
|
288
|
-
|
289
|
-
this._resetDirty();
|
290
379
|
}
|
291
380
|
|
292
381
|
if (state === SignalState.Clean) {
|
@@ -350,41 +439,7 @@ export class ComputedSignal<T> {
|
|
350
439
|
}
|
351
440
|
|
352
441
|
case SignalType.Async: {
|
353
|
-
const value: AsyncResult<T> =
|
354
|
-
(this._currentValue as AsyncResult<T>) ??
|
355
|
-
(this._currentValue = {
|
356
|
-
result: undefined,
|
357
|
-
error: undefined,
|
358
|
-
isPending: true,
|
359
|
-
isReady: false,
|
360
|
-
isError: false,
|
361
|
-
isSuccess: false,
|
362
|
-
|
363
|
-
invalidate: () => {
|
364
|
-
this._state = SignalState.Dirty;
|
365
|
-
this._dirty();
|
366
|
-
},
|
367
|
-
|
368
|
-
await: () => {
|
369
|
-
if (CURRENT_CONSUMER === undefined || CURRENT_CONSUMER._type !== SignalType.Async) {
|
370
|
-
throw new Error(
|
371
|
-
'Cannot await an async signal outside of an async signal. If you are using an async function, you must use signal.await() for all async signals _before_ the first language-level `await` keyword statement (e.g. it must be synchronous).',
|
372
|
-
);
|
373
|
-
}
|
374
|
-
|
375
|
-
if (value.isPending) {
|
376
|
-
const currentConsumer = CURRENT_CONSUMER;
|
377
|
-
ACTIVE_ASYNCS.get(this)?.finally(() => currentConsumer._check());
|
378
|
-
|
379
|
-
CURRENT_IS_WAITING = true;
|
380
|
-
throw WAITING;
|
381
|
-
} else if (value.isError) {
|
382
|
-
throw value.error;
|
383
|
-
}
|
384
|
-
|
385
|
-
return value.result as T;
|
386
|
-
},
|
387
|
-
});
|
442
|
+
const value: AsyncResult<T> = this._currentValue as AsyncResult<T>;
|
388
443
|
|
389
444
|
let nextValue;
|
390
445
|
|
@@ -397,9 +452,8 @@ export class ComputedSignal<T> {
|
|
397
452
|
value.isPending = false;
|
398
453
|
value.isError = true;
|
399
454
|
this._version++;
|
455
|
+
break;
|
400
456
|
}
|
401
|
-
|
402
|
-
break;
|
403
457
|
}
|
404
458
|
|
405
459
|
if (CURRENT_IS_WAITING) {
|
@@ -431,6 +485,7 @@ export class ComputedSignal<T> {
|
|
431
485
|
|
432
486
|
value.result = result;
|
433
487
|
value.isReady = true;
|
488
|
+
value.didResolve = true;
|
434
489
|
|
435
490
|
value.isPending = false;
|
436
491
|
value.isSuccess = true;
|
@@ -520,14 +575,21 @@ export class ComputedSignal<T> {
|
|
520
575
|
dirty.prevSub = undefined;
|
521
576
|
} else {
|
522
577
|
dirty.nextSub = oldHead;
|
578
|
+
dirty.prevSub = undefined;
|
523
579
|
oldHead.prevSub = dirty;
|
524
580
|
dep._subs = dirty;
|
525
581
|
}
|
526
582
|
|
583
|
+
if (process.env.NODE_ENV !== 'production') {
|
584
|
+
checkForCircularLinks(this._dirtyDep);
|
585
|
+
}
|
586
|
+
|
527
587
|
let nextDirty = dirty.nextDirty;
|
528
588
|
dirty.nextDirty = undefined;
|
529
589
|
dirty = nextDirty;
|
530
590
|
}
|
591
|
+
|
592
|
+
if (process.env.NODE_ENV !== 'production') checkForCircularLinks(this._dirtyDep);
|
531
593
|
}
|
532
594
|
|
533
595
|
_dirty() {
|
@@ -549,6 +611,8 @@ export class ComputedSignal<T> {
|
|
549
611
|
_dirtyConsumers() {
|
550
612
|
let link = this._subs;
|
551
613
|
|
614
|
+
if (process.env.NODE_ENV !== 'production') checkForCircularLinks(link);
|
615
|
+
|
552
616
|
while (link !== undefined) {
|
553
617
|
const consumer = link.sub.deref();
|
554
618
|
|
@@ -639,6 +703,7 @@ export interface AsyncPending<T> extends AsyncBaseResult<T> {
|
|
639
703
|
isReady: false;
|
640
704
|
isError: boolean;
|
641
705
|
isSuccess: boolean;
|
706
|
+
didResolve: boolean;
|
642
707
|
}
|
643
708
|
|
644
709
|
export interface AsyncReady<T> extends AsyncBaseResult<T> {
|
@@ -648,6 +713,7 @@ export interface AsyncReady<T> extends AsyncBaseResult<T> {
|
|
648
713
|
isReady: true;
|
649
714
|
isError: boolean;
|
650
715
|
isSuccess: boolean;
|
716
|
+
didResolve: boolean;
|
651
717
|
}
|
652
718
|
|
653
719
|
export type AsyncResult<T> = AsyncPending<T> | AsyncReady<T>;
|