@actuallab/core 13.0.4 → 13.0.12

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.d.ts CHANGED
@@ -183,6 +183,133 @@ declare class PromiseSource<T> {
183
183
  }
184
184
  declare const resolvedVoidPromise: Promise<void>;
185
185
 
186
+ /**
187
+ * A {@link PromiseSource} with a built-in timer that can reject (or run a
188
+ * caller-supplied callback) after a configurable delay. The timer is
189
+ * automatically cleared when the promise settles by other means, so callers
190
+ * don't have to remember to `clearTimeout()` on every success path.
191
+ *
192
+ * Typical use:
193
+ * ```ts
194
+ * const ps = new PromiseSourceWithTimeout<string>();
195
+ * ps.setTimeout(5_000); // rejects after 5s if still pending
196
+ * ps.setTimeout(5_000, () => ps.resolve('fallback')); // or run a custom callback
197
+ * ```
198
+ *
199
+ * Replaces the `PromiseSourceWithTimeout` previously kept in
200
+ * `ActualChat/src/nodejs/src/promises.ts`. Note: the Voxt original supported a
201
+ * "precise" (requestAnimationFrame-based) variant; we drop it here because no
202
+ * caller in ActualChat uses it outside `promises.ts` itself.
203
+ */
204
+ declare class PromiseSourceWithTimeout<T> extends PromiseSource<T> {
205
+ private _timer;
206
+ hasTimeout(): boolean;
207
+ /**
208
+ * Install (or replace) a timeout. Pass `null` to remove without setting a
209
+ * new one. `callback` is invoked when the timer fires — if omitted, the
210
+ * promise is rejected with a generic "timed out" error.
211
+ */
212
+ setTimeout(ms: number | null, callback?: () => unknown): void;
213
+ clearTimeout(): void;
214
+ resolve(value: T): boolean;
215
+ reject(reason?: unknown): boolean;
216
+ }
217
+
218
+ /**
219
+ * Pre-resolved promises for the three values most often returned as a
220
+ * "no-op" successful path: `void`, `true`, `false`. Returning these
221
+ * avoids allocating a fresh promise per call site.
222
+ *
223
+ * Idiomatic use:
224
+ * ```ts
225
+ * if (alreadyInitialized) return ResolvedPromise.Void;
226
+ * ```
227
+ */
228
+ declare const ResolvedPromise: {
229
+ readonly Void: Promise<void>;
230
+ readonly True: Promise<boolean>;
231
+ readonly False: Promise<boolean>;
232
+ };
233
+
234
+ /**
235
+ * Sentinel returned/thrown by code that distinguishes "the operation took
236
+ * too long" from a generic rejection. Identity-comparable via the
237
+ * `instance` singleton — callers can branch on `result === TimedOut.instance`.
238
+ */
239
+ declare class TimedOut {
240
+ static readonly instance: TimedOut;
241
+ }
242
+
243
+ /** Error type rejected by {@link withTimeout} when its budget elapses. */
244
+ declare class TimeoutError extends Error {
245
+ readonly name = "TimeoutError";
246
+ constructor(message?: string);
247
+ }
248
+ /**
249
+ * Race `promise` against a `ms`-millisecond timer. Resolves with the inner
250
+ * promise's value if it settles first; otherwise rejects with a
251
+ * {@link TimeoutError} carrying `message`. The timer is always cleared in
252
+ * a `finally`, so it cannot keep the event loop alive after the race ends.
253
+ *
254
+ * The inner promise's rejection (and the timeout's) are routed through
255
+ * `Promise.race`, so callers should `await` the result inside a `try/catch`
256
+ * exactly as they would for a single awaited promise. There's no
257
+ * "unhandled rejection" hazard from the timeout branch — the race always
258
+ * observes whichever promise wins, and the loser is allowed to settle
259
+ * silently into the void.
260
+ */
261
+ declare function withTimeout<T>(promise: PromiseLike<T>, ms: number, message?: string): Promise<T>;
262
+
263
+ /** Resolves after `ms` milliseconds. */
264
+ declare function delayAsync(ms: number): Promise<void>;
265
+ /** Resolves with `value` after `ms` milliseconds. */
266
+ declare function delayAsyncWith<T>(ms: number, value: T): Promise<T>;
267
+
268
+ declare function abortPromise(signal: AbortSignal): Promise<never>;
269
+
270
+ /** A function wrapped by {@link throttle} or {@link debounce}. Calling it
271
+ * triggers the underlying function according to the scheduling rule;
272
+ * `reset()` discards any pending scheduled call. */
273
+ interface ResettableFunc<TArgs extends unknown[]> {
274
+ (...args: TArgs): void;
275
+ reset(): void;
276
+ }
277
+ /** Throttle modes:
278
+ * - `default` — fire the head call immediately, then drop further calls
279
+ * until the interval elapses; the *last* one is fired at interval end.
280
+ * - `skip` — fire the head call immediately, drop ALL calls during the
281
+ * interval (no tail call).
282
+ * - `delayHead` — delay even the head call by one interval; subsequent
283
+ * calls overwrite the pending one. */
284
+ type ThrottleMode = 'default' | 'skip' | 'delayHead';
285
+ /**
286
+ * Throttle calls to `func` so that it fires at most once every `intervalMs`.
287
+ * The returned function has a `.reset()` that discards any pending tail call
288
+ * and re-arms the head-call behavior on the next invocation.
289
+ */
290
+ declare function throttle<TArgs extends unknown[]>(func: (...args: TArgs) => unknown, intervalMs: number, mode?: ThrottleMode): ResettableFunc<TArgs>;
291
+ /**
292
+ * Debounce calls to `func`: the function fires only after `intervalMs` have
293
+ * elapsed since the LAST call. Each new call resets the timer. `.reset()`
294
+ * discards any pending fire without invoking `func`.
295
+ */
296
+ declare function debounce<TArgs extends unknown[]>(func: (...args: TArgs) => unknown, intervalMs: number): ResettableFunc<TArgs>;
297
+
298
+ /**
299
+ * Wrap an async function so concurrent invocations are serialized into a
300
+ * single chain — each call awaits the previous one before running, and the
301
+ * caller receives the result of *its* own invocation.
302
+ *
303
+ * Useful for stateful async operations (DB writes, file I/O, network I/O
304
+ * that must not overlap) where the underlying contract is "one at a time."
305
+ *
306
+ * If `limit` is set and the queue is already that deep, the call short-
307
+ * circuits and returns the same promise the most recent queued call returned
308
+ * — i.e. it coalesces excess pressure onto the trailing in-flight call.
309
+ * Pass `null` (the default) for unbounded queueing.
310
+ */
311
+ declare function serialize<TArgs extends unknown[], TResult>(func: (...args: TArgs) => PromiseLike<TResult> | TResult, limit?: number | null): (...args: TArgs) => Promise<TResult>;
312
+
186
313
  /**
187
314
  * A fixed-capacity circular buffer supporting efficient push/pull at both
188
315
  * head and tail.
@@ -250,4 +377,29 @@ declare class RetryDelayer {
250
377
  cancelDelays(): void;
251
378
  }
252
379
 
253
- export { AsyncContext, AsyncContextKey, type AsyncDisposable, AsyncLock, type Disposable, DisposableBag, EventHandlerSet, type IResult, Log, LogLevel, LogLevelController, type LogRef, type LogScopeFns, PromiseSource, Result, type RetryDelay, RetryDelayLimitExceeded, RetryDelayNone, RetryDelaySeq, RetryDelayer, RingBuffer, abortSignalKey, createLogProvider, errorResult, initLogging, initWorkerLogging, resolvedVoidPromise, result, resultFrom, resultFromAsync };
380
+ /** A sequence of retry delays — given a 1-based try index (1 = first
381
+ * retry, i.e. AFTER the initial failure), returns the delay in ms before
382
+ * the next attempt. For more elaborate delay schemes, see
383
+ * {@link RetryDelaySeq.getDelay}. */
384
+ type RetryDelaySchedule = (tryIndex: number) => number;
385
+ /**
386
+ * Run `fn` up to `tryCount` times, awaiting a delay between attempts.
387
+ * The last error is rethrown if every attempt fails.
388
+ *
389
+ * `fn` is passed the 0-based attempt index and the last error (or
390
+ * `undefined` for the first attempt) — useful for adapting behavior per
391
+ * try (e.g. switching endpoints, escalating timeouts).
392
+ */
393
+ declare function retry<TResult>(tryCount: number, fn: (tryIndex: number, lastError: unknown) => PromiseLike<TResult> | TResult, retryDelays?: RetryDelaySchedule): Promise<TResult>;
394
+ /**
395
+ * Run `fn` and swallow any throw, returning `onError(e)` if provided or
396
+ * `undefined` otherwise. The returned promise always settles successfully
397
+ * — the caller never has to `try/catch` it.
398
+ *
399
+ * Useful where a logical "best-effort" return is wanted but the runtime
400
+ * defaults — unhandled rejection warnings, exception escape past finally
401
+ * blocks — are not.
402
+ */
403
+ declare function catchErrors<TResult>(fn: () => PromiseLike<TResult> | TResult, onError?: (e: unknown) => TResult): Promise<TResult | undefined>;
404
+
405
+ export { AsyncContext, AsyncContextKey, type AsyncDisposable, AsyncLock, type Disposable, DisposableBag, EventHandlerSet, type IResult, Log, LogLevel, LogLevelController, type LogRef, type LogScopeFns, PromiseSource, PromiseSourceWithTimeout, type ResettableFunc, ResolvedPromise, Result, type RetryDelay, RetryDelayLimitExceeded, RetryDelayNone, type RetryDelaySchedule, RetryDelaySeq, RetryDelayer, RingBuffer, type ThrottleMode, TimedOut, TimeoutError, abortPromise, abortSignalKey, catchErrors, createLogProvider, debounce, delayAsync, delayAsyncWith, errorResult, initLogging, initWorkerLogging, resolvedVoidPromise, result, resultFrom, resultFromAsync, retry, serialize, throttle, withTimeout };
package/dist/index.js CHANGED
@@ -649,6 +649,231 @@ var EventHandlerSet = class {
649
649
  }
650
650
  };
651
651
 
652
+ // src/withTimeout.ts
653
+ var TimeoutError = class extends Error {
654
+ name = "TimeoutError";
655
+ constructor(message = "Operation timed out") {
656
+ super(message);
657
+ }
658
+ };
659
+ async function withTimeout(promise, ms, message) {
660
+ let timer;
661
+ const timeoutPromise = new Promise((_, reject) => {
662
+ timer = setTimeout(
663
+ () => reject(new TimeoutError(message ?? `Operation timed out after ${ms}ms`)),
664
+ ms
665
+ );
666
+ });
667
+ timeoutPromise.catch(() => {
668
+ });
669
+ try {
670
+ return await Promise.race([promise, timeoutPromise]);
671
+ } finally {
672
+ if (timer !== void 0) clearTimeout(timer);
673
+ }
674
+ }
675
+
676
+ // src/promise-source-with-timeout.ts
677
+ var PromiseSourceWithTimeout = class extends PromiseSource {
678
+ _timer;
679
+ hasTimeout() {
680
+ return this._timer !== void 0;
681
+ }
682
+ /**
683
+ * Install (or replace) a timeout. Pass `null` to remove without setting a
684
+ * new one. `callback` is invoked when the timer fires — if omitted, the
685
+ * promise is rejected with a generic "timed out" error.
686
+ */
687
+ setTimeout(ms, callback) {
688
+ this.clearTimeout();
689
+ if (ms === null || this.isCompleted) return;
690
+ this._timer = setTimeout(() => {
691
+ this._timer = void 0;
692
+ if (callback) callback();
693
+ else this.reject(new TimeoutError(`The promise has timed out after ${ms}ms.`));
694
+ }, ms);
695
+ }
696
+ clearTimeout() {
697
+ if (this._timer === void 0) return;
698
+ clearTimeout(this._timer);
699
+ this._timer = void 0;
700
+ }
701
+ resolve(value) {
702
+ const didSettle = super.resolve(value);
703
+ if (didSettle) this.clearTimeout();
704
+ return didSettle;
705
+ }
706
+ reject(reason) {
707
+ const didSettle = super.reject(reason);
708
+ if (didSettle) this.clearTimeout();
709
+ return didSettle;
710
+ }
711
+ };
712
+
713
+ // src/resolved-promise.ts
714
+ var ResolvedPromise = {
715
+ Void: Promise.resolve(),
716
+ True: Promise.resolve(true),
717
+ False: Promise.resolve(false)
718
+ };
719
+
720
+ // src/timed-out.ts
721
+ var TimedOut = class _TimedOut {
722
+ static instance = new _TimedOut();
723
+ };
724
+
725
+ // src/delay.ts
726
+ function delayAsync(ms) {
727
+ return new Promise((resolve) => {
728
+ setTimeout(resolve, ms);
729
+ });
730
+ }
731
+ function delayAsyncWith(ms, value) {
732
+ return new Promise((resolve) => {
733
+ setTimeout(() => resolve(value), ms);
734
+ });
735
+ }
736
+
737
+ // src/abort-promise.ts
738
+ var abortPromiseCache = /* @__PURE__ */ new WeakMap();
739
+ function abortPromise(signal) {
740
+ if (signal.aborted) return Promise.reject(signal.reason);
741
+ let cached = abortPromiseCache.get(signal);
742
+ if (!cached) {
743
+ cached = new Promise((_, reject) => {
744
+ signal.addEventListener("abort", () => reject(signal.reason), { once: true });
745
+ });
746
+ cached.catch(() => {
747
+ });
748
+ abortPromiseCache.set(signal, cached);
749
+ }
750
+ return cached;
751
+ }
752
+
753
+ // src/internal-logging.ts
754
+ var scopeDefaults = {
755
+ retry: 3 /* Warn */,
756
+ throttle: 3 /* Warn */
757
+ };
758
+ var getLogs = createLogProvider("core.", scopeDefaults);
759
+
760
+ // src/throttle.ts
761
+ var { errorLog } = getLogs("throttle");
762
+ var Call = class {
763
+ constructor(func, self, parameters) {
764
+ this.func = func;
765
+ this.self = self;
766
+ this.parameters = parameters;
767
+ }
768
+ invoke() {
769
+ return this.func.apply(this.self, this.parameters);
770
+ }
771
+ invokeSilently() {
772
+ try {
773
+ return this.invoke();
774
+ } catch (error) {
775
+ errorLog?.log("Call.invokeSilently: unhandled error:", error);
776
+ }
777
+ }
778
+ };
779
+ function throttle(func, intervalMs, mode = "default") {
780
+ let lastCall = null;
781
+ let nextFireTime = 0;
782
+ let timeoutHandle = null;
783
+ const reset2 = () => {
784
+ if (timeoutHandle !== null) clearTimeout(timeoutHandle);
785
+ timeoutHandle = null;
786
+ lastCall = null;
787
+ nextFireTime = 0;
788
+ };
789
+ const fire = () => {
790
+ if (timeoutHandle !== null) clearTimeout(timeoutHandle);
791
+ if (lastCall !== null) {
792
+ const call = lastCall;
793
+ lastCall = null;
794
+ nextFireTime = Date.now() + intervalMs;
795
+ timeoutHandle = setTimeout(fire, intervalMs);
796
+ call.invokeSilently();
797
+ } else {
798
+ timeoutHandle = null;
799
+ nextFireTime = 0;
800
+ }
801
+ };
802
+ const wrapped = function(...args) {
803
+ const call = new Call(func, this, args);
804
+ const fireDelay = nextFireTime - Date.now();
805
+ if (timeoutHandle !== null && fireDelay <= 0) {
806
+ fire();
807
+ }
808
+ if (timeoutHandle === null) {
809
+ nextFireTime = Date.now() + intervalMs;
810
+ timeoutHandle = setTimeout(fire, intervalMs);
811
+ if (mode === "delayHead") {
812
+ lastCall = call;
813
+ } else {
814
+ call.invokeSilently();
815
+ }
816
+ } else if (mode !== "skip") {
817
+ lastCall = call;
818
+ }
819
+ };
820
+ wrapped.reset = reset2;
821
+ return wrapped;
822
+ }
823
+ function debounce(func, intervalMs) {
824
+ let lastCall = null;
825
+ let nextFireTime = 0;
826
+ let timeoutHandle = null;
827
+ const reset2 = () => {
828
+ timeoutHandle = null;
829
+ lastCall = null;
830
+ nextFireTime = 0;
831
+ };
832
+ const fire = () => {
833
+ if (timeoutHandle !== null) clearTimeout(timeoutHandle);
834
+ timeoutHandle = null;
835
+ const fireDelay = nextFireTime - Date.now();
836
+ if (fireDelay <= 0) {
837
+ nextFireTime = 0;
838
+ if (lastCall !== null) {
839
+ const call = lastCall;
840
+ lastCall = null;
841
+ call.invokeSilently();
842
+ }
843
+ } else {
844
+ timeoutHandle = setTimeout(fire, fireDelay);
845
+ }
846
+ };
847
+ const wrapped = function(...args) {
848
+ lastCall = new Call(func, this, args);
849
+ nextFireTime = Date.now() + intervalMs;
850
+ timeoutHandle ??= setTimeout(fire, intervalMs);
851
+ };
852
+ wrapped.reset = reset2;
853
+ return wrapped;
854
+ }
855
+
856
+ // src/serialize.ts
857
+ function serialize(func, limit = null) {
858
+ let lastCall = Promise.resolve(null);
859
+ let queueSize = 0;
860
+ return function(...args) {
861
+ if (limit !== null && queueSize >= limit) return lastCall;
862
+ queueSize++;
863
+ const prevCall = lastCall;
864
+ const invoke = () => func.apply(this, args);
865
+ return lastCall = (async () => {
866
+ try {
867
+ await prevCall.catch(() => {
868
+ });
869
+ return await invoke();
870
+ } finally {
871
+ queueSize--;
872
+ }
873
+ })();
874
+ };
875
+ }
876
+
652
877
  // src/ring-buffer.ts
653
878
  var RingBuffer = class {
654
879
  _buffer;
@@ -839,6 +1064,34 @@ var RetryDelayer = class {
839
1064
  this.cancelDelaysChanged.trigger();
840
1065
  }
841
1066
  };
1067
+
1068
+ // src/retry.ts
1069
+ var { warnLog } = getLogs("retry");
1070
+ var defaultRetryDelays = () => 50;
1071
+ async function retry(tryCount, fn, retryDelays = defaultRetryDelays) {
1072
+ if (tryCount <= 0)
1073
+ throw new Error("retry: tryCount must be positive.");
1074
+ let lastError;
1075
+ for (let tryIndex = 0; ; ) {
1076
+ if (tryIndex >= tryCount) throw lastError;
1077
+ try {
1078
+ return await fn(tryIndex, lastError);
1079
+ } catch (e) {
1080
+ lastError = e;
1081
+ }
1082
+ ++tryIndex;
1083
+ if (tryIndex >= tryCount) throw lastError;
1084
+ warnLog?.log(`retry(${tryIndex}/${tryCount}): error:`, lastError);
1085
+ await delayAsync(retryDelays(tryIndex));
1086
+ }
1087
+ }
1088
+ async function catchErrors(fn, onError) {
1089
+ try {
1090
+ return await fn();
1091
+ } catch (e) {
1092
+ return onError ? onError(e) : void 0;
1093
+ }
1094
+ }
842
1095
  export {
843
1096
  AsyncContext,
844
1097
  AsyncContextKey,
@@ -849,20 +1102,33 @@ export {
849
1102
  LogLevel,
850
1103
  LogLevelController,
851
1104
  PromiseSource,
1105
+ PromiseSourceWithTimeout,
1106
+ ResolvedPromise,
852
1107
  Result,
853
1108
  RetryDelayLimitExceeded,
854
1109
  RetryDelayNone,
855
1110
  RetryDelaySeq,
856
1111
  RetryDelayer,
857
1112
  RingBuffer,
1113
+ TimedOut,
1114
+ TimeoutError,
1115
+ abortPromise,
858
1116
  abortSignalKey,
1117
+ catchErrors,
859
1118
  createLogProvider,
1119
+ debounce,
1120
+ delayAsync,
1121
+ delayAsyncWith,
860
1122
  errorResult,
861
1123
  initLogging,
862
1124
  initWorkerLogging,
863
1125
  resolvedVoidPromise,
864
1126
  result,
865
1127
  resultFrom,
866
- resultFromAsync
1128
+ resultFromAsync,
1129
+ retry,
1130
+ serialize,
1131
+ throttle,
1132
+ withTimeout
867
1133
  };
868
1134
  //# sourceMappingURL=index.js.map