@esmj/signals 0.0.5 → 0.1.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/README.md +568 -2
- package/dist/index.d.mts +193 -47
- package/dist/index.d.ts +193 -47
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { Observable, Observer } from '@esmj/observable';
|
|
2
2
|
|
|
3
3
|
let context = null;
|
|
4
|
+
let batchDepth = 0;
|
|
5
|
+
let batchQueue = new Set();
|
|
6
|
+
let flushScheduled = false;
|
|
7
|
+
let flushCallbacks = [];
|
|
4
8
|
|
|
5
9
|
function untrack(callback) {
|
|
6
10
|
const prevContext = context;
|
|
@@ -11,6 +15,57 @@ function untrack(callback) {
|
|
|
11
15
|
return result;
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
function flushPending() {
|
|
19
|
+
flushScheduled = false;
|
|
20
|
+
const pendings = getPending();
|
|
21
|
+
for (const pending of pendings) {
|
|
22
|
+
pending.get();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Run all onFlush callbacks once, then clear
|
|
26
|
+
const callbacks = flushCallbacks;
|
|
27
|
+
flushCallbacks = [];
|
|
28
|
+
for (const cb of callbacks) {
|
|
29
|
+
cb();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function scheduleFlush() {
|
|
34
|
+
if (!flushScheduled) {
|
|
35
|
+
flushScheduled = true;
|
|
36
|
+
queueMicrotask(flushPending);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function onFlush(callback) {
|
|
41
|
+
flushCallbacks.push(callback);
|
|
42
|
+
|
|
43
|
+
// Ensure a flush is scheduled so the callback actually runs
|
|
44
|
+
scheduleFlush();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function afterFlush() {
|
|
48
|
+
return new Promise((resolve) => onFlush(resolve));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function batch(callback) {
|
|
52
|
+
batchDepth++;
|
|
53
|
+
try {
|
|
54
|
+
callback();
|
|
55
|
+
} finally {
|
|
56
|
+
batchDepth--;
|
|
57
|
+
if (batchDepth === 0) {
|
|
58
|
+
const queue = batchQueue;
|
|
59
|
+
batchQueue = new Set();
|
|
60
|
+
for (const observable of queue) {
|
|
61
|
+
observable.next();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
scheduleFlush();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
14
69
|
const INTERNAL_OBSERVABLE = Symbol('internal observable');
|
|
15
70
|
const ORIGINAL_FUNCTION = Symbol('original function');
|
|
16
71
|
|
|
@@ -75,29 +130,37 @@ class Watcher extends Observable {
|
|
|
75
130
|
}
|
|
76
131
|
|
|
77
132
|
getPending() {
|
|
78
|
-
const pendings =
|
|
133
|
+
const pendings = [];
|
|
134
|
+
for (const pending of this.#pendings) {
|
|
79
135
|
if (!pending.get[ORIGINAL_FUNCTION]) {
|
|
80
|
-
const originalGet = pending.get
|
|
136
|
+
const originalGet = pending.get;
|
|
137
|
+
const boundGet = originalGet.bind(pending);
|
|
81
138
|
|
|
82
139
|
pending.get = () => {
|
|
83
140
|
return untrack(() => {
|
|
84
141
|
this.#pendings.delete(pending);
|
|
85
|
-
return
|
|
142
|
+
return boundGet();
|
|
86
143
|
});
|
|
87
144
|
};
|
|
88
145
|
|
|
89
146
|
pending.get[ORIGINAL_FUNCTION] = originalGet;
|
|
90
147
|
}
|
|
91
148
|
|
|
92
|
-
|
|
93
|
-
}
|
|
149
|
+
pendings.push(pending);
|
|
150
|
+
}
|
|
94
151
|
|
|
95
152
|
return pendings;
|
|
96
153
|
}
|
|
97
154
|
|
|
98
155
|
unwatch(signal) {
|
|
99
|
-
signal.next = signal.next[ORIGINAL_FUNCTION]
|
|
100
|
-
|
|
156
|
+
signal.next = signal.next[ORIGINAL_FUNCTION]
|
|
157
|
+
? signal.next[ORIGINAL_FUNCTION]
|
|
158
|
+
: signal.next;
|
|
159
|
+
signal.get = signal.get[ORIGINAL_FUNCTION]
|
|
160
|
+
? signal.get[ORIGINAL_FUNCTION]
|
|
161
|
+
: signal.get;
|
|
162
|
+
|
|
163
|
+
this.#pendings.delete(signal);
|
|
101
164
|
|
|
102
165
|
return this.unsubscribe(signal);
|
|
103
166
|
}
|
|
@@ -108,15 +171,9 @@ let w = null;
|
|
|
108
171
|
function createWatcher(notify) {
|
|
109
172
|
w = new Watcher(notify);
|
|
110
173
|
}
|
|
111
|
-
|
|
174
|
+
|
|
112
175
|
createWatcher(() => {
|
|
113
|
-
|
|
114
|
-
clearTimeout(timer);
|
|
115
|
-
timer = setTimeout(() => {
|
|
116
|
-
getPending().forEach((pending) => {
|
|
117
|
-
pending.get();
|
|
118
|
-
});
|
|
119
|
-
}, 0);
|
|
176
|
+
scheduleFlush();
|
|
120
177
|
});
|
|
121
178
|
|
|
122
179
|
function getPending() {
|
|
@@ -131,13 +188,18 @@ function unwatch(signal) {
|
|
|
131
188
|
return w.unwatch(signal);
|
|
132
189
|
}
|
|
133
190
|
|
|
191
|
+
const NO_ERROR = Symbol('no error');
|
|
192
|
+
|
|
134
193
|
class Computed extends Observer {
|
|
135
194
|
#dirty = true;
|
|
136
|
-
#
|
|
195
|
+
#running = false;
|
|
137
196
|
#signal = null;
|
|
138
197
|
#context = this.#createNewContext();
|
|
139
198
|
#callback = null;
|
|
140
199
|
#options = null;
|
|
200
|
+
#revision = 0;
|
|
201
|
+
#sourceRevisions = new Map();
|
|
202
|
+
#error = NO_ERROR;
|
|
141
203
|
|
|
142
204
|
constructor(callback, options) {
|
|
143
205
|
super();
|
|
@@ -151,85 +213,144 @@ class Computed extends Observer {
|
|
|
151
213
|
}
|
|
152
214
|
|
|
153
215
|
#clearContextDependencies() {
|
|
154
|
-
|
|
155
|
-
(
|
|
156
|
-
|
|
157
|
-
},
|
|
158
|
-
);
|
|
216
|
+
for (const { unsubscribe } of this.#context.dependencies.values()) {
|
|
217
|
+
unsubscribe();
|
|
218
|
+
}
|
|
159
219
|
this.#context.dependencies.clear();
|
|
160
220
|
}
|
|
161
221
|
|
|
162
222
|
#createNewContext() {
|
|
163
223
|
const context = {
|
|
164
224
|
dependencies: new Map(),
|
|
225
|
+
sourceRevisions: new Map(),
|
|
165
226
|
observer: this,
|
|
166
227
|
};
|
|
167
228
|
|
|
168
229
|
return context;
|
|
169
230
|
}
|
|
170
231
|
|
|
171
|
-
|
|
172
|
-
this.#
|
|
173
|
-
context = this.#context;
|
|
232
|
+
getRevision() {
|
|
233
|
+
return this.#revision;
|
|
174
234
|
}
|
|
175
235
|
|
|
176
|
-
#
|
|
177
|
-
|
|
236
|
+
#needsRecompute() {
|
|
237
|
+
for (const [source, savedRevision] of this.#sourceRevisions) {
|
|
238
|
+
if (source instanceof Computed) {
|
|
239
|
+
untrack(() => source.get());
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (source.getRevision() !== savedRevision) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
178
247
|
}
|
|
179
248
|
|
|
180
249
|
next() {
|
|
181
250
|
this.#dirty = true;
|
|
251
|
+
watch(this);
|
|
182
252
|
this.#signal[INTERNAL_OBSERVABLE].next();
|
|
183
253
|
}
|
|
184
254
|
|
|
185
255
|
get() {
|
|
256
|
+
if (this.#running) {
|
|
257
|
+
throw new Error('Cycle detected in computed signal');
|
|
258
|
+
}
|
|
259
|
+
|
|
186
260
|
if (!this.#signal) {
|
|
187
261
|
this.#signal = createSignal(this.#run(), this.#options);
|
|
188
262
|
}
|
|
189
263
|
|
|
190
|
-
if (this.#dirty) {
|
|
191
|
-
this.#
|
|
264
|
+
if (this.#dirty || this.#error !== NO_ERROR) {
|
|
265
|
+
if (this.#sourceRevisions.size === 0 || this.#needsRecompute()) {
|
|
266
|
+
unwatch(this);
|
|
267
|
+
this.#run();
|
|
268
|
+
} else {
|
|
269
|
+
this.#dirty = false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (context) {
|
|
274
|
+
context.sourceRevisions.set(this, this.#revision);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (this.#error !== NO_ERROR) {
|
|
278
|
+
throw this.#error;
|
|
192
279
|
}
|
|
193
280
|
|
|
194
281
|
return this.#signal.get();
|
|
195
282
|
}
|
|
196
283
|
|
|
284
|
+
peek() {
|
|
285
|
+
if (!this.#signal) {
|
|
286
|
+
this.#signal = createSignal(this.#run(), this.#options);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (this.#dirty || this.#error !== NO_ERROR) {
|
|
290
|
+
if (this.#sourceRevisions.size === 0 || this.#needsRecompute()) {
|
|
291
|
+
unwatch(this);
|
|
292
|
+
this.#run();
|
|
293
|
+
} else {
|
|
294
|
+
this.#dirty = false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (this.#error !== NO_ERROR) {
|
|
299
|
+
throw this.#error;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return this.#signal.peek();
|
|
303
|
+
}
|
|
304
|
+
|
|
197
305
|
#run() {
|
|
306
|
+
this.#running = true;
|
|
198
307
|
this.#dirty = false;
|
|
308
|
+
this.#error = NO_ERROR;
|
|
199
309
|
|
|
200
310
|
this.#clearContextDependencies();
|
|
201
|
-
this.#
|
|
311
|
+
this.#context.sourceRevisions.clear();
|
|
312
|
+
|
|
313
|
+
const prevContext = context;
|
|
314
|
+
context = this.#context;
|
|
202
315
|
|
|
203
316
|
let result;
|
|
204
317
|
try {
|
|
205
318
|
result = this.#callback();
|
|
206
319
|
} catch (e) {
|
|
207
|
-
|
|
320
|
+
this.#error = e;
|
|
208
321
|
}
|
|
209
322
|
|
|
210
|
-
|
|
323
|
+
context = prevContext;
|
|
324
|
+
this.#sourceRevisions = this.#context.sourceRevisions;
|
|
325
|
+
this.#context.sourceRevisions = new Map();
|
|
326
|
+
this.#running = false;
|
|
211
327
|
|
|
212
|
-
// todo test it
|
|
213
328
|
if (result instanceof Promise) {
|
|
214
329
|
result = result
|
|
215
330
|
.then((value) => {
|
|
216
|
-
|
|
331
|
+
this.#error = NO_ERROR;
|
|
332
|
+
this.#signal.set(value);
|
|
217
333
|
})
|
|
218
334
|
.catch((e) => {
|
|
219
|
-
|
|
335
|
+
this.#error = e;
|
|
220
336
|
});
|
|
221
337
|
}
|
|
222
338
|
|
|
223
|
-
if (this.#signal) {
|
|
339
|
+
if (this.#error === NO_ERROR && this.#signal) {
|
|
224
340
|
this.#signal.set(result);
|
|
225
341
|
}
|
|
226
342
|
|
|
227
|
-
|
|
228
|
-
throw result;
|
|
229
|
-
}
|
|
343
|
+
this.#revision++;
|
|
230
344
|
|
|
231
345
|
return result;
|
|
232
346
|
}
|
|
347
|
+
|
|
348
|
+
destroy() {
|
|
349
|
+
this.#clearContextDependencies();
|
|
350
|
+
this.#sourceRevisions.clear();
|
|
351
|
+
this.#dirty = true;
|
|
352
|
+
this.#error = NO_ERROR;
|
|
353
|
+
}
|
|
233
354
|
}
|
|
234
355
|
|
|
235
356
|
function computed(callback, options) {
|
|
@@ -251,46 +372,71 @@ function effect(callback, options) {
|
|
|
251
372
|
c.get();
|
|
252
373
|
|
|
253
374
|
watch(c);
|
|
254
|
-
|
|
375
|
+
|
|
376
|
+
const dispose = () => {
|
|
255
377
|
destructor?.();
|
|
378
|
+
c.destroy();
|
|
256
379
|
unwatch(c);
|
|
257
380
|
};
|
|
381
|
+
|
|
382
|
+
dispose[Symbol.dispose] = dispose;
|
|
383
|
+
|
|
384
|
+
return dispose;
|
|
258
385
|
}
|
|
259
386
|
|
|
260
387
|
function createSignal(value, options = {}) {
|
|
261
388
|
const equals = options?.equals ?? Object.is;
|
|
389
|
+
let revision = 0;
|
|
262
390
|
|
|
263
391
|
const observable = new Observable();
|
|
264
392
|
function get() {
|
|
265
|
-
if (
|
|
393
|
+
if (context) {
|
|
266
394
|
context.dependencies.set(
|
|
267
395
|
observable,
|
|
268
396
|
observable.subscribe(context.observer),
|
|
269
397
|
);
|
|
270
|
-
}
|
|
271
398
|
|
|
272
|
-
|
|
273
|
-
throw value;
|
|
399
|
+
context.sourceRevisions.set(signal, revision);
|
|
274
400
|
}
|
|
275
401
|
|
|
276
402
|
return value;
|
|
277
403
|
}
|
|
278
404
|
|
|
279
|
-
|
|
405
|
+
function peek() {
|
|
406
|
+
return value;
|
|
407
|
+
}
|
|
408
|
+
|
|
280
409
|
function set(_value) {
|
|
281
410
|
if (!equals(value, _value)) {
|
|
282
411
|
value = _value;
|
|
412
|
+
revision++;
|
|
283
413
|
|
|
284
|
-
|
|
414
|
+
if (batchDepth > 0) {
|
|
415
|
+
batchQueue.add(observable);
|
|
416
|
+
} else {
|
|
417
|
+
observable.next();
|
|
418
|
+
}
|
|
285
419
|
}
|
|
286
420
|
|
|
287
421
|
return value;
|
|
288
422
|
}
|
|
289
423
|
|
|
290
|
-
|
|
424
|
+
function getRevision() {
|
|
425
|
+
return revision;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const signal = {
|
|
429
|
+
get,
|
|
430
|
+
set,
|
|
431
|
+
peek,
|
|
432
|
+
getRevision,
|
|
433
|
+
[INTERNAL_OBSERVABLE]: observable,
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
return signal;
|
|
291
437
|
}
|
|
292
438
|
|
|
293
439
|
// alias
|
|
294
440
|
const state = createSignal;
|
|
295
441
|
|
|
296
|
-
export { computed, createSignal, createWatcher, effect, getPending, state, untrack, unwatch, watch };
|
|
442
|
+
export { afterFlush, batch, computed, createSignal, createWatcher, effect, getPending, onFlush, state, untrack, unwatch, watch };
|