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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # signalium
2
2
 
3
+ ## 0.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 0376187: Fix a circular ref and add logs to detect circular refs in dev
8
+
9
+ ## 0.2.1
10
+
11
+ ### Patch Changes
12
+
13
+ - e8aa91a: Fix async init values
14
+
3
15
  ## 0.2.0
4
16
 
5
17
  ### Minor Changes
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,6 +1,6 @@
1
1
  {
2
2
  "name": "signalium",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "repository": "https://github.com/pzuraq/signalium",
6
6
  "description": "Chain-reactivity at critical mass",
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'vitest';
2
- import { state, computed, asyncComputed } from './utils/instrumented.js';
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
- isReady: boolean,
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', false), {
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', true), {
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', true), {
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', false), {
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', true), {
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', true), {
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', true), {
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', false), {
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', true), {
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', true), {
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', true), {
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', false), {
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', true), {
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', true), {
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', true), {
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', true), {
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', false), {
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', false), {
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', false), {
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', true), {
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', false), {
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', false, '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>;