@actuallab/fusion 13.0.25 → 13.0.163
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/dist/index.cjs +379 -188
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -28
- package/dist/index.d.ts +74 -28
- package/dist/index.js +370 -178
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ function getInstanceId(instance) {
|
|
|
28
28
|
import {
|
|
29
29
|
AsyncContext as AsyncContext2,
|
|
30
30
|
EventHandlerSet,
|
|
31
|
+
isCancellation,
|
|
31
32
|
PromiseSource,
|
|
32
33
|
Result,
|
|
33
34
|
result
|
|
@@ -54,6 +55,11 @@ var ComputedRegistry = class _ComputedRegistry {
|
|
|
54
55
|
}
|
|
55
56
|
static register(computed) {
|
|
56
57
|
const key = computed.input;
|
|
58
|
+
const displaced = _ComputedRegistry._entries.get(key)?.deref();
|
|
59
|
+
if (displaced === computed)
|
|
60
|
+
return;
|
|
61
|
+
if (displaced !== void 0 && displaced.state !== 2 /* Invalidated */)
|
|
62
|
+
displaced.invalidate();
|
|
57
63
|
_ComputedRegistry._entries.set(key, new WeakRef(computed));
|
|
58
64
|
_ComputedRegistry._finalization.register(computed, key, computed);
|
|
59
65
|
}
|
|
@@ -69,19 +75,36 @@ import { AsyncContext, AsyncContextKey } from "@actuallab/core";
|
|
|
69
75
|
var computeContextKey = new AsyncContextKey("ComputeContext", void 0);
|
|
70
76
|
var ComputeContext = class {
|
|
71
77
|
computed;
|
|
78
|
+
parent;
|
|
72
79
|
_isCapturing = true;
|
|
73
|
-
|
|
80
|
+
_captured;
|
|
81
|
+
constructor(computed, parent) {
|
|
74
82
|
this.computed = computed;
|
|
83
|
+
this.parent = parent;
|
|
75
84
|
}
|
|
76
85
|
get isCapturing() {
|
|
77
86
|
return this._isCapturing;
|
|
78
87
|
}
|
|
88
|
+
get captured() {
|
|
89
|
+
return this._captured;
|
|
90
|
+
}
|
|
79
91
|
stopCapturing() {
|
|
80
92
|
this._isCapturing = false;
|
|
81
93
|
}
|
|
82
94
|
captureDependency(dependency) {
|
|
83
95
|
if (!this._isCapturing) return;
|
|
84
|
-
this.
|
|
96
|
+
this._captured = dependency;
|
|
97
|
+
this.computed?.addDependency(dependency);
|
|
98
|
+
}
|
|
99
|
+
// Walks the caller chain for a computation still in progress under the same key —
|
|
100
|
+
// C# AsyncLockSet(LockReentryMode.CheckedFail) analog, used to fail fast on reentry.
|
|
101
|
+
// Only Computing ancestors count: a context leaked past its finished computation
|
|
102
|
+
// (e.g. via AsyncLocalStorage into a later callback) must not fail later calls.
|
|
103
|
+
hasKeyInChain(key) {
|
|
104
|
+
const computed = this.computed;
|
|
105
|
+
if (computed?.isComputing === true && computed.input === key)
|
|
106
|
+
return true;
|
|
107
|
+
return this.parent?.hasKeyInChain(key) ?? false;
|
|
85
108
|
}
|
|
86
109
|
/** Resolve ComputeContext from an AsyncContext (or current). */
|
|
87
110
|
static from(ctx) {
|
|
@@ -89,7 +112,20 @@ var ComputeContext = class {
|
|
|
89
112
|
}
|
|
90
113
|
};
|
|
91
114
|
|
|
115
|
+
// src/computed-options.ts
|
|
116
|
+
var ComputedOptions = class _ComputedOptions {
|
|
117
|
+
static default = new _ComputedOptions();
|
|
118
|
+
static mutableStateDefault = new _ComputedOptions({
|
|
119
|
+
errorAutoInvalidateDelay: Infinity
|
|
120
|
+
});
|
|
121
|
+
errorAutoInvalidateDelay;
|
|
122
|
+
constructor(overrides) {
|
|
123
|
+
this.errorAutoInvalidateDelay = overrides?.errorAutoInvalidateDelay ?? 1e3;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
92
127
|
// src/computed.ts
|
|
128
|
+
var { errorLog } = getLogs("Computed");
|
|
93
129
|
var _nextVersion = 0;
|
|
94
130
|
var ConsistencyState = /* @__PURE__ */ ((ConsistencyState2) => {
|
|
95
131
|
ConsistencyState2[ConsistencyState2["Computing"] = 0] = "Computing";
|
|
@@ -97,36 +133,44 @@ var ConsistencyState = /* @__PURE__ */ ((ConsistencyState2) => {
|
|
|
97
133
|
ConsistencyState2[ConsistencyState2["Invalidated"] = 2] = "Invalidated";
|
|
98
134
|
return ConsistencyState2;
|
|
99
135
|
})(ConsistencyState || {});
|
|
100
|
-
var Computed = class
|
|
101
|
-
/** Delay before auto-invalidating a Computed that holds an error result. 0 = disabled. */
|
|
102
|
-
static errorAutoInvalidateDelay = 1e3;
|
|
136
|
+
var Computed = class {
|
|
103
137
|
/** Capture the Computed backing a computation — works with both local and RPC compute methods. */
|
|
104
138
|
static async capture(fn) {
|
|
105
|
-
const
|
|
106
|
-
const captureCtx = new ComputeContext(
|
|
107
|
-
|
|
108
|
-
computeContextKey
|
|
109
|
-
captureCtx
|
|
139
|
+
const currentCtx = AsyncContext2.getOrCreate();
|
|
140
|
+
const captureCtx = new ComputeContext(
|
|
141
|
+
void 0,
|
|
142
|
+
currentCtx.get(computeContextKey)
|
|
110
143
|
);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
144
|
+
const asyncCtx = currentCtx.with(computeContextKey, captureCtx);
|
|
145
|
+
try {
|
|
146
|
+
await asyncCtx.run(fn);
|
|
147
|
+
} catch (e) {
|
|
148
|
+
if (isCancellation(e)) throw e;
|
|
149
|
+
const captured2 = captureCtx.captured;
|
|
150
|
+
if (captured2?.hasError) return captured2;
|
|
151
|
+
throw e;
|
|
152
|
+
}
|
|
153
|
+
const captured = captureCtx.captured;
|
|
154
|
+
if (captured === void 0)
|
|
114
155
|
throw new Error(
|
|
115
156
|
"No Computed was captured \u2014 fn must call a compute function."
|
|
116
157
|
);
|
|
117
|
-
return
|
|
158
|
+
return captured;
|
|
118
159
|
}
|
|
119
160
|
input;
|
|
161
|
+
options;
|
|
120
162
|
_version;
|
|
121
163
|
_state;
|
|
122
164
|
_output;
|
|
123
165
|
_invalidatePending = false;
|
|
166
|
+
_errorTimer;
|
|
124
167
|
_dependencies = /* @__PURE__ */ new Set();
|
|
125
168
|
_dependants = /* @__PURE__ */ new Map();
|
|
126
|
-
|
|
169
|
+
_onInvalidated = new EventHandlerSet();
|
|
127
170
|
_renewer;
|
|
128
|
-
constructor(input, renewer) {
|
|
171
|
+
constructor(input, renewer, options = ComputedOptions.default) {
|
|
129
172
|
this.input = input;
|
|
173
|
+
this.options = options;
|
|
130
174
|
this._renewer = renewer;
|
|
131
175
|
this._version = ++_nextVersion;
|
|
132
176
|
this._state = 0 /* Computing */;
|
|
@@ -160,6 +204,9 @@ var Computed = class _Computed {
|
|
|
160
204
|
get isConsistent() {
|
|
161
205
|
return this._state === 1 /* Consistent */;
|
|
162
206
|
}
|
|
207
|
+
get isComputing() {
|
|
208
|
+
return this._state === 0 /* Computing */;
|
|
209
|
+
}
|
|
163
210
|
get dependencies() {
|
|
164
211
|
return this._dependencies;
|
|
165
212
|
}
|
|
@@ -167,7 +214,14 @@ var Computed = class _Computed {
|
|
|
167
214
|
if (this._state === 1 /* Consistent */) return this;
|
|
168
215
|
const latest = this._latest();
|
|
169
216
|
if (latest?.isConsistent) return latest;
|
|
170
|
-
if (this._renewer !== void 0)
|
|
217
|
+
if (this._renewer !== void 0) {
|
|
218
|
+
const renewer = this._renewer;
|
|
219
|
+
const isolated = (AsyncContext2.current ?? AsyncContext2.empty).with(
|
|
220
|
+
computeContextKey,
|
|
221
|
+
void 0
|
|
222
|
+
);
|
|
223
|
+
return isolated.run(renewer);
|
|
224
|
+
}
|
|
171
225
|
throw new Error(
|
|
172
226
|
"Cannot recompute: Computed is invalidated and has no renewer."
|
|
173
227
|
);
|
|
@@ -204,11 +258,11 @@ var Computed = class _Computed {
|
|
|
204
258
|
this.invalidate();
|
|
205
259
|
return;
|
|
206
260
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
261
|
+
const delay = this.options.errorAutoInvalidateDelay;
|
|
262
|
+
if (this._output.hasError && delay !== Infinity && delay > 0) {
|
|
263
|
+
this._errorTimer = setTimeout(() => this.invalidate(), delay);
|
|
264
|
+
this._errorTimer.unref?.();
|
|
265
|
+
}
|
|
212
266
|
}
|
|
213
267
|
_register() {
|
|
214
268
|
ComputedRegistry.register(this);
|
|
@@ -216,6 +270,8 @@ var Computed = class _Computed {
|
|
|
216
270
|
_unregister() {
|
|
217
271
|
ComputedRegistry.unregister(this);
|
|
218
272
|
}
|
|
273
|
+
// Never throws — mirrors .NET Computed.Invalidate: own handlers fire first
|
|
274
|
+
// (each isolated), then dependants propagate unconditionally, then unregister.
|
|
219
275
|
invalidate() {
|
|
220
276
|
if (this._state === 2 /* Invalidated */)
|
|
221
277
|
return;
|
|
@@ -224,35 +280,74 @@ var Computed = class _Computed {
|
|
|
224
280
|
return;
|
|
225
281
|
}
|
|
226
282
|
this._state = 2 /* Invalidated */;
|
|
227
|
-
this.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
283
|
+
if (this._errorTimer !== void 0) {
|
|
284
|
+
clearTimeout(this._errorTimer);
|
|
285
|
+
this._errorTimer = void 0;
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
this._onInvalidated.triggerSafe(
|
|
289
|
+
void 0,
|
|
290
|
+
(e) => errorLog?.log("onInvalidated handler failed", e)
|
|
291
|
+
);
|
|
292
|
+
this._onInvalidated.clear();
|
|
293
|
+
} finally {
|
|
294
|
+
for (const dependency of this._dependencies)
|
|
295
|
+
dependency._dependants.delete(this._version);
|
|
296
|
+
this._dependencies.clear();
|
|
297
|
+
for (const [, ref] of this._dependants) {
|
|
298
|
+
const dependant = ref.deref();
|
|
299
|
+
if (dependant != null) {
|
|
300
|
+
try {
|
|
301
|
+
dependant.invalidate();
|
|
302
|
+
} catch (e) {
|
|
303
|
+
errorLog?.log("Error while invalidating dependant", e);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
this._dependants.clear();
|
|
308
|
+
this._unregister();
|
|
231
309
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
this.
|
|
235
|
-
|
|
310
|
+
}
|
|
311
|
+
onInvalidated(handler) {
|
|
312
|
+
if (this._state === 2 /* Invalidated */) {
|
|
313
|
+
handler();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
this._onInvalidated.add(handler);
|
|
236
317
|
}
|
|
237
318
|
whenInvalidated(abortSignal) {
|
|
238
319
|
if (this._state === 2 /* Invalidated */)
|
|
239
320
|
return Promise.resolve();
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (abortSignal !== void 0 && !abortSignal.aborted) {
|
|
244
|
-
abortSignal.addEventListener(
|
|
245
|
-
"abort",
|
|
246
|
-
() => {
|
|
247
|
-
this.onInvalidated.remove(handler);
|
|
248
|
-
ps.reject(abortSignal.reason);
|
|
249
|
-
},
|
|
250
|
-
{ once: true }
|
|
321
|
+
if (abortSignal?.aborted)
|
|
322
|
+
return Promise.reject(
|
|
323
|
+
abortSignal.reason ?? new Error("Operation cancelled.")
|
|
251
324
|
);
|
|
325
|
+
const ps = new PromiseSource();
|
|
326
|
+
let onAbort;
|
|
327
|
+
const onInvalidated = () => {
|
|
328
|
+
if (onAbort !== void 0)
|
|
329
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
330
|
+
ps.resolve(void 0);
|
|
331
|
+
};
|
|
332
|
+
this._onInvalidated.add(onInvalidated);
|
|
333
|
+
if (abortSignal !== void 0) {
|
|
334
|
+
onAbort = () => {
|
|
335
|
+
this._onInvalidated.remove(onInvalidated);
|
|
336
|
+
ps.reject(
|
|
337
|
+
abortSignal.reason ?? new Error("Operation cancelled.")
|
|
338
|
+
);
|
|
339
|
+
};
|
|
340
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
252
341
|
}
|
|
253
342
|
return ps;
|
|
254
343
|
}
|
|
255
344
|
addDependency(dependency) {
|
|
345
|
+
if (this._state !== 0 /* Computing */)
|
|
346
|
+
return;
|
|
347
|
+
if (dependency._state === 2 /* Invalidated */) {
|
|
348
|
+
this.invalidate();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
256
351
|
this._dependencies.add(dependency);
|
|
257
352
|
dependency._dependants.set(
|
|
258
353
|
this._version,
|
|
@@ -261,6 +356,9 @@ var Computed = class _Computed {
|
|
|
261
356
|
}
|
|
262
357
|
};
|
|
263
358
|
var StateBoundComputed = class extends Computed {
|
|
359
|
+
constructor(input, renewer, options = ComputedOptions.mutableStateDefault) {
|
|
360
|
+
super(input, renewer, options);
|
|
361
|
+
}
|
|
264
362
|
_latest() {
|
|
265
363
|
return this.input.computed;
|
|
266
364
|
}
|
|
@@ -274,24 +372,27 @@ var StateBoundComputed = class extends Computed {
|
|
|
274
372
|
import {
|
|
275
373
|
AsyncContext as AsyncContext3,
|
|
276
374
|
AsyncLock,
|
|
375
|
+
isCancellation as isCancellation2,
|
|
277
376
|
result as result2,
|
|
278
377
|
errorResult
|
|
279
378
|
} from "@actuallab/core";
|
|
280
379
|
var RS = "";
|
|
281
380
|
function defaultArgToString(arg) {
|
|
282
|
-
return JSON.stringify(arg) ?? "
|
|
381
|
+
return JSON.stringify(arg) ?? "";
|
|
283
382
|
}
|
|
284
383
|
var _nextFunctionId = 0;
|
|
285
384
|
var ComputeFunction = class {
|
|
286
385
|
methodName;
|
|
287
386
|
id;
|
|
387
|
+
options;
|
|
288
388
|
argToString = defaultArgToString;
|
|
289
389
|
_impl;
|
|
290
390
|
_locks = /* @__PURE__ */ new Map();
|
|
291
|
-
constructor(methodName, impl) {
|
|
391
|
+
constructor(methodName, impl, options = ComputedOptions.default) {
|
|
292
392
|
this.methodName = methodName;
|
|
293
393
|
this.id = `${methodName}[${++_nextFunctionId}]`;
|
|
294
394
|
this._impl = impl;
|
|
395
|
+
this.options = options;
|
|
295
396
|
}
|
|
296
397
|
/** Build a string key for the given instance and arguments. */
|
|
297
398
|
buildKey(instance, args) {
|
|
@@ -301,7 +402,7 @@ var ComputeFunction = class {
|
|
|
301
402
|
}
|
|
302
403
|
return key;
|
|
303
404
|
}
|
|
304
|
-
async invoke(instance, args
|
|
405
|
+
async invoke(instance, args) {
|
|
305
406
|
const asyncCtx = AsyncContext3.fromArgs(args);
|
|
306
407
|
const callerComputeCtx = asyncCtx?.get(computeContextKey);
|
|
307
408
|
const argsWithoutCtx = asyncCtx?.stripFromArgs(args) ?? args;
|
|
@@ -311,6 +412,8 @@ var ComputeFunction = class {
|
|
|
311
412
|
callerComputeCtx?.captureDependency(existing);
|
|
312
413
|
return existing;
|
|
313
414
|
}
|
|
415
|
+
if (callerComputeCtx?.hasKeyInChain(key))
|
|
416
|
+
throw new Error(`Reentrant compute call detected for key "${key}".`);
|
|
314
417
|
let lock = this._locks.get(key);
|
|
315
418
|
if (lock === void 0) {
|
|
316
419
|
lock = new AsyncLock();
|
|
@@ -319,10 +422,17 @@ var ComputeFunction = class {
|
|
|
319
422
|
const computed = await lock.run(async () => {
|
|
320
423
|
const cached = ComputedRegistry.get(key);
|
|
321
424
|
if (cached?.isConsistent) return cached;
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
425
|
+
const renewer = () => this.invoke(instance, argsWithoutCtx);
|
|
426
|
+
const newComputed = new Computed(
|
|
427
|
+
key,
|
|
428
|
+
renewer,
|
|
429
|
+
this.options
|
|
430
|
+
);
|
|
431
|
+
ComputedRegistry.register(newComputed);
|
|
432
|
+
const childComputeCtx = new ComputeContext(
|
|
433
|
+
newComputed,
|
|
434
|
+
callerComputeCtx
|
|
435
|
+
);
|
|
326
436
|
const childAsyncCtx = (asyncCtx ?? AsyncContext3.empty).with(
|
|
327
437
|
computeContextKey,
|
|
328
438
|
childComputeCtx
|
|
@@ -330,7 +440,7 @@ var ComputeFunction = class {
|
|
|
330
440
|
let fnResult;
|
|
331
441
|
try {
|
|
332
442
|
const value = childAsyncCtx.run(
|
|
333
|
-
() => this._impl.call(instance, ...argsWithoutCtx)
|
|
443
|
+
() => this._impl.call(instance, ...argsWithoutCtx, childAsyncCtx)
|
|
334
444
|
);
|
|
335
445
|
const resolved = value instanceof Promise ? await value : value;
|
|
336
446
|
fnResult = result2(resolved);
|
|
@@ -338,27 +448,38 @@ var ComputeFunction = class {
|
|
|
338
448
|
fnResult = errorResult(e);
|
|
339
449
|
}
|
|
340
450
|
newComputed.setOutput(fnResult);
|
|
451
|
+
if (fnResult.hasError && isCancellation2(fnResult.error))
|
|
452
|
+
newComputed.invalidate();
|
|
341
453
|
return newComputed;
|
|
342
454
|
});
|
|
455
|
+
if (!lock.isLocked && this._locks.get(key) === lock)
|
|
456
|
+
this._locks.delete(key);
|
|
343
457
|
callerComputeCtx?.captureDependency(computed);
|
|
344
458
|
return computed;
|
|
345
459
|
}
|
|
346
460
|
};
|
|
347
461
|
|
|
348
462
|
// src/compute-method.ts
|
|
463
|
+
import { ownMetadata, resolveArgCount } from "@actuallab/core";
|
|
349
464
|
var METHODS_META = /* @__PURE__ */ Symbol.for("actuallab.methods");
|
|
350
465
|
function getMethodsMeta(cls) {
|
|
351
466
|
return cls[Symbol.metadata]?.[METHODS_META];
|
|
352
467
|
}
|
|
353
|
-
function computeMethod(
|
|
468
|
+
function computeMethod(targetOrOptions, context) {
|
|
469
|
+
if (typeof targetOrOptions === "function")
|
|
470
|
+
return computeMethodImpl(targetOrOptions, context, {});
|
|
471
|
+
const options = targetOrOptions ?? {};
|
|
472
|
+
return (target, ctx) => computeMethodImpl(target, ctx, options);
|
|
473
|
+
}
|
|
474
|
+
function computeMethodImpl(target, context, options) {
|
|
354
475
|
const methodName = String(context.name);
|
|
355
|
-
const methods = context.metadata
|
|
476
|
+
const methods = ownMetadata(context.metadata, METHODS_META);
|
|
356
477
|
methods[methodName] = {
|
|
357
478
|
...methods[methodName],
|
|
358
479
|
compute: true,
|
|
359
|
-
argCount: target.
|
|
480
|
+
argCount: resolveArgCount(target, options.argCount, methodName)
|
|
360
481
|
};
|
|
361
|
-
const cf = new ComputeFunction(methodName, target);
|
|
482
|
+
const cf = new ComputeFunction(methodName, target, new ComputedOptions(options));
|
|
362
483
|
const replacement = function(...allArgs) {
|
|
363
484
|
return cf.invoke(this, allArgs).then((c) => c.value);
|
|
364
485
|
};
|
|
@@ -390,11 +511,20 @@ function wrapComputeMethod(fn) {
|
|
|
390
511
|
}
|
|
391
512
|
|
|
392
513
|
// src/update-delayer.ts
|
|
514
|
+
import { awaitWithCleanup, RetryDelaySeq } from "@actuallab/core";
|
|
515
|
+
var defaultRetryDelays = RetryDelaySeq.exp(1e3, 6e4);
|
|
516
|
+
var defaultMinDelayMs = 32;
|
|
517
|
+
function computeUpdateDelay(retryCount, updateDelayMs, retryDelays, minDelayMs) {
|
|
518
|
+
const min = retryCount === 0 ? minDelayMs : retryDelays.min;
|
|
519
|
+
const raw = retryCount > 0 ? retryDelays.getDelay(retryCount) : updateDelayMs;
|
|
520
|
+
return Math.max(min, raw);
|
|
521
|
+
}
|
|
393
522
|
var FixedDelayer = class _FixedDelayer {
|
|
394
523
|
static zero = () => Promise.resolve();
|
|
395
524
|
static _cache = /* @__PURE__ */ new Map();
|
|
525
|
+
// Floors ms at minDelay like C# FixedDelayer.Get — a zero delayer requires the explicit .zero.
|
|
396
526
|
static get(ms) {
|
|
397
|
-
|
|
527
|
+
ms = Math.max(ms, defaultMinDelayMs);
|
|
398
528
|
let delayer = _FixedDelayer._cache.get(ms);
|
|
399
529
|
if (delayer === void 0) {
|
|
400
530
|
delayer = new _FixedDelayer(ms).delay;
|
|
@@ -403,42 +533,57 @@ var FixedDelayer = class _FixedDelayer {
|
|
|
403
533
|
return delayer;
|
|
404
534
|
}
|
|
405
535
|
ms;
|
|
536
|
+
retryDelays;
|
|
537
|
+
minDelay;
|
|
406
538
|
delay;
|
|
407
|
-
constructor(ms) {
|
|
539
|
+
constructor(ms, retryDelays = defaultRetryDelays, minDelay = defaultMinDelayMs) {
|
|
408
540
|
this.ms = ms;
|
|
409
|
-
this.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
abortSignal.addEventListener(
|
|
418
|
-
"abort",
|
|
419
|
-
() => {
|
|
420
|
-
clearTimeout(timer);
|
|
421
|
-
reject(abortSignal.reason instanceof Error ? abortSignal.reason : new Error(String(abortSignal.reason)));
|
|
422
|
-
},
|
|
423
|
-
{ once: true }
|
|
541
|
+
this.retryDelays = retryDelays;
|
|
542
|
+
this.minDelay = minDelay;
|
|
543
|
+
this.delay = (retryCount, abortSignal) => {
|
|
544
|
+
const delayMs = computeUpdateDelay(
|
|
545
|
+
retryCount,
|
|
546
|
+
this.ms,
|
|
547
|
+
this.retryDelays,
|
|
548
|
+
this.minDelay
|
|
424
549
|
);
|
|
425
|
-
|
|
426
|
-
(resolve
|
|
427
|
-
|
|
550
|
+
if (delayMs <= 0) return Promise.resolve();
|
|
551
|
+
return awaitWithCleanup(abortSignal, "resolve", (complete, addCleanup) => {
|
|
552
|
+
const timer = setTimeout(complete, delayMs);
|
|
553
|
+
addCleanup(() => clearTimeout(timer));
|
|
554
|
+
});
|
|
555
|
+
};
|
|
428
556
|
}
|
|
429
557
|
};
|
|
430
558
|
var defaultUpdateDelayer = FixedDelayer.get(1e3 / 60);
|
|
431
559
|
|
|
560
|
+
// src/ui-update-delayer.ts
|
|
561
|
+
import { awaitWithCleanup as awaitWithCleanup2 } from "@actuallab/core";
|
|
562
|
+
|
|
432
563
|
// src/ui-action-tracker.ts
|
|
433
564
|
import { EventHandlerSet as EventHandlerSet2 } from "@actuallab/core";
|
|
434
|
-
var { errorLog } = getLogs("UIActionTracker");
|
|
565
|
+
var { errorLog: errorLog2 } = getLogs("UIActionTracker");
|
|
435
566
|
var UIActionTracker = class {
|
|
436
567
|
_activeCount = 0;
|
|
568
|
+
_lastResultAt = 0;
|
|
569
|
+
_errorTimes = [];
|
|
437
570
|
changed = new EventHandlerSet2();
|
|
438
571
|
errors = [];
|
|
572
|
+
// C# UIActionTracker.Options.InstantUpdatePeriod / UIActionFailureTracker MaxDuplicateRecency.
|
|
573
|
+
instantUpdatePeriod = 300;
|
|
574
|
+
maxDuplicateRecency = 1e3;
|
|
575
|
+
maxErrors = 100;
|
|
439
576
|
get isActive() {
|
|
440
577
|
return this._activeCount > 0;
|
|
441
578
|
}
|
|
579
|
+
// C# UIActionTracker.AreInstantUpdatesEnabled — active, or within instantUpdatePeriod of the last result.
|
|
580
|
+
areInstantUpdatesEnabled() {
|
|
581
|
+
if (this._activeCount > 0)
|
|
582
|
+
return true;
|
|
583
|
+
if (this._lastResultAt === 0)
|
|
584
|
+
return false;
|
|
585
|
+
return this._lastResultAt + this.instantUpdatePeriod >= Date.now();
|
|
586
|
+
}
|
|
442
587
|
/** Run a command silently — errors are added to the error list but not thrown. */
|
|
443
588
|
async run(fn) {
|
|
444
589
|
this._activeCount++;
|
|
@@ -446,12 +591,10 @@ var UIActionTracker = class {
|
|
|
446
591
|
try {
|
|
447
592
|
await fn();
|
|
448
593
|
} catch (e) {
|
|
449
|
-
|
|
450
|
-
this.
|
|
594
|
+
errorLog2?.log("UI action failed", e);
|
|
595
|
+
this._addError(e);
|
|
451
596
|
} finally {
|
|
452
|
-
this.
|
|
453
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
454
|
-
this.changed.trigger();
|
|
597
|
+
this._onCompleted();
|
|
455
598
|
}
|
|
456
599
|
}
|
|
457
600
|
/** Call a command and return the result — errors are added to the error list AND thrown. */
|
|
@@ -461,65 +604,106 @@ var UIActionTracker = class {
|
|
|
461
604
|
try {
|
|
462
605
|
return await fn();
|
|
463
606
|
} catch (e) {
|
|
464
|
-
|
|
465
|
-
this.
|
|
607
|
+
errorLog2?.log("UI action failed", e);
|
|
608
|
+
this._addError(e);
|
|
466
609
|
throw e;
|
|
467
610
|
} finally {
|
|
468
|
-
this.
|
|
469
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
470
|
-
this.changed.trigger();
|
|
611
|
+
this._onCompleted();
|
|
471
612
|
}
|
|
472
613
|
}
|
|
473
614
|
dismissError(index) {
|
|
474
615
|
if (index >= 0 && index < this.errors.length) {
|
|
475
616
|
this.errors.splice(index, 1);
|
|
617
|
+
this._errorTimes.splice(index, 1);
|
|
476
618
|
this.changed.trigger();
|
|
477
619
|
}
|
|
478
620
|
}
|
|
621
|
+
// Private methods
|
|
622
|
+
_onCompleted() {
|
|
623
|
+
this._activeCount--;
|
|
624
|
+
this._lastResultAt = Date.now();
|
|
625
|
+
this.changed.trigger();
|
|
626
|
+
}
|
|
627
|
+
// C# UIActionFailureTracker.TryAddFailure — drop a same-name/same-message error seen within
|
|
628
|
+
// maxDuplicateRecency; a size cap is the backstop against unbounded growth.
|
|
629
|
+
_addError(e) {
|
|
630
|
+
const now = Date.now();
|
|
631
|
+
const { name, message } = errorInfo(e);
|
|
632
|
+
const minAt = now - this.maxDuplicateRecency;
|
|
633
|
+
for (let i = 0; i < this.errors.length; i++) {
|
|
634
|
+
if (this._errorTimes[i] < minAt)
|
|
635
|
+
continue;
|
|
636
|
+
const prev = errorInfo(this.errors[i]);
|
|
637
|
+
if (prev.name === name && prev.message === message)
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
this.errors.push(e);
|
|
641
|
+
this._errorTimes.push(now);
|
|
642
|
+
while (this.errors.length > this.maxErrors) {
|
|
643
|
+
this.errors.shift();
|
|
644
|
+
this._errorTimes.shift();
|
|
645
|
+
}
|
|
646
|
+
this.changed.trigger();
|
|
647
|
+
}
|
|
479
648
|
};
|
|
649
|
+
function errorInfo(e) {
|
|
650
|
+
if (e instanceof Error)
|
|
651
|
+
return { name: e.name, message: e.message };
|
|
652
|
+
return { name: "", message: String(e) };
|
|
653
|
+
}
|
|
480
654
|
var uiActions = new UIActionTracker();
|
|
481
655
|
|
|
482
656
|
// src/ui-update-delayer.ts
|
|
483
657
|
var UIUpdateDelayer = class _UIUpdateDelayer {
|
|
484
658
|
static _cache = /* @__PURE__ */ new Map();
|
|
659
|
+
// Floors ms at minDelay like C# FixedDelayer.Get — a zero delayer requires FixedDelayer.zero.
|
|
485
660
|
static get(ms) {
|
|
486
|
-
|
|
661
|
+
ms = Math.max(ms, defaultMinDelayMs);
|
|
487
662
|
let delayer = _UIUpdateDelayer._cache.get(ms);
|
|
488
663
|
if (delayer !== void 0) return delayer;
|
|
664
|
+
delayer = new _UIUpdateDelayer(ms).delay;
|
|
665
|
+
_UIUpdateDelayer._cache.set(ms, delayer);
|
|
666
|
+
return delayer;
|
|
667
|
+
}
|
|
668
|
+
ms;
|
|
669
|
+
retryDelays;
|
|
670
|
+
minDelay;
|
|
671
|
+
delay;
|
|
672
|
+
constructor(ms, retryDelays = defaultRetryDelays, minDelay = defaultMinDelayMs) {
|
|
673
|
+
this.ms = ms;
|
|
674
|
+
this.retryDelays = retryDelays;
|
|
675
|
+
this.minDelay = minDelay;
|
|
676
|
+
this.delay = this._delay.bind(this);
|
|
677
|
+
}
|
|
678
|
+
// Private methods
|
|
679
|
+
// Mirrors C# UpdateDelayer.Delay: race the instant-updates window against the full delay,
|
|
680
|
+
// then enforce minDelay measured from delay start so instant updates never hot-loop the CPU.
|
|
681
|
+
async _delay(retryCount, abortSignal) {
|
|
489
682
|
const t = uiActions;
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
683
|
+
const minDelay = retryCount === 0 ? this.minDelay : this.retryDelays.min;
|
|
684
|
+
const delayMs = computeUpdateDelay(retryCount, this.ms, this.retryDelays, this.minDelay);
|
|
685
|
+
if (delayMs <= 0)
|
|
686
|
+
return;
|
|
687
|
+
const startedAt = Date.now();
|
|
688
|
+
if (!t.areInstantUpdatesEnabled())
|
|
689
|
+
await awaitWithCleanup2(abortSignal, "resolve", (complete, addCleanup) => {
|
|
690
|
+
const timer = setTimeout(complete, delayMs);
|
|
691
|
+
addCleanup(() => clearTimeout(timer));
|
|
494
692
|
const onChanged = () => {
|
|
495
|
-
if (t.
|
|
496
|
-
|
|
497
|
-
t.changed.remove(onChanged);
|
|
498
|
-
resolve();
|
|
499
|
-
}
|
|
693
|
+
if (t.areInstantUpdatesEnabled())
|
|
694
|
+
complete();
|
|
500
695
|
};
|
|
501
696
|
t.changed.add(onChanged);
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
() => {
|
|
512
|
-
clearTimeout(timer);
|
|
513
|
-
t.changed.remove(onChanged);
|
|
514
|
-
reject(abortSignal.reason instanceof Error ? abortSignal.reason : new Error(String(abortSignal.reason)));
|
|
515
|
-
},
|
|
516
|
-
{ once: true }
|
|
517
|
-
);
|
|
518
|
-
}
|
|
697
|
+
addCleanup(() => t.changed.remove(onChanged));
|
|
698
|
+
});
|
|
699
|
+
if (abortSignal?.aborted)
|
|
700
|
+
return;
|
|
701
|
+
const remaining = minDelay - (Date.now() - startedAt);
|
|
702
|
+
if (remaining > 0)
|
|
703
|
+
await awaitWithCleanup2(abortSignal, "resolve", (complete, addCleanup) => {
|
|
704
|
+
const timer = setTimeout(complete, remaining);
|
|
705
|
+
addCleanup(() => clearTimeout(timer));
|
|
519
706
|
});
|
|
520
|
-
};
|
|
521
|
-
_UIUpdateDelayer._cache.set(ms, delayer);
|
|
522
|
-
return delayer;
|
|
523
707
|
}
|
|
524
708
|
};
|
|
525
709
|
|
|
@@ -528,16 +712,22 @@ import {
|
|
|
528
712
|
PromiseSource as PromiseSource2,
|
|
529
713
|
resolvedVoidPromise
|
|
530
714
|
} from "@actuallab/core";
|
|
531
|
-
var State = class {
|
|
715
|
+
var State = class _State {
|
|
532
716
|
_computed;
|
|
533
717
|
_updateIndex = 0;
|
|
534
|
-
|
|
718
|
+
// Reference to the last non-error computed, not its raw value, so an actual
|
|
719
|
+
// `undefined` value is distinguishable from "no non-error value yet" (S12).
|
|
720
|
+
_lastNonErrorComputed;
|
|
535
721
|
_whenUpdatedSource = null;
|
|
722
|
+
_isDisposed = false;
|
|
536
723
|
get updateIndex() {
|
|
537
724
|
return this._updateIndex;
|
|
538
725
|
}
|
|
726
|
+
get isDisposed() {
|
|
727
|
+
return this._isDisposed;
|
|
728
|
+
}
|
|
539
729
|
get lastNonErrorValue() {
|
|
540
|
-
return this.
|
|
730
|
+
return this._lastNonErrorComputed?.value;
|
|
541
731
|
}
|
|
542
732
|
get hasValue() {
|
|
543
733
|
return this._computed.hasValue;
|
|
@@ -576,25 +766,46 @@ var State = class {
|
|
|
576
766
|
whenInvalidated() {
|
|
577
767
|
return this._computed.whenInvalidated();
|
|
578
768
|
}
|
|
579
|
-
|
|
769
|
+
// Versioned wait: resolves at once when a newer generation than sinceIndex
|
|
770
|
+
// already landed (no lost wakeup), rejects once disposed (C# StateSnapshot.WhenUpdated).
|
|
771
|
+
whenUpdated(sinceIndex = this._updateIndex) {
|
|
772
|
+
if (this._isDisposed)
|
|
773
|
+
return Promise.reject(_State._disposedError());
|
|
774
|
+
if (this._updateIndex > sinceIndex)
|
|
775
|
+
return resolvedVoidPromise;
|
|
580
776
|
return this._whenUpdatedSource ??= new PromiseSource2();
|
|
581
777
|
}
|
|
582
778
|
whenFirstTimeUpdated() {
|
|
583
|
-
return this.
|
|
779
|
+
return this.whenUpdated(0);
|
|
584
780
|
}
|
|
585
781
|
_initialize(output, renewer) {
|
|
586
|
-
this._computed =
|
|
782
|
+
this._computed = this._createComputed(renewer);
|
|
587
783
|
this._computed.setOutput(output);
|
|
588
|
-
this.
|
|
784
|
+
if (this._computed.hasValue) this._lastNonErrorComputed = this._computed;
|
|
785
|
+
}
|
|
786
|
+
_createComputed(renewer) {
|
|
787
|
+
return new StateBoundComputed(this, renewer);
|
|
589
788
|
}
|
|
590
789
|
_update(computed, output) {
|
|
790
|
+
const replaced = this._computed;
|
|
591
791
|
computed.setOutput(output);
|
|
592
792
|
this._computed = computed;
|
|
593
|
-
if (
|
|
793
|
+
if (replaced?.isConsistent) replaced.invalidate();
|
|
794
|
+
if (computed.hasValue) this._lastNonErrorComputed = computed;
|
|
594
795
|
this._updateIndex++;
|
|
595
796
|
this._whenUpdatedSource?.resolve(void 0);
|
|
596
797
|
this._whenUpdatedSource = null;
|
|
597
798
|
}
|
|
799
|
+
_onDisposed() {
|
|
800
|
+
if (this._isDisposed)
|
|
801
|
+
return;
|
|
802
|
+
this._isDisposed = true;
|
|
803
|
+
this._whenUpdatedSource?.reject(_State._disposedError());
|
|
804
|
+
this._whenUpdatedSource = null;
|
|
805
|
+
}
|
|
806
|
+
static _disposedError() {
|
|
807
|
+
return new Error("State is disposed.");
|
|
808
|
+
}
|
|
598
809
|
};
|
|
599
810
|
|
|
600
811
|
// src/computed-state.ts
|
|
@@ -604,76 +815,40 @@ import {
|
|
|
604
815
|
result as result3,
|
|
605
816
|
errorResult as errorResult2
|
|
606
817
|
} from "@actuallab/core";
|
|
607
|
-
var { errorLog:
|
|
818
|
+
var { errorLog: errorLog3 } = getLogs("ComputedState");
|
|
608
819
|
var ComputedState = class extends State {
|
|
609
820
|
_computer;
|
|
610
821
|
_updateDelayer;
|
|
611
822
|
_disposeController;
|
|
612
823
|
_cancelDelaySource = new PromiseSource3();
|
|
613
824
|
_renewer;
|
|
614
|
-
_hasOutput = false;
|
|
615
825
|
constructor(computer, options) {
|
|
616
826
|
super();
|
|
617
827
|
this._computer = computer;
|
|
618
828
|
this._updateDelayer = options?.updateDelayer ?? defaultUpdateDelayer;
|
|
619
829
|
this._disposeController = new AbortController();
|
|
620
830
|
this._renewer = async () => {
|
|
621
|
-
const
|
|
831
|
+
const sinceIndex = this._updateIndex;
|
|
622
832
|
this._cancelDelaySource.resolve(void 0);
|
|
623
|
-
await whenUpdated;
|
|
833
|
+
await this.whenUpdated(sinceIndex);
|
|
624
834
|
return this._computed;
|
|
625
835
|
};
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
this._hasOutput = true;
|
|
632
|
-
}
|
|
836
|
+
this._initialize(
|
|
837
|
+
options?.initialOutput ?? options?.initialValue,
|
|
838
|
+
this._renewer
|
|
839
|
+
);
|
|
840
|
+
this._computed.invalidate();
|
|
633
841
|
void this._updateCycle();
|
|
634
842
|
}
|
|
635
|
-
get hasValue() {
|
|
636
|
-
if (!this._hasOutput && this._updateIndex === 0) return false;
|
|
637
|
-
return this._computed.hasValue;
|
|
638
|
-
}
|
|
639
|
-
get hasError() {
|
|
640
|
-
if (!this._hasOutput && this._updateIndex === 0) return false;
|
|
641
|
-
return this._computed.hasError;
|
|
642
|
-
}
|
|
643
|
-
get value() {
|
|
644
|
-
if (!this._hasOutput && this._updateIndex === 0)
|
|
645
|
-
throw new Error("ComputedState has no value yet.");
|
|
646
|
-
if (this._computed.hasValue) return this._computed.value;
|
|
647
|
-
if (this._lastNonErrorValue !== void 0)
|
|
648
|
-
return this._lastNonErrorValue;
|
|
649
|
-
throw new Error("ComputedState has no value yet.");
|
|
650
|
-
}
|
|
651
|
-
get error() {
|
|
652
|
-
if (!this._hasOutput && this._updateIndex === 0) return void 0;
|
|
653
|
-
return this._computed.error;
|
|
654
|
-
}
|
|
655
|
-
get valueOrUndefined() {
|
|
656
|
-
if (!this._hasOutput && this._updateIndex === 0) return void 0;
|
|
657
|
-
return this._computed.valueOrUndefined ?? this._lastNonErrorValue;
|
|
658
|
-
}
|
|
659
|
-
get output() {
|
|
660
|
-
if (!this._hasOutput && this._updateIndex === 0)
|
|
661
|
-
throw new Error("ComputedState has no output yet.");
|
|
662
|
-
return this._computed.output;
|
|
663
|
-
}
|
|
664
|
-
get computed() {
|
|
665
|
-
if (!this._hasOutput && this._updateIndex === 0)
|
|
666
|
-
throw new Error("ComputedState has not been computed yet.");
|
|
667
|
-
return this._computed;
|
|
668
|
-
}
|
|
669
|
-
get isDisposed() {
|
|
670
|
-
return this._disposeController.signal.aborted;
|
|
671
|
-
}
|
|
672
843
|
dispose() {
|
|
673
|
-
if (
|
|
844
|
+
if (this.isDisposed)
|
|
845
|
+
return;
|
|
846
|
+
this._onDisposed();
|
|
847
|
+
this._disposeController.abort();
|
|
674
848
|
}
|
|
675
849
|
async _updateCycle() {
|
|
676
850
|
const disposeSignal = this._disposeController.signal;
|
|
851
|
+
let retryCount = 0;
|
|
677
852
|
try {
|
|
678
853
|
while (!disposeSignal.aborted) {
|
|
679
854
|
const computed = new StateBoundComputed(this, this._renewer);
|
|
@@ -689,7 +864,10 @@ var ComputedState = class extends State {
|
|
|
689
864
|
} catch (e) {
|
|
690
865
|
output = errorResult2(e);
|
|
691
866
|
}
|
|
867
|
+
if (this.isDisposed)
|
|
868
|
+
return;
|
|
692
869
|
this._update(computed, output);
|
|
870
|
+
retryCount = output.hasError ? retryCount + 1 : 0;
|
|
693
871
|
if (this._cancelDelaySource.isCompleted)
|
|
694
872
|
this._cancelDelaySource = new PromiseSource3();
|
|
695
873
|
try {
|
|
@@ -698,41 +876,55 @@ var ComputedState = class extends State {
|
|
|
698
876
|
return;
|
|
699
877
|
}
|
|
700
878
|
await Promise.race([
|
|
701
|
-
this._updateDelayer(disposeSignal),
|
|
879
|
+
this._updateDelayer(retryCount, disposeSignal),
|
|
702
880
|
this._cancelDelaySource
|
|
703
881
|
]);
|
|
704
882
|
}
|
|
705
883
|
} catch (e) {
|
|
706
884
|
if (!disposeSignal.aborted)
|
|
707
|
-
|
|
885
|
+
errorLog3?.log("UpdateCycle() failed and stopped", e);
|
|
708
886
|
}
|
|
709
887
|
}
|
|
710
888
|
};
|
|
711
889
|
|
|
712
890
|
// src/mutable-state.ts
|
|
713
|
-
import "@actuallab/core";
|
|
891
|
+
import { Result as Result2, result as result4 } from "@actuallab/core";
|
|
714
892
|
var MutableState = class extends State {
|
|
893
|
+
_nextOutput;
|
|
715
894
|
_renewer = () => {
|
|
716
|
-
const computed =
|
|
717
|
-
this._update(computed, this.
|
|
895
|
+
const computed = this._createComputed(this._renewer);
|
|
896
|
+
this._update(computed, this._nextOutput);
|
|
897
|
+
this._wireRenewal(computed);
|
|
718
898
|
return computed;
|
|
719
899
|
};
|
|
720
900
|
constructor(initialOutput) {
|
|
721
901
|
super();
|
|
722
|
-
this.
|
|
902
|
+
this._nextOutput = initialOutput instanceof Result2 ? initialOutput : result4(initialOutput);
|
|
903
|
+
this._initialize(this._nextOutput, this._renewer);
|
|
904
|
+
this._wireRenewal(this._computed);
|
|
723
905
|
}
|
|
724
906
|
use(asyncContext) {
|
|
725
907
|
return this._computed.use(asyncContext);
|
|
726
908
|
}
|
|
727
909
|
set(output) {
|
|
910
|
+
const next = output instanceof Result2 ? output : result4(output);
|
|
911
|
+
if (this._nextOutput.equals(next))
|
|
912
|
+
return;
|
|
913
|
+
this._nextOutput = next;
|
|
728
914
|
this._computed.invalidate();
|
|
729
|
-
|
|
915
|
+
}
|
|
916
|
+
// Private methods
|
|
917
|
+
// Renew synchronously on invalidation so a MutableState is never observed invalidated.
|
|
918
|
+
// update() de-dupes: if the computed was already renewed, it returns the latest instead.
|
|
919
|
+
_wireRenewal(computed) {
|
|
920
|
+
computed.onInvalidated(() => void computed.update());
|
|
730
921
|
}
|
|
731
922
|
};
|
|
732
923
|
export {
|
|
733
924
|
ComputeContext,
|
|
734
925
|
ComputeFunction,
|
|
735
926
|
Computed,
|
|
927
|
+
ComputedOptions,
|
|
736
928
|
ComputedRegistry,
|
|
737
929
|
ComputedState,
|
|
738
930
|
ConsistencyState,
|