@thomas-siegfried/tapout 0.0.1 → 0.0.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/LICENSE +21 -21
- package/README.md +1205 -1205
- package/package.json +55 -55
- package/src/applyBindings.ts +372 -372
- package/src/arrayToDomMapping.ts +300 -300
- package/src/attributeInterpolationMarkup.ts +91 -91
- package/src/bindingContext.ts +207 -207
- package/src/bindingEvent.ts +198 -198
- package/src/bindingProvider.ts +260 -260
- package/src/bindings.ts +963 -963
- package/src/compareArrays.ts +122 -122
- package/src/componentBinding.ts +318 -318
- package/src/componentDecorator.ts +62 -62
- package/src/components.ts +367 -367
- package/src/computed.ts +439 -439
- package/src/configure.ts +52 -52
- package/src/controlFlowBindings.ts +206 -206
- package/src/core.ts +60 -60
- package/src/decorators.ts +407 -407
- package/src/dependencyDetection.ts +76 -76
- package/src/disposable.ts +36 -36
- package/src/domData.ts +42 -42
- package/src/domNodeDisposal.ts +94 -94
- package/src/effects.ts +29 -29
- package/src/event.ts +173 -173
- package/src/expressionRewriting.ts +219 -219
- package/src/extenders.ts +102 -102
- package/src/filters.ts +91 -91
- package/src/index.ts +150 -150
- package/src/interpolationMarkup.ts +130 -130
- package/src/memoization.ts +71 -71
- package/src/namespacedBindings.ts +132 -132
- package/src/observable.ts +48 -48
- package/src/observableArray.ts +397 -397
- package/src/options.ts +25 -25
- package/src/selectExtensions.ts +68 -68
- package/src/slotBinding.ts +84 -84
- package/src/subscribable.ts +266 -266
- package/src/tasks.ts +79 -79
- package/src/templateEngine.ts +108 -108
- package/src/templateRendering.ts +399 -399
- package/src/templateRewriting.ts +94 -94
- package/src/templateSources.ts +134 -134
- package/src/utils.ts +123 -123
- package/src/utilsDom.ts +87 -87
- package/src/virtualElements.ts +153 -153
- package/src/wireParams.ts +49 -49
package/src/computed.ts
CHANGED
|
@@ -1,439 +1,439 @@
|
|
|
1
|
-
import { Subscribable, valuesArePrimitiveAndEqual } from './subscribable.js';
|
|
2
|
-
import type { ReadableSubscribable } from './subscribable.js';
|
|
3
|
-
import { begin, end, registerDependency } from './dependencyDetection.js';
|
|
4
|
-
import { options } from './options.js';
|
|
5
|
-
|
|
6
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
-
type AnySubscribable = Subscribable<any>;
|
|
8
|
-
|
|
9
|
-
interface DependencyTracking {
|
|
10
|
-
_target: AnySubscribable;
|
|
11
|
-
_order: number;
|
|
12
|
-
_version: number;
|
|
13
|
-
dispose?(): void;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ComputedOptions<T> {
|
|
17
|
-
read: () => T;
|
|
18
|
-
write?: (value: T) => void;
|
|
19
|
-
pure?: boolean;
|
|
20
|
-
deferEvaluation?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class Computed<T> extends Subscribable<T> implements ReadableSubscribable<T> {
|
|
24
|
-
private _latestValue: T | undefined = undefined;
|
|
25
|
-
private _readFunction: (() => T) | undefined;
|
|
26
|
-
private _writeFunction: ((value: T) => void) | undefined;
|
|
27
|
-
private _isDisposed = false;
|
|
28
|
-
private _isBeingEvaluated = false;
|
|
29
|
-
private _isDirty = true;
|
|
30
|
-
private _pure: boolean;
|
|
31
|
-
private _isSleeping: boolean;
|
|
32
|
-
private _dependencyTracking: Record<number, DependencyTracking | null> = {};
|
|
33
|
-
private _dependenciesCount = 0;
|
|
34
|
-
private _deferEvaluation: boolean;
|
|
35
|
-
|
|
36
|
-
constructor(evaluatorOrOptions: (() => T) | ComputedOptions<T>) {
|
|
37
|
-
super();
|
|
38
|
-
if (typeof evaluatorOrOptions === 'function') {
|
|
39
|
-
this._readFunction = evaluatorOrOptions;
|
|
40
|
-
this._pure = false;
|
|
41
|
-
this._deferEvaluation = false;
|
|
42
|
-
} else {
|
|
43
|
-
this._readFunction = evaluatorOrOptions.read;
|
|
44
|
-
this._writeFunction = evaluatorOrOptions.write;
|
|
45
|
-
this._pure = evaluatorOrOptions.pure ?? false;
|
|
46
|
-
this._deferEvaluation = evaluatorOrOptions.deferEvaluation ?? false;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
this._isSleeping = this._pure;
|
|
50
|
-
this.equalityComparer = valuesArePrimitiveAndEqual;
|
|
51
|
-
|
|
52
|
-
if (!this._isSleeping && !this._deferEvaluation) {
|
|
53
|
-
this.evaluateImmediate();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (options.deferUpdates) {
|
|
57
|
-
this.extend({ deferred: true });
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
get hasWriteFunction(): boolean {
|
|
62
|
-
return typeof this._writeFunction === 'function';
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
get isPure(): boolean {
|
|
66
|
-
return this._pure;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
get(): T {
|
|
70
|
-
if (!this._isDisposed) {
|
|
71
|
-
registerDependency(this);
|
|
72
|
-
}
|
|
73
|
-
if (this._isDirty || (this._isSleeping && this.haveDependenciesChanged())) {
|
|
74
|
-
this.evaluateImmediate();
|
|
75
|
-
}
|
|
76
|
-
return this._latestValue as T;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
peek(): T {
|
|
80
|
-
if (
|
|
81
|
-
(this._isDirty && !this._dependenciesCount) ||
|
|
82
|
-
(this._isSleeping && this.haveDependenciesChanged())
|
|
83
|
-
) {
|
|
84
|
-
this.evaluateImmediate();
|
|
85
|
-
}
|
|
86
|
-
return this._latestValue as T;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
set(value: T): void {
|
|
90
|
-
if (!this._writeFunction) {
|
|
91
|
-
throw new Error('Cannot write a value to a computed unless you specify a \'write\' option.');
|
|
92
|
-
}
|
|
93
|
-
this._writeFunction(value);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
getVersion(): number {
|
|
97
|
-
if (this._isSleeping && (this._isDirty || this.haveDependenciesChanged())) {
|
|
98
|
-
this.evaluateImmediate();
|
|
99
|
-
}
|
|
100
|
-
return super.getVersion();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
dispose(): void {
|
|
104
|
-
if (this._isDisposed) return;
|
|
105
|
-
|
|
106
|
-
if (!this._isSleeping) {
|
|
107
|
-
for (const id of Object.keys(this._dependencyTracking)) {
|
|
108
|
-
const dep = this._dependencyTracking[Number(id)];
|
|
109
|
-
if (dep?.dispose) {
|
|
110
|
-
dep.dispose();
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
this._dependencyTracking = {};
|
|
116
|
-
this._dependenciesCount = 0;
|
|
117
|
-
this._isDisposed = true;
|
|
118
|
-
this._isDirty = false;
|
|
119
|
-
this._isSleeping = false;
|
|
120
|
-
this._readFunction = undefined;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
getDependenciesCount(): number {
|
|
124
|
-
return this._dependenciesCount;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
getDependencies(): AnySubscribable[] {
|
|
128
|
-
const deps: AnySubscribable[] = [];
|
|
129
|
-
for (const id of Object.keys(this._dependencyTracking)) {
|
|
130
|
-
const tracking = this._dependencyTracking[Number(id)];
|
|
131
|
-
if (tracking) {
|
|
132
|
-
deps[tracking._order] = tracking._target;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return deps;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
isActive(): boolean {
|
|
139
|
-
return this._isDirty || this._dependenciesCount > 0;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** @internal */
|
|
143
|
-
private _isStale = false;
|
|
144
|
-
/** @internal */
|
|
145
|
-
_evalDelayed?: (isChange: boolean) => void;
|
|
146
|
-
|
|
147
|
-
override limit(limitFunction: (callback: () => void) => () => void): void {
|
|
148
|
-
super.limit(limitFunction);
|
|
149
|
-
|
|
150
|
-
this._evalIfChanged = () => {
|
|
151
|
-
if (!this._isSleeping) {
|
|
152
|
-
if (this._isStale) {
|
|
153
|
-
this.evaluateImmediate();
|
|
154
|
-
} else {
|
|
155
|
-
this._isDirty = false;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return this._latestValue as T;
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
this._evalDelayed = (isChange: boolean) => {
|
|
162
|
-
this._limitBeforeChange!(this._latestValue as T);
|
|
163
|
-
|
|
164
|
-
this._isDirty = true;
|
|
165
|
-
if (isChange) {
|
|
166
|
-
this._isStale = true;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
this._limitChange!(this as unknown as T, !isChange);
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
haveDependenciesChanged(): boolean {
|
|
174
|
-
if (this._isBeingEvaluated) {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
this._isBeingEvaluated = true;
|
|
178
|
-
try {
|
|
179
|
-
for (const id of Object.keys(this._dependencyTracking)) {
|
|
180
|
-
const dep = this._dependencyTracking[Number(id)];
|
|
181
|
-
if (dep && dep._target.hasChanged(dep._version)) {
|
|
182
|
-
return true;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return false;
|
|
186
|
-
} finally {
|
|
187
|
-
this._isBeingEvaluated = false;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
protected override beforeSubscriptionAdd(event: string): void {
|
|
192
|
-
if (this._deferEvaluation && (event === 'change' || event === 'beforeChange')) {
|
|
193
|
-
this._deferEvaluation = false;
|
|
194
|
-
this.peek();
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (!this._pure || this._isDisposed || !this._isSleeping || event !== 'change') {
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
this._wake();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
protected override afterSubscriptionRemove(event: string): void {
|
|
204
|
-
if (!this._pure || this._isDisposed || event !== 'change') {
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
if (!this.hasSubscriptionsForEvent('change')) {
|
|
208
|
-
this._sleep();
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
private _wake(): void {
|
|
213
|
-
this._isSleeping = false;
|
|
214
|
-
|
|
215
|
-
if (this._isDirty || this.haveDependenciesChanged()) {
|
|
216
|
-
this._dependencyTracking = {};
|
|
217
|
-
this._dependenciesCount = 0;
|
|
218
|
-
if (this.evaluateImmediate()) {
|
|
219
|
-
this.updateVersion();
|
|
220
|
-
}
|
|
221
|
-
} else {
|
|
222
|
-
const dependenciesOrder: number[] = [];
|
|
223
|
-
for (const id of Object.keys(this._dependencyTracking)) {
|
|
224
|
-
const dep = this._dependencyTracking[Number(id)];
|
|
225
|
-
if (dep) {
|
|
226
|
-
dependenciesOrder[dep._order] = Number(id);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
for (let order = 0; order < dependenciesOrder.length; order++) {
|
|
231
|
-
const id = dependenciesOrder[order];
|
|
232
|
-
if (id === undefined) continue;
|
|
233
|
-
const dep = this._dependencyTracking[id]!;
|
|
234
|
-
const subscription = this._subscribeToDependency(dep._target);
|
|
235
|
-
subscription._order = order;
|
|
236
|
-
subscription._version = dep._version;
|
|
237
|
-
this._dependencyTracking[id] = subscription;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (this.haveDependenciesChanged()) {
|
|
241
|
-
if (this.evaluateImmediate()) {
|
|
242
|
-
this.updateVersion();
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (!this._isDisposed) {
|
|
248
|
-
this.notifySubscribers(this._latestValue as T, 'awake');
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
private _sleep(): void {
|
|
253
|
-
for (const id of Object.keys(this._dependencyTracking)) {
|
|
254
|
-
const dep = this._dependencyTracking[Number(id)];
|
|
255
|
-
if (dep?.dispose) {
|
|
256
|
-
this._dependencyTracking[Number(id)] = {
|
|
257
|
-
_target: dep._target,
|
|
258
|
-
_order: dep._order,
|
|
259
|
-
_version: dep._version,
|
|
260
|
-
};
|
|
261
|
-
dep.dispose();
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
this._isSleeping = true;
|
|
265
|
-
this.notifySubscribers(undefined as T, 'asleep');
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
private evaluateImmediate(notifyChange = false): boolean {
|
|
269
|
-
if (this._isBeingEvaluated || this._isDisposed) {
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
this._isBeingEvaluated = true;
|
|
274
|
-
try {
|
|
275
|
-
return this._evaluateWithDependencyDetection(notifyChange);
|
|
276
|
-
} finally {
|
|
277
|
-
this._isBeingEvaluated = false;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
private _evaluateWithDependencyDetection(notifyChange: boolean): boolean {
|
|
282
|
-
const isInitial = this._pure ? undefined : this._dependenciesCount === 0;
|
|
283
|
-
|
|
284
|
-
const disposalCandidates = this._dependencyTracking;
|
|
285
|
-
let disposalCount = this._dependenciesCount;
|
|
286
|
-
|
|
287
|
-
this._dependencyTracking = {};
|
|
288
|
-
this._dependenciesCount = 0;
|
|
289
|
-
|
|
290
|
-
begin({
|
|
291
|
-
callback: (subscribable: AnySubscribable, id: number) => {
|
|
292
|
-
if (this._isDisposed) return;
|
|
293
|
-
|
|
294
|
-
if (this._pure && subscribable === this) {
|
|
295
|
-
throw new Error("A 'pure' computed must not be called recursively");
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (disposalCount && disposalCandidates[id]) {
|
|
299
|
-
this._addDependencyTracking(id, subscribable, disposalCandidates[id]!);
|
|
300
|
-
disposalCandidates[id] = null;
|
|
301
|
-
--disposalCount;
|
|
302
|
-
} else if (!this._dependencyTracking[id]) {
|
|
303
|
-
const trackingObj = this._isSleeping
|
|
304
|
-
? { _target: subscribable, _order: 0, _version: 0 }
|
|
305
|
-
: this._subscribeToDependency(subscribable);
|
|
306
|
-
this._addDependencyTracking(id, subscribable, trackingObj);
|
|
307
|
-
}
|
|
308
|
-
},
|
|
309
|
-
computed: this,
|
|
310
|
-
isInitial: isInitial === undefined ? undefined : !!isInitial,
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
let newValue: T;
|
|
314
|
-
try {
|
|
315
|
-
newValue = this._readFunction!();
|
|
316
|
-
} finally {
|
|
317
|
-
end();
|
|
318
|
-
|
|
319
|
-
if (disposalCount && !this._isSleeping) {
|
|
320
|
-
for (const id of Object.keys(disposalCandidates)) {
|
|
321
|
-
const entry = disposalCandidates[Number(id)];
|
|
322
|
-
if (entry?.dispose) {
|
|
323
|
-
entry.dispose();
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
this._isDirty = false;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
let changed: boolean;
|
|
332
|
-
if (!this._dependenciesCount) {
|
|
333
|
-
this.dispose();
|
|
334
|
-
changed = true;
|
|
335
|
-
} else {
|
|
336
|
-
changed = this.isDifferent(this._latestValue as T, newValue);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (changed) {
|
|
340
|
-
if (!this._isSleeping) {
|
|
341
|
-
if (notifyChange) {
|
|
342
|
-
this.notifySubscribers(this._latestValue as T, 'beforeChange');
|
|
343
|
-
}
|
|
344
|
-
} else {
|
|
345
|
-
this.updateVersion();
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
this._latestValue = newValue;
|
|
349
|
-
this.notifySubscribers(this._latestValue, 'spectate');
|
|
350
|
-
|
|
351
|
-
if (!this._isSleeping && notifyChange) {
|
|
352
|
-
this.notifySubscribers(this._latestValue);
|
|
353
|
-
}
|
|
354
|
-
if (this._recordUpdate) {
|
|
355
|
-
this._recordUpdate();
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (isInitial) {
|
|
360
|
-
this.notifySubscribers(this._latestValue as T, 'awake');
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return changed;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
private _addDependencyTracking(
|
|
367
|
-
id: number,
|
|
368
|
-
target: AnySubscribable,
|
|
369
|
-
trackingObj: DependencyTracking,
|
|
370
|
-
): void {
|
|
371
|
-
this._dependencyTracking[id] = trackingObj;
|
|
372
|
-
trackingObj._order = this._dependenciesCount++;
|
|
373
|
-
trackingObj._version = target.getVersion();
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
private _subscribeToDependency(target: AnySubscribable): DependencyTracking {
|
|
377
|
-
if (target._deferUpdates) {
|
|
378
|
-
const dirtySub = target.subscribe(() => {
|
|
379
|
-
if (this._evalDelayed && !this._isBeingEvaluated) {
|
|
380
|
-
this._evalDelayed(false);
|
|
381
|
-
}
|
|
382
|
-
}, 'dirty');
|
|
383
|
-
const changeSub = target.subscribe(() => {
|
|
384
|
-
if (!this._notificationIsPending) {
|
|
385
|
-
if (this._evalDelayed) {
|
|
386
|
-
this._evalDelayed(true);
|
|
387
|
-
} else {
|
|
388
|
-
this.evaluateImmediate(true);
|
|
389
|
-
}
|
|
390
|
-
} else if (this._isDirty) {
|
|
391
|
-
this._isStale = true;
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
return {
|
|
395
|
-
_target: target,
|
|
396
|
-
_order: 0,
|
|
397
|
-
_version: 0,
|
|
398
|
-
dispose() {
|
|
399
|
-
dirtySub.dispose();
|
|
400
|
-
changeSub.dispose();
|
|
401
|
-
},
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const subscription = target.subscribe(() => {
|
|
406
|
-
if (this._evalDelayed) {
|
|
407
|
-
this._evalDelayed(true);
|
|
408
|
-
} else {
|
|
409
|
-
this.evaluateImmediate(true);
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
return {
|
|
413
|
-
_target: target,
|
|
414
|
-
_order: 0,
|
|
415
|
-
_version: 0,
|
|
416
|
-
dispose() {
|
|
417
|
-
subscription.dispose();
|
|
418
|
-
},
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
export class PureComputed<T> extends Computed<T> {
|
|
424
|
-
constructor(evaluatorOrOptions: (() => T) | Omit<ComputedOptions<T>, 'pure'>) {
|
|
425
|
-
if (typeof evaluatorOrOptions === 'function') {
|
|
426
|
-
super({ read: evaluatorOrOptions, pure: true });
|
|
427
|
-
} else {
|
|
428
|
-
super({ ...evaluatorOrOptions, pure: true });
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
export function isComputed(value: unknown): value is Computed<unknown> {
|
|
434
|
-
return value instanceof Computed;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
export function isPureComputed(value: unknown): value is PureComputed<unknown> {
|
|
438
|
-
return value instanceof Computed && (value as Computed<unknown>).isPure;
|
|
439
|
-
}
|
|
1
|
+
import { Subscribable, valuesArePrimitiveAndEqual } from './subscribable.js';
|
|
2
|
+
import type { ReadableSubscribable } from './subscribable.js';
|
|
3
|
+
import { begin, end, registerDependency } from './dependencyDetection.js';
|
|
4
|
+
import { options } from './options.js';
|
|
5
|
+
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
type AnySubscribable = Subscribable<any>;
|
|
8
|
+
|
|
9
|
+
interface DependencyTracking {
|
|
10
|
+
_target: AnySubscribable;
|
|
11
|
+
_order: number;
|
|
12
|
+
_version: number;
|
|
13
|
+
dispose?(): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ComputedOptions<T> {
|
|
17
|
+
read: () => T;
|
|
18
|
+
write?: (value: T) => void;
|
|
19
|
+
pure?: boolean;
|
|
20
|
+
deferEvaluation?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class Computed<T> extends Subscribable<T> implements ReadableSubscribable<T> {
|
|
24
|
+
private _latestValue: T | undefined = undefined;
|
|
25
|
+
private _readFunction: (() => T) | undefined;
|
|
26
|
+
private _writeFunction: ((value: T) => void) | undefined;
|
|
27
|
+
private _isDisposed = false;
|
|
28
|
+
private _isBeingEvaluated = false;
|
|
29
|
+
private _isDirty = true;
|
|
30
|
+
private _pure: boolean;
|
|
31
|
+
private _isSleeping: boolean;
|
|
32
|
+
private _dependencyTracking: Record<number, DependencyTracking | null> = {};
|
|
33
|
+
private _dependenciesCount = 0;
|
|
34
|
+
private _deferEvaluation: boolean;
|
|
35
|
+
|
|
36
|
+
constructor(evaluatorOrOptions: (() => T) | ComputedOptions<T>) {
|
|
37
|
+
super();
|
|
38
|
+
if (typeof evaluatorOrOptions === 'function') {
|
|
39
|
+
this._readFunction = evaluatorOrOptions;
|
|
40
|
+
this._pure = false;
|
|
41
|
+
this._deferEvaluation = false;
|
|
42
|
+
} else {
|
|
43
|
+
this._readFunction = evaluatorOrOptions.read;
|
|
44
|
+
this._writeFunction = evaluatorOrOptions.write;
|
|
45
|
+
this._pure = evaluatorOrOptions.pure ?? false;
|
|
46
|
+
this._deferEvaluation = evaluatorOrOptions.deferEvaluation ?? false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this._isSleeping = this._pure;
|
|
50
|
+
this.equalityComparer = valuesArePrimitiveAndEqual;
|
|
51
|
+
|
|
52
|
+
if (!this._isSleeping && !this._deferEvaluation) {
|
|
53
|
+
this.evaluateImmediate();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (options.deferUpdates) {
|
|
57
|
+
this.extend({ deferred: true });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get hasWriteFunction(): boolean {
|
|
62
|
+
return typeof this._writeFunction === 'function';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get isPure(): boolean {
|
|
66
|
+
return this._pure;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get(): T {
|
|
70
|
+
if (!this._isDisposed) {
|
|
71
|
+
registerDependency(this);
|
|
72
|
+
}
|
|
73
|
+
if (this._isDirty || (this._isSleeping && this.haveDependenciesChanged())) {
|
|
74
|
+
this.evaluateImmediate();
|
|
75
|
+
}
|
|
76
|
+
return this._latestValue as T;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
peek(): T {
|
|
80
|
+
if (
|
|
81
|
+
(this._isDirty && !this._dependenciesCount) ||
|
|
82
|
+
(this._isSleeping && this.haveDependenciesChanged())
|
|
83
|
+
) {
|
|
84
|
+
this.evaluateImmediate();
|
|
85
|
+
}
|
|
86
|
+
return this._latestValue as T;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
set(value: T): void {
|
|
90
|
+
if (!this._writeFunction) {
|
|
91
|
+
throw new Error('Cannot write a value to a computed unless you specify a \'write\' option.');
|
|
92
|
+
}
|
|
93
|
+
this._writeFunction(value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getVersion(): number {
|
|
97
|
+
if (this._isSleeping && (this._isDirty || this.haveDependenciesChanged())) {
|
|
98
|
+
this.evaluateImmediate();
|
|
99
|
+
}
|
|
100
|
+
return super.getVersion();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
dispose(): void {
|
|
104
|
+
if (this._isDisposed) return;
|
|
105
|
+
|
|
106
|
+
if (!this._isSleeping) {
|
|
107
|
+
for (const id of Object.keys(this._dependencyTracking)) {
|
|
108
|
+
const dep = this._dependencyTracking[Number(id)];
|
|
109
|
+
if (dep?.dispose) {
|
|
110
|
+
dep.dispose();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this._dependencyTracking = {};
|
|
116
|
+
this._dependenciesCount = 0;
|
|
117
|
+
this._isDisposed = true;
|
|
118
|
+
this._isDirty = false;
|
|
119
|
+
this._isSleeping = false;
|
|
120
|
+
this._readFunction = undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getDependenciesCount(): number {
|
|
124
|
+
return this._dependenciesCount;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getDependencies(): AnySubscribable[] {
|
|
128
|
+
const deps: AnySubscribable[] = [];
|
|
129
|
+
for (const id of Object.keys(this._dependencyTracking)) {
|
|
130
|
+
const tracking = this._dependencyTracking[Number(id)];
|
|
131
|
+
if (tracking) {
|
|
132
|
+
deps[tracking._order] = tracking._target;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return deps;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
isActive(): boolean {
|
|
139
|
+
return this._isDirty || this._dependenciesCount > 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** @internal */
|
|
143
|
+
private _isStale = false;
|
|
144
|
+
/** @internal */
|
|
145
|
+
_evalDelayed?: (isChange: boolean) => void;
|
|
146
|
+
|
|
147
|
+
override limit(limitFunction: (callback: () => void) => () => void): void {
|
|
148
|
+
super.limit(limitFunction);
|
|
149
|
+
|
|
150
|
+
this._evalIfChanged = () => {
|
|
151
|
+
if (!this._isSleeping) {
|
|
152
|
+
if (this._isStale) {
|
|
153
|
+
this.evaluateImmediate();
|
|
154
|
+
} else {
|
|
155
|
+
this._isDirty = false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return this._latestValue as T;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
this._evalDelayed = (isChange: boolean) => {
|
|
162
|
+
this._limitBeforeChange!(this._latestValue as T);
|
|
163
|
+
|
|
164
|
+
this._isDirty = true;
|
|
165
|
+
if (isChange) {
|
|
166
|
+
this._isStale = true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this._limitChange!(this as unknown as T, !isChange);
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
haveDependenciesChanged(): boolean {
|
|
174
|
+
if (this._isBeingEvaluated) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
this._isBeingEvaluated = true;
|
|
178
|
+
try {
|
|
179
|
+
for (const id of Object.keys(this._dependencyTracking)) {
|
|
180
|
+
const dep = this._dependencyTracking[Number(id)];
|
|
181
|
+
if (dep && dep._target.hasChanged(dep._version)) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
} finally {
|
|
187
|
+
this._isBeingEvaluated = false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
protected override beforeSubscriptionAdd(event: string): void {
|
|
192
|
+
if (this._deferEvaluation && (event === 'change' || event === 'beforeChange')) {
|
|
193
|
+
this._deferEvaluation = false;
|
|
194
|
+
this.peek();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!this._pure || this._isDisposed || !this._isSleeping || event !== 'change') {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
this._wake();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
protected override afterSubscriptionRemove(event: string): void {
|
|
204
|
+
if (!this._pure || this._isDisposed || event !== 'change') {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!this.hasSubscriptionsForEvent('change')) {
|
|
208
|
+
this._sleep();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private _wake(): void {
|
|
213
|
+
this._isSleeping = false;
|
|
214
|
+
|
|
215
|
+
if (this._isDirty || this.haveDependenciesChanged()) {
|
|
216
|
+
this._dependencyTracking = {};
|
|
217
|
+
this._dependenciesCount = 0;
|
|
218
|
+
if (this.evaluateImmediate()) {
|
|
219
|
+
this.updateVersion();
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
const dependenciesOrder: number[] = [];
|
|
223
|
+
for (const id of Object.keys(this._dependencyTracking)) {
|
|
224
|
+
const dep = this._dependencyTracking[Number(id)];
|
|
225
|
+
if (dep) {
|
|
226
|
+
dependenciesOrder[dep._order] = Number(id);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (let order = 0; order < dependenciesOrder.length; order++) {
|
|
231
|
+
const id = dependenciesOrder[order];
|
|
232
|
+
if (id === undefined) continue;
|
|
233
|
+
const dep = this._dependencyTracking[id]!;
|
|
234
|
+
const subscription = this._subscribeToDependency(dep._target);
|
|
235
|
+
subscription._order = order;
|
|
236
|
+
subscription._version = dep._version;
|
|
237
|
+
this._dependencyTracking[id] = subscription;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (this.haveDependenciesChanged()) {
|
|
241
|
+
if (this.evaluateImmediate()) {
|
|
242
|
+
this.updateVersion();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!this._isDisposed) {
|
|
248
|
+
this.notifySubscribers(this._latestValue as T, 'awake');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private _sleep(): void {
|
|
253
|
+
for (const id of Object.keys(this._dependencyTracking)) {
|
|
254
|
+
const dep = this._dependencyTracking[Number(id)];
|
|
255
|
+
if (dep?.dispose) {
|
|
256
|
+
this._dependencyTracking[Number(id)] = {
|
|
257
|
+
_target: dep._target,
|
|
258
|
+
_order: dep._order,
|
|
259
|
+
_version: dep._version,
|
|
260
|
+
};
|
|
261
|
+
dep.dispose();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
this._isSleeping = true;
|
|
265
|
+
this.notifySubscribers(undefined as T, 'asleep');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private evaluateImmediate(notifyChange = false): boolean {
|
|
269
|
+
if (this._isBeingEvaluated || this._isDisposed) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
this._isBeingEvaluated = true;
|
|
274
|
+
try {
|
|
275
|
+
return this._evaluateWithDependencyDetection(notifyChange);
|
|
276
|
+
} finally {
|
|
277
|
+
this._isBeingEvaluated = false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private _evaluateWithDependencyDetection(notifyChange: boolean): boolean {
|
|
282
|
+
const isInitial = this._pure ? undefined : this._dependenciesCount === 0;
|
|
283
|
+
|
|
284
|
+
const disposalCandidates = this._dependencyTracking;
|
|
285
|
+
let disposalCount = this._dependenciesCount;
|
|
286
|
+
|
|
287
|
+
this._dependencyTracking = {};
|
|
288
|
+
this._dependenciesCount = 0;
|
|
289
|
+
|
|
290
|
+
begin({
|
|
291
|
+
callback: (subscribable: AnySubscribable, id: number) => {
|
|
292
|
+
if (this._isDisposed) return;
|
|
293
|
+
|
|
294
|
+
if (this._pure && subscribable === this) {
|
|
295
|
+
throw new Error("A 'pure' computed must not be called recursively");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (disposalCount && disposalCandidates[id]) {
|
|
299
|
+
this._addDependencyTracking(id, subscribable, disposalCandidates[id]!);
|
|
300
|
+
disposalCandidates[id] = null;
|
|
301
|
+
--disposalCount;
|
|
302
|
+
} else if (!this._dependencyTracking[id]) {
|
|
303
|
+
const trackingObj = this._isSleeping
|
|
304
|
+
? { _target: subscribable, _order: 0, _version: 0 }
|
|
305
|
+
: this._subscribeToDependency(subscribable);
|
|
306
|
+
this._addDependencyTracking(id, subscribable, trackingObj);
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
computed: this,
|
|
310
|
+
isInitial: isInitial === undefined ? undefined : !!isInitial,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
let newValue: T;
|
|
314
|
+
try {
|
|
315
|
+
newValue = this._readFunction!();
|
|
316
|
+
} finally {
|
|
317
|
+
end();
|
|
318
|
+
|
|
319
|
+
if (disposalCount && !this._isSleeping) {
|
|
320
|
+
for (const id of Object.keys(disposalCandidates)) {
|
|
321
|
+
const entry = disposalCandidates[Number(id)];
|
|
322
|
+
if (entry?.dispose) {
|
|
323
|
+
entry.dispose();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
this._isDirty = false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let changed: boolean;
|
|
332
|
+
if (!this._dependenciesCount) {
|
|
333
|
+
this.dispose();
|
|
334
|
+
changed = true;
|
|
335
|
+
} else {
|
|
336
|
+
changed = this.isDifferent(this._latestValue as T, newValue);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (changed) {
|
|
340
|
+
if (!this._isSleeping) {
|
|
341
|
+
if (notifyChange) {
|
|
342
|
+
this.notifySubscribers(this._latestValue as T, 'beforeChange');
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
this.updateVersion();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
this._latestValue = newValue;
|
|
349
|
+
this.notifySubscribers(this._latestValue, 'spectate');
|
|
350
|
+
|
|
351
|
+
if (!this._isSleeping && notifyChange) {
|
|
352
|
+
this.notifySubscribers(this._latestValue);
|
|
353
|
+
}
|
|
354
|
+
if (this._recordUpdate) {
|
|
355
|
+
this._recordUpdate();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (isInitial) {
|
|
360
|
+
this.notifySubscribers(this._latestValue as T, 'awake');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return changed;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private _addDependencyTracking(
|
|
367
|
+
id: number,
|
|
368
|
+
target: AnySubscribable,
|
|
369
|
+
trackingObj: DependencyTracking,
|
|
370
|
+
): void {
|
|
371
|
+
this._dependencyTracking[id] = trackingObj;
|
|
372
|
+
trackingObj._order = this._dependenciesCount++;
|
|
373
|
+
trackingObj._version = target.getVersion();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private _subscribeToDependency(target: AnySubscribable): DependencyTracking {
|
|
377
|
+
if (target._deferUpdates) {
|
|
378
|
+
const dirtySub = target.subscribe(() => {
|
|
379
|
+
if (this._evalDelayed && !this._isBeingEvaluated) {
|
|
380
|
+
this._evalDelayed(false);
|
|
381
|
+
}
|
|
382
|
+
}, 'dirty');
|
|
383
|
+
const changeSub = target.subscribe(() => {
|
|
384
|
+
if (!this._notificationIsPending) {
|
|
385
|
+
if (this._evalDelayed) {
|
|
386
|
+
this._evalDelayed(true);
|
|
387
|
+
} else {
|
|
388
|
+
this.evaluateImmediate(true);
|
|
389
|
+
}
|
|
390
|
+
} else if (this._isDirty) {
|
|
391
|
+
this._isStale = true;
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
return {
|
|
395
|
+
_target: target,
|
|
396
|
+
_order: 0,
|
|
397
|
+
_version: 0,
|
|
398
|
+
dispose() {
|
|
399
|
+
dirtySub.dispose();
|
|
400
|
+
changeSub.dispose();
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const subscription = target.subscribe(() => {
|
|
406
|
+
if (this._evalDelayed) {
|
|
407
|
+
this._evalDelayed(true);
|
|
408
|
+
} else {
|
|
409
|
+
this.evaluateImmediate(true);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
return {
|
|
413
|
+
_target: target,
|
|
414
|
+
_order: 0,
|
|
415
|
+
_version: 0,
|
|
416
|
+
dispose() {
|
|
417
|
+
subscription.dispose();
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export class PureComputed<T> extends Computed<T> {
|
|
424
|
+
constructor(evaluatorOrOptions: (() => T) | Omit<ComputedOptions<T>, 'pure'>) {
|
|
425
|
+
if (typeof evaluatorOrOptions === 'function') {
|
|
426
|
+
super({ read: evaluatorOrOptions, pure: true });
|
|
427
|
+
} else {
|
|
428
|
+
super({ ...evaluatorOrOptions, pure: true });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function isComputed(value: unknown): value is Computed<unknown> {
|
|
434
|
+
return value instanceof Computed;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function isPureComputed(value: unknown): value is PureComputed<unknown> {
|
|
438
|
+
return value instanceof Computed && (value as Computed<unknown>).isPure;
|
|
439
|
+
}
|