async-reactivity 2.0.25 → 2.0.27
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/lib/computed.js +38 -126
- package/lib/computed.test.js +294 -269
- package/lib/effect.js +130 -0
- package/lib/effect.test.js +192 -0
- package/lib/listener.test.js +1 -1
- package/lib/ref.js +17 -6
- package/lib/watcher.js +9 -37
- package/package.json +6 -6
- package/src/computed.test.ts +304 -274
- package/src/computed.ts +40 -131
- package/src/effect.test.ts +246 -0
- package/src/effect.ts +149 -0
- package/src/listener.test.ts +1 -1
- package/src/ref.ts +21 -6
- package/src/tsconfig.json +3 -3
- package/src/watcher.ts +9 -43
- package/types/computed.d.ts +7 -19
- package/types/computed.d.ts.map +1 -1
- package/types/effect.d.ts +27 -0
- package/types/effect.d.ts.map +1 -0
- package/types/effect.test.d.ts +2 -0
- package/types/effect.test.d.ts.map +1 -0
- package/types/ref.d.ts +7 -2
- package/types/ref.d.ts.map +1 -1
- package/types/watcher.d.ts +2 -9
- package/types/watcher.d.ts.map +1 -1
- package/lib/tracker.js +0 -18
- package/src/tracker.ts +0 -24
- package/types/tracker.d.ts +0 -10
- package/types/tracker.d.ts.map +0 -1
package/lib/effect.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export var EffectState;
|
|
2
|
+
(function (EffectState) {
|
|
3
|
+
EffectState[EffectState["Initial"] = 0] = "Initial";
|
|
4
|
+
EffectState[EffectState["Running"] = 1] = "Running";
|
|
5
|
+
EffectState[EffectState["Waiting"] = 2] = "Waiting";
|
|
6
|
+
EffectState[EffectState["Scheduled"] = 3] = "Scheduled";
|
|
7
|
+
})(EffectState || (EffectState = {}));
|
|
8
|
+
;
|
|
9
|
+
export const InSyncSymbol = Symbol('inSync');
|
|
10
|
+
export default class Effect {
|
|
11
|
+
effect;
|
|
12
|
+
computed;
|
|
13
|
+
state = EffectState.Initial;
|
|
14
|
+
dependencies = new Map(); // boolean indicates whether effect is in sync with the dependency
|
|
15
|
+
abortHandler;
|
|
16
|
+
constructor(effect, computed = false) {
|
|
17
|
+
this.effect = effect;
|
|
18
|
+
this.computed = computed;
|
|
19
|
+
if (!computed) {
|
|
20
|
+
this.run();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
innerTrackDependency(dependency) {
|
|
24
|
+
if (dependency === undefined) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
this.dependencies.set(dependency, true);
|
|
28
|
+
dependency.addDependent(this);
|
|
29
|
+
return dependency.value;
|
|
30
|
+
}
|
|
31
|
+
trackDependency = this.innerTrackDependency.bind(this);
|
|
32
|
+
run() {
|
|
33
|
+
let firstRun = this.state === EffectState.Initial;
|
|
34
|
+
if (this.state === EffectState.Scheduled) {
|
|
35
|
+
const inSync = [...this.dependencies.entries()].every(([d, inSync]) => {
|
|
36
|
+
if (!inSync) {
|
|
37
|
+
d.value;
|
|
38
|
+
return this.dependencies.get(d);
|
|
39
|
+
}
|
|
40
|
+
return inSync;
|
|
41
|
+
});
|
|
42
|
+
if (inSync) {
|
|
43
|
+
this.state = EffectState.Waiting;
|
|
44
|
+
return InSyncSymbol;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
this.state = EffectState.Running;
|
|
48
|
+
const getResult = () => {
|
|
49
|
+
this.clearDependencies(runPromise);
|
|
50
|
+
this.abortHandler = (() => {
|
|
51
|
+
const handler = {
|
|
52
|
+
promise: undefined,
|
|
53
|
+
abort: () => { },
|
|
54
|
+
aborted: false,
|
|
55
|
+
controller: new AbortController()
|
|
56
|
+
};
|
|
57
|
+
handler.promise = new Promise((resolve) => {
|
|
58
|
+
handler.abort = () => {
|
|
59
|
+
handler.aborted = true;
|
|
60
|
+
handler.controller.abort();
|
|
61
|
+
resolve();
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
return handler;
|
|
65
|
+
})();
|
|
66
|
+
const completeRun = (resolve, result, err, async) => {
|
|
67
|
+
if (this.abortHandler.aborted) {
|
|
68
|
+
firstRun = false;
|
|
69
|
+
if (this.computed || async) {
|
|
70
|
+
return getResult();
|
|
71
|
+
}
|
|
72
|
+
return Promise.resolve().then(() => getResult());
|
|
73
|
+
}
|
|
74
|
+
this.state = EffectState.Waiting;
|
|
75
|
+
this.abortHandler = undefined;
|
|
76
|
+
resolveRunPromise();
|
|
77
|
+
if (resolve) {
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
try {
|
|
85
|
+
const resultMaybePromise = this.effect(this.trackDependency, firstRun, this.abortHandler.controller.signal);
|
|
86
|
+
if (resultMaybePromise instanceof Promise) {
|
|
87
|
+
return Promise.race([resultMaybePromise, this.abortHandler.promise])
|
|
88
|
+
.then((result) => completeRun(true, result, undefined, true))
|
|
89
|
+
.catch((err) => completeRun(false, undefined, err, true));
|
|
90
|
+
}
|
|
91
|
+
return completeRun(true, resultMaybePromise, undefined, false);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
return completeRun(false, undefined, err, false);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
let resolveRunPromise;
|
|
98
|
+
const runPromise = new Promise(resolve => {
|
|
99
|
+
resolveRunPromise = resolve;
|
|
100
|
+
});
|
|
101
|
+
return getResult();
|
|
102
|
+
}
|
|
103
|
+
clearDependencies(promise) {
|
|
104
|
+
for (const dependency of this.dependencies.keys()) {
|
|
105
|
+
dependency.removeDependent(this, promise);
|
|
106
|
+
}
|
|
107
|
+
this.dependencies.clear();
|
|
108
|
+
}
|
|
109
|
+
invalidate(dependency) {
|
|
110
|
+
if (dependency) {
|
|
111
|
+
this.dependencies.set(dependency, false);
|
|
112
|
+
}
|
|
113
|
+
if (this.state === EffectState.Running && !this.abortHandler?.aborted) {
|
|
114
|
+
this.abortHandler?.abort();
|
|
115
|
+
}
|
|
116
|
+
else if (this.state === EffectState.Waiting) {
|
|
117
|
+
this.state = EffectState.Scheduled;
|
|
118
|
+
Promise.resolve().then(this.run.bind(this));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
validate(dependency) {
|
|
122
|
+
this.dependencies.set(dependency, true);
|
|
123
|
+
}
|
|
124
|
+
dispose() {
|
|
125
|
+
this.clearDependencies();
|
|
126
|
+
this.abortHandler?.abort();
|
|
127
|
+
this.abortHandler = undefined;
|
|
128
|
+
this.state = EffectState.Initial;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import 'mocha';
|
|
2
|
+
import assert from 'assert';
|
|
3
|
+
import Effect from './effect.js';
|
|
4
|
+
import Ref from './ref.js';
|
|
5
|
+
import Computed from './computed.js';
|
|
6
|
+
import { values } from 'lodash-es';
|
|
7
|
+
describe('effect', function () {
|
|
8
|
+
describe('sync', function () {
|
|
9
|
+
it('should run effect immediately', function () {
|
|
10
|
+
let count = 0;
|
|
11
|
+
new Effect(() => {
|
|
12
|
+
count++;
|
|
13
|
+
});
|
|
14
|
+
assert.strictEqual(count, 1);
|
|
15
|
+
});
|
|
16
|
+
it('should not run immediately after dependency is changed', function () {
|
|
17
|
+
let count = 0;
|
|
18
|
+
const a = new Ref(5);
|
|
19
|
+
new Effect((value) => {
|
|
20
|
+
value(a);
|
|
21
|
+
count++;
|
|
22
|
+
});
|
|
23
|
+
assert.strictEqual(count, 1);
|
|
24
|
+
a.value = 4;
|
|
25
|
+
assert.strictEqual(count, 1);
|
|
26
|
+
});
|
|
27
|
+
it('should run effect when dependency (ref) changes', async function () {
|
|
28
|
+
let count = 0;
|
|
29
|
+
const a = new Ref(5);
|
|
30
|
+
new Effect((value) => {
|
|
31
|
+
value(a);
|
|
32
|
+
count++;
|
|
33
|
+
});
|
|
34
|
+
a.value = 4;
|
|
35
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
36
|
+
assert.strictEqual(count, 2);
|
|
37
|
+
});
|
|
38
|
+
it('should indicate first run', async function () {
|
|
39
|
+
let count = 0;
|
|
40
|
+
const a = new Ref(5);
|
|
41
|
+
new Effect((_value, firstRun) => {
|
|
42
|
+
values(a);
|
|
43
|
+
assert.strictEqual(firstRun, count === 0);
|
|
44
|
+
count++;
|
|
45
|
+
});
|
|
46
|
+
a.value = 4;
|
|
47
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
48
|
+
});
|
|
49
|
+
it('should run effect once when dependency (ref) changes multiple times synchronously', async function () {
|
|
50
|
+
let count = 0;
|
|
51
|
+
const a = new Ref(5);
|
|
52
|
+
new Effect((value) => {
|
|
53
|
+
value(a);
|
|
54
|
+
count++;
|
|
55
|
+
});
|
|
56
|
+
a.value = 4;
|
|
57
|
+
a.value = 3;
|
|
58
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
59
|
+
assert.strictEqual(count, 2);
|
|
60
|
+
});
|
|
61
|
+
it('should run effect when dependency (computed) changes', async function () {
|
|
62
|
+
let count = 0;
|
|
63
|
+
const a = new Ref(5);
|
|
64
|
+
const b = new Computed(value => value(a) + 1);
|
|
65
|
+
new Effect((value) => {
|
|
66
|
+
value(b);
|
|
67
|
+
count++;
|
|
68
|
+
});
|
|
69
|
+
assert.strictEqual(count, 1);
|
|
70
|
+
a.value = 4;
|
|
71
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
72
|
+
assert.strictEqual(count, 2);
|
|
73
|
+
});
|
|
74
|
+
it('should not run effect when dependency changes but value is the same', async function () {
|
|
75
|
+
let count = 0;
|
|
76
|
+
const a = new Ref(5);
|
|
77
|
+
const b = new Computed(value => value(a) % 2);
|
|
78
|
+
new Effect((value) => {
|
|
79
|
+
value(b);
|
|
80
|
+
count++;
|
|
81
|
+
});
|
|
82
|
+
assert.strictEqual(count, 1);
|
|
83
|
+
a.value = 3;
|
|
84
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
85
|
+
assert.strictEqual(count, 1);
|
|
86
|
+
});
|
|
87
|
+
it('should debounce and rerun effect if dependency changes while running', async function () {
|
|
88
|
+
let count = 0;
|
|
89
|
+
const a = new Ref(5);
|
|
90
|
+
new Effect((value) => {
|
|
91
|
+
value(a);
|
|
92
|
+
count++;
|
|
93
|
+
a.value = 6;
|
|
94
|
+
});
|
|
95
|
+
assert.strictEqual(count, 1);
|
|
96
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
97
|
+
assert.strictEqual(count, 2);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe('async', function () {
|
|
101
|
+
it('should await effect', async function () {
|
|
102
|
+
const e = new Effect(async () => {
|
|
103
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
104
|
+
});
|
|
105
|
+
// @ts-expect-error
|
|
106
|
+
assert.strictEqual(e.state, 1);
|
|
107
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
108
|
+
// @ts-expect-error
|
|
109
|
+
assert.strictEqual(e.state, 2);
|
|
110
|
+
await e.run();
|
|
111
|
+
// @ts-expect-error
|
|
112
|
+
assert.strictEqual(e.state, 2);
|
|
113
|
+
});
|
|
114
|
+
it('should respect async dependencies', async function () {
|
|
115
|
+
let count = 0;
|
|
116
|
+
const a = new Ref(5);
|
|
117
|
+
const b = new Ref(5);
|
|
118
|
+
new Effect(async (value) => {
|
|
119
|
+
value(a);
|
|
120
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
121
|
+
value(b);
|
|
122
|
+
count++;
|
|
123
|
+
});
|
|
124
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
125
|
+
b.value = 4;
|
|
126
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
127
|
+
assert.strictEqual(count, 2);
|
|
128
|
+
});
|
|
129
|
+
it('should abort previous run if dependency changes while running', async function () {
|
|
130
|
+
let count = 0;
|
|
131
|
+
const a = new Ref(5);
|
|
132
|
+
new Effect(async (value, _firstRun, abortSignal) => {
|
|
133
|
+
value(a);
|
|
134
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
135
|
+
assert.strictEqual(abortSignal.aborted, count === 0);
|
|
136
|
+
count++;
|
|
137
|
+
});
|
|
138
|
+
a.value = 4;
|
|
139
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
140
|
+
assert.strictEqual(count, 2);
|
|
141
|
+
});
|
|
142
|
+
it('should not abort if effect completes', async function () {
|
|
143
|
+
let count = 0;
|
|
144
|
+
const a = new Ref(5);
|
|
145
|
+
new Effect(async (value, _firstRun, abortSignal) => {
|
|
146
|
+
value(a);
|
|
147
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
148
|
+
assert.strictEqual(abortSignal.aborted, false);
|
|
149
|
+
count++;
|
|
150
|
+
});
|
|
151
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
152
|
+
a.value = 4;
|
|
153
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
154
|
+
a.value = 3;
|
|
155
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
156
|
+
assert.strictEqual(count, 3);
|
|
157
|
+
});
|
|
158
|
+
it('should not care if promise rejects', async function () {
|
|
159
|
+
let count = 0;
|
|
160
|
+
const a = new Ref(5);
|
|
161
|
+
new Effect(async (value) => {
|
|
162
|
+
value(a);
|
|
163
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
164
|
+
count++;
|
|
165
|
+
throw new Error('Test error');
|
|
166
|
+
});
|
|
167
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
168
|
+
a.value = 4;
|
|
169
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
170
|
+
a.value = 3;
|
|
171
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
172
|
+
assert.strictEqual(count, 3);
|
|
173
|
+
});
|
|
174
|
+
it('should not dispose computed while effect is running', async function () {
|
|
175
|
+
let gate = 0;
|
|
176
|
+
const a = new Ref(5);
|
|
177
|
+
const b = new Computed(() => {
|
|
178
|
+
gate++;
|
|
179
|
+
return 1;
|
|
180
|
+
}, undefined, 0);
|
|
181
|
+
new Effect(async (value) => {
|
|
182
|
+
value(a);
|
|
183
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
184
|
+
value(b);
|
|
185
|
+
});
|
|
186
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
187
|
+
a.value = 4;
|
|
188
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
189
|
+
assert.strictEqual(gate, 1);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
package/lib/listener.test.js
CHANGED
|
@@ -21,7 +21,7 @@ describe('listener', function () {
|
|
|
21
21
|
const onChange = mock.fn();
|
|
22
22
|
new Watcher(listener, onChange);
|
|
23
23
|
assert.strictEqual(onChange.mock.callCount(), 1);
|
|
24
|
-
assert.deepStrictEqual(onChange.mock.calls[0].arguments, [1]);
|
|
24
|
+
assert.deepStrictEqual(onChange.mock.calls[0].arguments, [1, undefined]);
|
|
25
25
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
26
26
|
assert.strictEqual(onChange.mock.callCount(), 2);
|
|
27
27
|
assert.deepStrictEqual(onChange.mock.calls[1].arguments, [2, 1]);
|
package/lib/ref.js
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
import Tracker from "./tracker.js";
|
|
2
1
|
import defaultIsEqual from "./defaultIsEqual.js";
|
|
3
|
-
export default class Ref
|
|
2
|
+
export default class Ref {
|
|
4
3
|
isEqual;
|
|
4
|
+
dependents = new Set();
|
|
5
|
+
_value;
|
|
5
6
|
constructor(_value, isEqual = (defaultIsEqual)) {
|
|
6
|
-
super();
|
|
7
7
|
this._value = _value;
|
|
8
8
|
this.isEqual = isEqual;
|
|
9
9
|
}
|
|
10
10
|
set value(_value) {
|
|
11
|
-
const
|
|
11
|
+
const oldValue = this._value;
|
|
12
12
|
this._value = _value;
|
|
13
|
-
if (!this.isEqual(
|
|
13
|
+
if (!this.isEqual(_value, oldValue)) {
|
|
14
14
|
this.invalidate();
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
get value() {
|
|
18
|
-
return
|
|
18
|
+
return this._value;
|
|
19
|
+
}
|
|
20
|
+
addDependent(dependent) {
|
|
21
|
+
this.dependents.add(dependent);
|
|
22
|
+
}
|
|
23
|
+
removeDependent(dependent) {
|
|
24
|
+
this.dependents.delete(dependent);
|
|
25
|
+
}
|
|
26
|
+
invalidate() {
|
|
27
|
+
for (const dependent of this.dependents) {
|
|
28
|
+
dependent.invalidate(this);
|
|
29
|
+
}
|
|
19
30
|
}
|
|
20
31
|
}
|
package/lib/watcher.js
CHANGED
|
@@ -1,42 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
(function (WatchState) {
|
|
4
|
-
WatchState[WatchState["Uncertain"] = 0] = "Uncertain";
|
|
5
|
-
WatchState[WatchState["Valid"] = 1] = "Valid";
|
|
6
|
-
})(WatchState || (WatchState = {}));
|
|
7
|
-
;
|
|
8
|
-
export default class Watcher extends Tracker {
|
|
9
|
-
onChange;
|
|
10
|
-
dependency;
|
|
11
|
-
state = WatchState.Valid;
|
|
1
|
+
import Effect from "./effect.js";
|
|
2
|
+
export default class Watcher extends Effect {
|
|
12
3
|
constructor(dependency, onChange, immediate = true) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
invalidate() {
|
|
23
|
-
if (this.state === WatchState.Uncertain) {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
this.state = WatchState.Uncertain;
|
|
27
|
-
Promise.resolve().then(() => {
|
|
28
|
-
const oldValue = this._value;
|
|
29
|
-
this._value = this.dependency.value;
|
|
30
|
-
if (this.state === WatchState.Uncertain) {
|
|
31
|
-
this.onChange(this._value, oldValue);
|
|
32
|
-
this.state = WatchState.Valid;
|
|
4
|
+
let oldValue = undefined;
|
|
5
|
+
super((value, firstRun) => {
|
|
6
|
+
const v = value(dependency);
|
|
7
|
+
const localOldValue = oldValue;
|
|
8
|
+
oldValue = v;
|
|
9
|
+
if (!firstRun || immediate) {
|
|
10
|
+
onChange(v, localOldValue);
|
|
33
11
|
}
|
|
34
12
|
});
|
|
35
13
|
}
|
|
36
|
-
validate() {
|
|
37
|
-
this.state = WatchState.Valid;
|
|
38
|
-
}
|
|
39
|
-
dispose() {
|
|
40
|
-
this.dependency.removeDependent(this);
|
|
41
|
-
}
|
|
42
14
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "async-reactivity",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.27",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -23,12 +23,12 @@
|
|
|
23
23
|
"license": "ISC",
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/lodash-es": "^4.17.12",
|
|
26
|
-
"@types/mocha": "^10.0.
|
|
27
|
-
"@types/node": "^
|
|
28
|
-
"mocha": "^
|
|
29
|
-
"typescript": "^5.
|
|
26
|
+
"@types/mocha": "^10.0.10",
|
|
27
|
+
"@types/node": "^24.11.0",
|
|
28
|
+
"mocha": "^11.7.5",
|
|
29
|
+
"typescript": "^5.9.3"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"lodash-es": "^4.17.
|
|
32
|
+
"lodash-es": "^4.17.23"
|
|
33
33
|
}
|
|
34
34
|
}
|