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