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/src/computed.ts
CHANGED
|
@@ -1,39 +1,31 @@
|
|
|
1
1
|
import Dependency from "./dependency.js";
|
|
2
2
|
import Dependent from "./dependent.js";
|
|
3
|
-
import Tracker from "./tracker.js";
|
|
4
3
|
import defaultIsEqual from "./defaultIsEqual.js";
|
|
5
4
|
import { Deferrer } from "./deferrer.js";
|
|
6
5
|
import { debounce } from 'lodash-es';
|
|
6
|
+
import Effect, { EffectState, InSyncSymbol } from "./effect.js";
|
|
7
7
|
|
|
8
8
|
export declare type TrackValue = (<T>(dependency: Dependency<T>) => T) & (<T>(dependency: Dependency<T> | undefined) => T | undefined);
|
|
9
9
|
export declare type ComputeFunc<T> = (value: TrackValue, previousValue: T | undefined, abortSignal: AbortSignal) => T;
|
|
10
10
|
export declare type ComputeFuncScoped<T1, T2> = (value: TrackValue, scope: T1, previousValue: T2 | undefined, abortSignal: AbortSignal) => T2;
|
|
11
11
|
|
|
12
|
-
enum ComputedState {
|
|
13
|
-
Invalid,
|
|
14
|
-
Valid,
|
|
15
|
-
Uncertain,
|
|
16
|
-
Computing
|
|
17
|
-
}
|
|
18
|
-
|
|
19
12
|
class CircularDependencyError extends Error { }
|
|
20
13
|
|
|
21
|
-
export default class Computed<T> extends
|
|
14
|
+
export default class Computed<T> extends Effect implements Dependent, Dependency<T> {
|
|
15
|
+
private _value?: T;
|
|
22
16
|
getter: ComputeFunc<T>;
|
|
23
17
|
isEqual: typeof defaultIsEqual<T>;
|
|
24
|
-
private
|
|
25
|
-
private dependencies = new Map<Dependency<unknown>, boolean>(); // boolean indicates whether value is up to date in regards to the dependency (false - up to date, true - needs update)
|
|
26
|
-
private computePromise?: Promise<unknown>;
|
|
27
|
-
private computePromiseActions?: { resolve: Function, reject: Function };
|
|
28
|
-
private lastComputeAttemptPromise?: Promise<void>;
|
|
18
|
+
private dependents = new Set<Dependent>();
|
|
29
19
|
private deferrer?: Deferrer;
|
|
30
|
-
private abortController?: AbortController;
|
|
31
20
|
|
|
32
21
|
constructor(getter: ComputeFunc<T>, isEqual = defaultIsEqual<T>, timeToLive?: number) {
|
|
33
|
-
|
|
22
|
+
|
|
23
|
+
super(((value, _firstRun, abortSignal) => {
|
|
24
|
+
return getter(value, this._value, abortSignal);
|
|
25
|
+
}), true);
|
|
26
|
+
|
|
34
27
|
this.getter = getter;
|
|
35
28
|
this.isEqual = isEqual;
|
|
36
|
-
this.prepareComputePromise();
|
|
37
29
|
|
|
38
30
|
if (timeToLive !== undefined) {
|
|
39
31
|
this.deferrer = new Deferrer(debounce(() => {
|
|
@@ -44,149 +36,66 @@ export default class Computed<T> extends Tracker<T> implements Dependent, Depend
|
|
|
44
36
|
}
|
|
45
37
|
}
|
|
46
38
|
|
|
47
|
-
private prepareComputePromise() {
|
|
48
|
-
this.computePromise = new Promise((resolve, reject) => {
|
|
49
|
-
this.computePromiseActions = {
|
|
50
|
-
resolve,
|
|
51
|
-
reject
|
|
52
|
-
};
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
39
|
public get value(): T {
|
|
57
|
-
if (this.state ===
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
private compute() {
|
|
65
|
-
if (this.state === ComputedState.Uncertain) {
|
|
66
|
-
const upToDate = ![...this.dependencies.entries()].some(([d, needsUpdate]) => {
|
|
67
|
-
if (needsUpdate && d instanceof Computed && d.state === ComputedState.Uncertain) {
|
|
68
|
-
d.value;
|
|
69
|
-
return this.dependencies.get(d);
|
|
40
|
+
if (this.state === EffectState.Initial || this.state === EffectState.Scheduled) {
|
|
41
|
+
const oldValue = this._value!;
|
|
42
|
+
const newValue = this.run() as T;
|
|
43
|
+
if (newValue !== InSyncSymbol) {
|
|
44
|
+
this._value = newValue;
|
|
45
|
+
if (this.isEqual(this._value!, oldValue)) {
|
|
46
|
+
this.validateDependents();
|
|
70
47
|
}
|
|
71
|
-
return needsUpdate;
|
|
72
|
-
});
|
|
73
|
-
if (upToDate) {
|
|
74
|
-
this.finalizeComputing();
|
|
75
|
-
this.validateDependents();
|
|
76
|
-
return this._value!;
|
|
77
48
|
}
|
|
78
|
-
} else if (this.state === ComputedState.Computing) {
|
|
79
|
-
this.abortController?.abort();
|
|
80
49
|
}
|
|
81
50
|
|
|
82
|
-
this.state = ComputedState.Computing;
|
|
83
|
-
this.clearDependencies(true);
|
|
84
|
-
this.abortController = new AbortController();
|
|
85
|
-
|
|
86
|
-
const newValue: T = this.getter(this.trackDependency, this._value, this.abortController.signal);
|
|
87
|
-
if (this.isEqual(newValue, this._value!)) {
|
|
88
|
-
this.handlePromiseThen(this.lastComputeAttemptPromise!, this._value);
|
|
89
|
-
this.validateDependents();
|
|
90
|
-
} else if (newValue instanceof Promise) {
|
|
91
|
-
const computeAttemptPromise = newValue
|
|
92
|
-
.then(result => this.handlePromiseThen(computeAttemptPromise, result))
|
|
93
|
-
.catch(error => this.handlePromiseCatch(computeAttemptPromise, error)) as Promise<void>;
|
|
94
|
-
this.lastComputeAttemptPromise = computeAttemptPromise;
|
|
95
|
-
this._value = this.computePromise! as T;
|
|
96
|
-
} else {
|
|
97
|
-
this._value = newValue;
|
|
98
|
-
this.handlePromiseThen(this.lastComputeAttemptPromise!, this._value);
|
|
99
|
-
}
|
|
100
51
|
return this._value!;
|
|
101
52
|
}
|
|
102
53
|
|
|
103
|
-
private
|
|
104
|
-
for (const
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
this.dependencies.clear();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
private handlePromiseThen(computeAttemptPromise: Promise<void>, result: any) {
|
|
111
|
-
if (this.lastComputeAttemptPromise === computeAttemptPromise) {
|
|
112
|
-
this.computePromiseActions!.resolve(result);
|
|
113
|
-
this.finalizeComputing();
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
private handlePromiseCatch(computeAttemptPromise: Promise<void>, error: any) {
|
|
118
|
-
if (this.lastComputeAttemptPromise === computeAttemptPromise) {
|
|
119
|
-
this.computePromiseActions!.reject(error);
|
|
120
|
-
this.finalizeComputing();
|
|
54
|
+
private validateDependents() {
|
|
55
|
+
for (const dependent of this.dependents.keys()) {
|
|
56
|
+
dependent.validate(this);
|
|
121
57
|
}
|
|
122
58
|
}
|
|
123
59
|
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
return undefined;
|
|
127
|
-
}
|
|
128
|
-
if (this.dependents.has(dependency as any)) {
|
|
60
|
+
public addDependent(dependent: Dependent) {
|
|
61
|
+
if (this.dependencies.has(dependent as any)) {
|
|
129
62
|
throw new CircularDependencyError();
|
|
130
63
|
}
|
|
131
|
-
this.
|
|
132
|
-
dependency.addDependent(this);
|
|
133
|
-
return dependency.value;
|
|
64
|
+
this.dependents.add(dependent);
|
|
134
65
|
}
|
|
135
66
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
this.state = ComputedState.Valid;
|
|
140
|
-
this.lastComputeAttemptPromise = undefined;
|
|
141
|
-
this.abortController = undefined;
|
|
142
|
-
this.prepareComputePromise();
|
|
67
|
+
public removeDependent(dependent: Dependent, promise = Promise.resolve()): void {
|
|
68
|
+
this.dependents.delete(dependent);
|
|
69
|
+
this.deferrer?.finally(promise);
|
|
143
70
|
}
|
|
144
71
|
|
|
145
72
|
public invalidate(dependency?: Dependency<unknown>) {
|
|
146
|
-
if (this.state ===
|
|
147
|
-
this.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
this.state = ComputedState.Uncertain;
|
|
151
|
-
if (dependency) {
|
|
152
|
-
this.dependencies.set(dependency, true);
|
|
153
|
-
} else {
|
|
154
|
-
for (const dep of this.dependencies.keys()) {
|
|
155
|
-
this.dependencies.set(dep, true);
|
|
156
|
-
}
|
|
73
|
+
if (this.state === EffectState.Waiting) {
|
|
74
|
+
this.state = EffectState.Scheduled;
|
|
75
|
+
for (const dependent of this.dependents) {
|
|
76
|
+
dependent.invalidate(this);
|
|
157
77
|
}
|
|
158
|
-
super.invalidate();
|
|
159
78
|
}
|
|
79
|
+
|
|
80
|
+
super.invalidate(dependency);
|
|
160
81
|
}
|
|
161
82
|
|
|
162
83
|
public forceInvalidate() {
|
|
163
|
-
this.
|
|
164
|
-
|
|
165
|
-
|
|
84
|
+
if (this.dependencies.size === 0) {
|
|
85
|
+
this.invalidate();
|
|
86
|
+
} else {
|
|
87
|
+
for (const dependency of this.dependencies.keys()) {
|
|
88
|
+
this.invalidate(dependency);
|
|
89
|
+
}
|
|
166
90
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
public validate(dependency: Dependency<unknown>) {
|
|
170
|
-
this.dependencies.set(dependency, false);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private validateDependents() {
|
|
174
|
-
for (const dependent of this.dependents.keys()) {
|
|
175
|
-
dependent.validate(this);
|
|
91
|
+
if (this.state !== EffectState.Running) {
|
|
92
|
+
this.state = EffectState.Initial;
|
|
176
93
|
}
|
|
177
94
|
}
|
|
178
95
|
|
|
179
|
-
public removeDependent(dependent: Dependent, promise = Promise.resolve()): void {
|
|
180
|
-
super.removeDependent(dependent);
|
|
181
|
-
this.deferrer?.finally(promise);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
96
|
public reset() {
|
|
185
|
-
this.clearDependencies(false);
|
|
186
|
-
this.state = ComputedState.Invalid;
|
|
187
97
|
this._value = undefined;
|
|
188
|
-
|
|
189
|
-
this.prepareComputePromise();
|
|
98
|
+
super.dispose();
|
|
190
99
|
}
|
|
191
100
|
|
|
192
101
|
public dispose() {
|
|
@@ -0,0 +1,246 @@
|
|
|
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
|
+
|
|
8
|
+
describe('effect', function () {
|
|
9
|
+
describe('sync', function () {
|
|
10
|
+
it('should run effect immediately', function () {
|
|
11
|
+
let count = 0;
|
|
12
|
+
|
|
13
|
+
new Effect(() => {
|
|
14
|
+
count++;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
assert.strictEqual(count, 1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should not run immediately after dependency is changed', function () {
|
|
21
|
+
let count = 0;
|
|
22
|
+
const a = new Ref(5);
|
|
23
|
+
|
|
24
|
+
new Effect((value) => {
|
|
25
|
+
value(a);
|
|
26
|
+
count++;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
assert.strictEqual(count, 1);
|
|
30
|
+
a.value = 4;
|
|
31
|
+
assert.strictEqual(count, 1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should run effect when dependency (ref) changes', async function () {
|
|
35
|
+
let count = 0;
|
|
36
|
+
const a = new Ref(5);
|
|
37
|
+
|
|
38
|
+
new Effect((value) => {
|
|
39
|
+
value(a);
|
|
40
|
+
count++;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
a.value = 4;
|
|
44
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
45
|
+
|
|
46
|
+
assert.strictEqual(count, 2);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should indicate first run', async function () {
|
|
50
|
+
let count = 0;
|
|
51
|
+
const a = new Ref(5);
|
|
52
|
+
|
|
53
|
+
new Effect((_value, firstRun) => {
|
|
54
|
+
values(a);
|
|
55
|
+
assert.strictEqual(firstRun, count === 0);
|
|
56
|
+
count++;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
a.value = 4;
|
|
60
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should run effect once when dependency (ref) changes multiple times synchronously', async function () {
|
|
64
|
+
let count = 0;
|
|
65
|
+
const a = new Ref(5);
|
|
66
|
+
|
|
67
|
+
new Effect((value) => {
|
|
68
|
+
value(a);
|
|
69
|
+
count++;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
a.value = 4;
|
|
73
|
+
a.value = 3;
|
|
74
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
75
|
+
|
|
76
|
+
assert.strictEqual(count, 2);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should run effect when dependency (computed) changes', async function () {
|
|
80
|
+
let count = 0;
|
|
81
|
+
const a = new Ref(5);
|
|
82
|
+
const b = new Computed(value => value(a) + 1);
|
|
83
|
+
|
|
84
|
+
new Effect((value) => {
|
|
85
|
+
value(b);
|
|
86
|
+
count++;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
assert.strictEqual(count, 1);
|
|
90
|
+
a.value = 4;
|
|
91
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
92
|
+
|
|
93
|
+
assert.strictEqual(count, 2);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should not run effect when dependency changes but value is the same', async function () {
|
|
97
|
+
let count = 0;
|
|
98
|
+
const a = new Ref(5);
|
|
99
|
+
const b = new Computed(value => value(a) % 2);
|
|
100
|
+
|
|
101
|
+
new Effect((value) => {
|
|
102
|
+
value(b);
|
|
103
|
+
count++;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
assert.strictEqual(count, 1);
|
|
107
|
+
a.value = 3;
|
|
108
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
109
|
+
|
|
110
|
+
assert.strictEqual(count, 1);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should debounce and rerun effect if dependency changes while running', async function () {
|
|
114
|
+
let count = 0;
|
|
115
|
+
const a = new Ref(5);
|
|
116
|
+
|
|
117
|
+
new Effect((value) => {
|
|
118
|
+
value(a);
|
|
119
|
+
count++;
|
|
120
|
+
a.value = 6;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
assert.strictEqual(count, 1);
|
|
124
|
+
|
|
125
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
126
|
+
assert.strictEqual(count, 2);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('async', function () {
|
|
131
|
+
it('should await effect', async function () {
|
|
132
|
+
const e = new Effect(async () => {
|
|
133
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
134
|
+
});
|
|
135
|
+
// @ts-expect-error
|
|
136
|
+
assert.strictEqual(e.state, 1);
|
|
137
|
+
|
|
138
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
139
|
+
// @ts-expect-error
|
|
140
|
+
assert.strictEqual(e.state, 2);
|
|
141
|
+
|
|
142
|
+
await e.run();
|
|
143
|
+
// @ts-expect-error
|
|
144
|
+
assert.strictEqual(e.state, 2);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should respect async dependencies', async function () {
|
|
148
|
+
let count = 0;
|
|
149
|
+
const a = new Ref(5);
|
|
150
|
+
const b = new Ref(5);
|
|
151
|
+
|
|
152
|
+
new Effect(async (value) => {
|
|
153
|
+
value(a);
|
|
154
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
155
|
+
value(b);
|
|
156
|
+
count++;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
160
|
+
b.value = 4;
|
|
161
|
+
|
|
162
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
163
|
+
assert.strictEqual(count, 2);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should abort previous run if dependency changes while running', async function () {
|
|
167
|
+
let count = 0;
|
|
168
|
+
const a = new Ref(5);
|
|
169
|
+
|
|
170
|
+
new Effect(async (value, _firstRun, abortSignal) => {
|
|
171
|
+
value(a);
|
|
172
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
173
|
+
assert.strictEqual(abortSignal.aborted, count === 0);
|
|
174
|
+
count++;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
a.value = 4;
|
|
178
|
+
|
|
179
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
180
|
+
assert.strictEqual(count, 2);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should not abort if effect completes', async function () {
|
|
184
|
+
let count = 0;
|
|
185
|
+
const a = new Ref(5);
|
|
186
|
+
|
|
187
|
+
new Effect(async (value, _firstRun, abortSignal) => {
|
|
188
|
+
value(a);
|
|
189
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
190
|
+
assert.strictEqual(abortSignal.aborted, false);
|
|
191
|
+
count++;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
195
|
+
a.value = 4;
|
|
196
|
+
|
|
197
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
198
|
+
a.value = 3;
|
|
199
|
+
|
|
200
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
201
|
+
assert.strictEqual(count, 3);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should not care if promise rejects', async function () {
|
|
205
|
+
let count = 0;
|
|
206
|
+
const a = new Ref(5);
|
|
207
|
+
|
|
208
|
+
new Effect(async (value) => {
|
|
209
|
+
value(a);
|
|
210
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
211
|
+
count++;
|
|
212
|
+
throw new Error('Test error');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
216
|
+
a.value = 4;
|
|
217
|
+
|
|
218
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
219
|
+
a.value = 3;
|
|
220
|
+
|
|
221
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
222
|
+
assert.strictEqual(count, 3);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should not dispose computed while effect is running', async function () {
|
|
226
|
+
let gate = 0;
|
|
227
|
+
const a = new Ref(5);
|
|
228
|
+
const b = new Computed(() => {
|
|
229
|
+
gate++;
|
|
230
|
+
return 1;
|
|
231
|
+
}, undefined, 0);
|
|
232
|
+
|
|
233
|
+
new Effect(async (value) => {
|
|
234
|
+
value(a);
|
|
235
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
236
|
+
value(b);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
240
|
+
a.value = 4;
|
|
241
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
242
|
+
|
|
243
|
+
assert.strictEqual(gate, 1);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
package/src/effect.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import Dependency from "./dependency.js";
|
|
2
|
+
import Dependent from "./dependent.js";
|
|
3
|
+
|
|
4
|
+
export declare type TrackValue = (<T>(dependency: Dependency<T>) => T) & (<T>(dependency: Dependency<T> | undefined) => T | undefined);
|
|
5
|
+
export declare type EffectFunc = (value: TrackValue, firstRun: boolean, abortSignal: AbortSignal) => unknown | Promise<unknown>;
|
|
6
|
+
|
|
7
|
+
export enum EffectState {
|
|
8
|
+
Initial,
|
|
9
|
+
Running,
|
|
10
|
+
Waiting,
|
|
11
|
+
Scheduled,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const InSyncSymbol = Symbol('inSync');
|
|
15
|
+
|
|
16
|
+
export default class Effect implements Dependent {
|
|
17
|
+
protected state = EffectState.Initial;
|
|
18
|
+
protected dependencies = new Map<Dependency<unknown>, boolean>(); // boolean indicates whether effect is in sync with the dependency
|
|
19
|
+
private abortHandler?: { promise: Promise<void>, abort: Function, aborted: boolean, controller: AbortController };
|
|
20
|
+
|
|
21
|
+
constructor(private readonly effect: EffectFunc, private readonly computed = false) {
|
|
22
|
+
if (!computed) {
|
|
23
|
+
this.run();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private innerTrackDependency(this: Effect, dependency?: Dependency<unknown>) {
|
|
28
|
+
if (dependency === undefined) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
this.dependencies.set(dependency, true);
|
|
32
|
+
dependency.addDependent(this);
|
|
33
|
+
return dependency.value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private trackDependency = this.innerTrackDependency.bind(this) as TrackValue;
|
|
37
|
+
|
|
38
|
+
run() {
|
|
39
|
+
let firstRun = this.state === EffectState.Initial;
|
|
40
|
+
|
|
41
|
+
if (this.state === EffectState.Scheduled) {
|
|
42
|
+
const inSync = [...this.dependencies.entries()].every(([d, inSync]) => {
|
|
43
|
+
if (!inSync) {
|
|
44
|
+
d.value;
|
|
45
|
+
return this.dependencies.get(d);
|
|
46
|
+
}
|
|
47
|
+
return inSync;
|
|
48
|
+
});
|
|
49
|
+
if (inSync) {
|
|
50
|
+
this.state = EffectState.Waiting;
|
|
51
|
+
return InSyncSymbol;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.state = EffectState.Running;
|
|
56
|
+
|
|
57
|
+
const getResult = (): unknown | Promise<unknown> => {
|
|
58
|
+
this.clearDependencies(runPromise);
|
|
59
|
+
this.abortHandler = (() => {
|
|
60
|
+
const handler = {
|
|
61
|
+
promise: undefined as unknown as Promise<void>,
|
|
62
|
+
abort: () => { },
|
|
63
|
+
aborted: false,
|
|
64
|
+
controller: new AbortController()
|
|
65
|
+
};
|
|
66
|
+
handler.promise = new Promise<void>((resolve) => {
|
|
67
|
+
handler.abort = () => {
|
|
68
|
+
handler.aborted = true;
|
|
69
|
+
handler.controller.abort();
|
|
70
|
+
resolve();
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
return handler;
|
|
74
|
+
})();
|
|
75
|
+
|
|
76
|
+
const completeRun = (resolve: boolean, result: unknown, err: unknown, async: boolean) => {
|
|
77
|
+
if (this.abortHandler!.aborted) {
|
|
78
|
+
firstRun = false;
|
|
79
|
+
if (this.computed || async) {
|
|
80
|
+
return getResult();
|
|
81
|
+
}
|
|
82
|
+
return Promise.resolve().then(() => getResult());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.state = EffectState.Waiting;
|
|
86
|
+
this.abortHandler = undefined;
|
|
87
|
+
resolveRunPromise();
|
|
88
|
+
|
|
89
|
+
if (resolve) {
|
|
90
|
+
return result;
|
|
91
|
+
} else {
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const resultMaybePromise = this.effect(this.trackDependency, firstRun, this.abortHandler.controller.signal);
|
|
98
|
+
if (resultMaybePromise instanceof Promise) {
|
|
99
|
+
return Promise.race([resultMaybePromise, this.abortHandler!.promise])
|
|
100
|
+
.then((result) => completeRun(true, result, undefined, true))
|
|
101
|
+
.catch((err) => completeRun(false, undefined, err, true));
|
|
102
|
+
}
|
|
103
|
+
return completeRun(true, resultMaybePromise, undefined, false);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
return completeRun(false, undefined, err, false);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
let resolveRunPromise: Function;
|
|
110
|
+
const runPromise = new Promise<void>(resolve => {
|
|
111
|
+
resolveRunPromise = resolve;
|
|
112
|
+
});
|
|
113
|
+
return getResult();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private clearDependencies(promise?: Promise<void>) {
|
|
117
|
+
for (const dependency of this.dependencies.keys()) {
|
|
118
|
+
dependency.removeDependent(this, promise);
|
|
119
|
+
}
|
|
120
|
+
this.dependencies.clear();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
invalidate(dependency?: Dependency<unknown>) {
|
|
124
|
+
if (dependency) {
|
|
125
|
+
this.dependencies.set(dependency, false);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (this.state === EffectState.Running && !this.abortHandler?.aborted) {
|
|
129
|
+
this.abortHandler?.abort();
|
|
130
|
+
} else if (this.state === EffectState.Waiting) {
|
|
131
|
+
this.state = EffectState.Scheduled;
|
|
132
|
+
Promise.resolve().then(this.run.bind(this));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
validate(dependency: Dependency<unknown>) {
|
|
137
|
+
this.dependencies.set(dependency, true);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
dispose() {
|
|
141
|
+
this.clearDependencies();
|
|
142
|
+
|
|
143
|
+
this.abortHandler?.abort();
|
|
144
|
+
this.abortHandler = undefined;
|
|
145
|
+
|
|
146
|
+
this.state = EffectState.Initial;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
}
|
package/src/listener.test.ts
CHANGED
|
@@ -36,7 +36,7 @@ describe('listener', function () {
|
|
|
36
36
|
new Watcher(listener, onChange);
|
|
37
37
|
|
|
38
38
|
assert.strictEqual(onChange.mock.callCount(), 1);
|
|
39
|
-
assert.deepStrictEqual(onChange.mock.calls[0].arguments, [1]);
|
|
39
|
+
assert.deepStrictEqual(onChange.mock.calls[0].arguments, [1, undefined]);
|
|
40
40
|
|
|
41
41
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
42
42
|
|
package/src/ref.ts
CHANGED
|
@@ -1,25 +1,40 @@
|
|
|
1
1
|
import Dependency from "./dependency.js";
|
|
2
|
-
import Tracker from "./tracker.js";
|
|
3
2
|
import defaultIsEqual from "./defaultIsEqual.js";
|
|
3
|
+
import { Dependent } from "./index.js";
|
|
4
4
|
|
|
5
|
-
export default class Ref<T>
|
|
5
|
+
export default class Ref<T> implements Dependency<T> {
|
|
6
6
|
private isEqual: typeof defaultIsEqual<T>;
|
|
7
|
+
protected dependents = new Set<Dependent>();
|
|
8
|
+
protected _value?: T;
|
|
7
9
|
|
|
8
10
|
constructor(_value: T, isEqual = defaultIsEqual<T>) {
|
|
9
|
-
super();
|
|
10
11
|
this._value = _value;
|
|
11
12
|
this.isEqual = isEqual;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
public set value(_value: T) {
|
|
15
|
-
const
|
|
16
|
+
const oldValue = this._value!;
|
|
16
17
|
this._value = _value;
|
|
17
|
-
if (!this.isEqual(
|
|
18
|
+
if (!this.isEqual(_value, oldValue)) {
|
|
18
19
|
this.invalidate();
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
public get value(): T {
|
|
23
|
-
return
|
|
24
|
+
return this._value!;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public addDependent(dependent: Dependent) {
|
|
28
|
+
this.dependents.add(dependent);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public removeDependent(dependent: Dependent) {
|
|
32
|
+
this.dependents.delete(dependent);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public invalidate(): void {
|
|
36
|
+
for (const dependent of this.dependents) {
|
|
37
|
+
dependent.invalidate(this);
|
|
38
|
+
}
|
|
24
39
|
}
|
|
25
40
|
}
|