resultar 3.1.0 → 3.2.1

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/README.md CHANGED
@@ -170,12 +170,17 @@ const user = ResultAsync.retryOrElse(
170
170
  {
171
171
  times: 2,
172
172
  delayMs: ({ nextAttempt }) => nextAttempt * 100,
173
+ jittered: 0.5,
173
174
  while: (error) => error._tag === 'RateLimitError' || error._tag === 'ServiceUnavailableError',
174
175
  orElse: () => readCachedUser(id),
175
176
  },
176
177
  )
177
178
  ```
178
179
 
180
+ `jittered` randomizes the computed retry delay by a percentage factor to avoid synchronized retries.
181
+ For example, `delayMs: 100` with `jittered: 0.5` waits between `50ms` and `150ms`; omit it or use
182
+ `0` to keep the exact delay.
183
+
179
184
  Race a primary and replica with cooperative loser abort:
180
185
 
181
186
  ```ts
@@ -640,6 +645,7 @@ const user = ResultAsync.retry(
640
645
  {
641
646
  times: 2,
642
647
  delayMs: ({ nextAttempt }) => nextAttempt * 100,
648
+ jittered: 0.2,
643
649
  while: (error) => error._tag === 'RateLimitError',
644
650
  },
645
651
  )
@@ -653,6 +659,9 @@ const withFallback = ResultAsync.retryOrElse(
653
659
  )
654
660
  ```
655
661
 
662
+ Use `jittered` when many callers may retry at the same time. The value is a delay spread factor:
663
+ `jittered: 0.2` applies a random delay from `80%` to `120%` of the computed `delayMs`.
664
+
656
665
  For resourceful async work, use `ResultAsync.withResource` when acquisition and cleanup must stay
657
666
  paired. Cleanup is best-effort and always runs after successful acquisition, including when the use
658
667
  step returns `Err` or rejects.
@@ -932,8 +941,8 @@ pnpm exec resultar-lint doctor
932
941
  pnpm exec resultar-lint unpatch
933
942
  ```
934
943
 
935
- For TypeScript 7 native-preview projects, `resultar-tsgo` exposes a `tsgo` wrapper that runs native
936
- TypeScript and then Resultar lint validation.
944
+ For TypeScript 7 projects using `typescript@rc`, `resultar-tsgo` exposes a `tsgo` wrapper that runs
945
+ native TypeScript and then Resultar lint validation.
937
946
 
938
947
  ## API Decision Guide
939
948
 
@@ -1021,12 +1030,11 @@ This repository is a pnpm workspace:
1021
1030
 
1022
1031
  - `packages/resultar`: Resultar runtime package.
1023
1032
  - `packages/resultar-lint`: TypeScript language-service diagnostics and no-discard CLI.
1024
- - `packages/resultar-tsgo`: TypeScript 7 native-preview `tsgo` wrapper plus Resultar lint
1025
- validation.
1033
+ - `packages/resultar-tsgo`: TypeScript 7 `typescript@rc` wrapper plus Resultar lint validation.
1026
1034
  - `benchmarks`: benchmark package.
1027
1035
  - `examples/resultar`: runnable core Resultar cookbook.
1028
1036
  - `examples/lint`: TypeScript 6 Resultar lint, patched `tsc`, and Vite+ / Oxlint smoke example.
1029
- - `examples/tsgo-lint`: TypeScript 7 native-preview Resultar lint smoke example.
1037
+ - `examples/tsgo-lint`: TypeScript 7 `typescript@rc` Resultar lint smoke example.
1030
1038
 
1031
1039
  Common commands:
1032
1040
 
package/dist/index.d.ts CHANGED
@@ -432,6 +432,7 @@ type ResultAsyncRetryTaskErr<Task> = Task extends ResultAsyncRetryTask<unknown,
432
432
  type ResultAsyncRetryDelay = number | ((context: ResultAsyncRetryContext) => number);
433
433
  interface ResultAsyncRetryOptions<E> {
434
434
  readonly delayMs?: ResultAsyncRetryDelay;
435
+ readonly jittered?: number;
435
436
  readonly onRetry?: (error: E, context: ResultAsyncRetryContext) => void | Promise<void>;
436
437
  readonly signal?: ResultAsyncAbortSignal;
437
438
  readonly times: number;
package/dist/index.js CHANGED
@@ -743,13 +743,34 @@ const validateResultAsyncRetryTimes = (times) => {
743
743
  const validateResultAsyncRetryDelayValue = (delayMs) => {
744
744
  if (!Number.isFinite(delayMs) || delayMs < 0) throw createIllegalArgumentException("ResultAsync retry delayMs must be a non-negative finite number");
745
745
  };
746
+ const validateResultAsyncRetryJittered = (jittered) => {
747
+ if (!Number.isFinite(jittered) || jittered < 0) throw createIllegalArgumentException("ResultAsync retry jittered must be a non-negative finite number");
748
+ };
749
+ const validateResultAsyncRetryJitterRandomValue = (value) => {
750
+ if (!Number.isFinite(value) || value < 0 || value > 1) throw createIllegalArgumentException("ResultAsync retry jitter random value must be a finite number between 0 and 1");
751
+ };
752
+ const validateResultAsyncRetryStaticJittered = (jittered) => {
753
+ if (jittered !== void 0) validateResultAsyncRetryJittered(jittered);
754
+ };
746
755
  const validateResultAsyncRetryStaticDelay = (delay) => {
747
756
  if (typeof delay === "number") validateResultAsyncRetryDelayValue(delay);
748
757
  };
749
- const normalizeResultAsyncRetryDelay = (delay, context) => {
758
+ const getResultAsyncRetryJitterRandom = () => {
759
+ const value = Math.random();
760
+ validateResultAsyncRetryJitterRandomValue(value);
761
+ return value;
762
+ };
763
+ const applyResultAsyncRetryJitter = (delayMs, jittered) => {
764
+ if (jittered === void 0 || jittered === 0 || delayMs === 0) return delayMs;
765
+ const random = getResultAsyncRetryJitterRandom();
766
+ const range = delayMs * jittered;
767
+ const min = Math.max(0, delayMs - range);
768
+ return min + (delayMs + range - min) * random;
769
+ };
770
+ const normalizeResultAsyncRetryDelay = (delay, jittered, context) => {
750
771
  const delayMs = typeof delay === "function" ? delay(context) : delay ?? 0;
751
772
  validateResultAsyncRetryDelayValue(delayMs);
752
- return delayMs;
773
+ return applyResultAsyncRetryJitter(delayMs, jittered);
753
774
  };
754
775
  const waitResultAsyncRetryDelay = async (delayMs, signal) => {
755
776
  if (signal.aborted) return Result.err(createResultAsyncRetryAbortError(signal));
@@ -776,6 +797,7 @@ const resolveResultAsyncRetryRecovery = async (recovered) => recovered instanceo
776
797
  const runResultAsyncRetryAttempts = async (task, options, recover) => {
777
798
  validateResultAsyncRetryTimes(options.times);
778
799
  validateResultAsyncRetryStaticDelay(options.delayMs);
800
+ validateResultAsyncRetryStaticJittered(options.jittered);
779
801
  const signal = options.signal ?? new AbortController().signal;
780
802
  let attempt = 0;
781
803
  while (true) {
@@ -785,7 +807,7 @@ const runResultAsyncRetryAttempts = async (task, options, recover) => {
785
807
  const context = createResultAsyncRetryContext(attempt, options.times);
786
808
  if (!await shouldRetryResultAsyncError(options, result.error, context) || attempt >= options.times) return resolveResultAsyncRetryRecovery(recover(result.error, context));
787
809
  await callResultAsyncRetryOnRetry(options, result.error, context);
788
- const delayResult = await waitResultAsyncRetryDelay(normalizeResultAsyncRetryDelay(options.delayMs, context), signal);
810
+ const delayResult = await waitResultAsyncRetryDelay(normalizeResultAsyncRetryDelay(options.delayMs, options.jittered, context), signal);
789
811
  if (delayResult.isErr()) return Result.err(delayResult.error);
790
812
  attempt += 1;
791
813
  }