@shivaedev/effect-test 0.8.1 → 0.9.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0 - 2026-10-25
4
+
5
+ ### Added
6
+
7
+ - `@shivaedev/effect-test` readiness retries accept `timeout` and `wait` options. A timeout preserves the final typed failure; a custom wait lets adapters settle observable work through the same retry engine.
8
+
9
+ ### Breaking
10
+
11
+ - `@shivaedev/effect-react/client/testing.ts` now returns an Effect from `eventually(assert)`. Replace `yield* Effect.promise(() => eventually(assert))` with `yield* eventually(assert)` in stories and Effect tests, or replace `await eventually(assert)` with `await Effect.runPromise(eventually(assert))` in async tests. Assertions wait inside React `act` in live time, including under TestClock. Timeout errors include elapsed milliseconds and the last assertion message, with the assertion error as their `cause`.
12
+
3
13
  ## 0.8.1 - 2026-10-25
4
14
 
5
15
  ### Fixed
package/README.md CHANGED
@@ -163,7 +163,7 @@ Both test and live clock fixtures install a fresh native TestConsole for each te
163
163
 
164
164
  ### Waiting for a typed readiness condition
165
165
 
166
- `eventually(effect, options)` retries the Effect's typed failures until it succeeds or uses its retry budget. It uses the active Clock: under TestClock it advances time with `TestClock.adjust`; under live time it retries with `Schedule.spaced`.
166
+ `eventually(effect, options)` retries the Effect's typed failures until it succeeds or uses its retry budget. It uses the active Clock: under TestClock it advances time with `TestClock.adjust`; under live time it waits with `Effect.sleep`.
167
167
 
168
168
  ```ts
169
169
  import { eventually } from "@shivaedev/effect-test/eventually.ts";
@@ -181,7 +181,7 @@ effectApp("reaches the selected time", function* () {
181
181
  });
182
182
  ```
183
183
 
184
- The attempts see 0, 7, 14 and subsequent multiples of 7 through 56 milliseconds. `times` counts retries after the initial attempt, so `times: 0` tries once. When the budget runs out, the final typed failure remains the failure.
184
+ The attempts see 0, 7, 14 and subsequent multiples of 7 through 56 milliseconds. `times` counts retries after the initial attempt, so `times: 0` tries once. When the budget runs out, the final typed failure remains the failure. `timeout` sets an elapsed-time budget on the active clock, checked after a failed attempt. `wait` replaces the interval delay with a settlement Effect, so adapters such as React testing can own how pending work settles.
185
185
 
186
186
  A thrown assertion is a defect, so it escapes on the first attempt. Interruption also escapes immediately. Capture an assertion with `Effect.try` only when its failure means the state is not ready yet:
187
187
 
@@ -1,7 +1,9 @@
1
- import { type Duration, Effect } from "effect";
1
+ import { Duration, Effect } from "effect";
2
2
  export interface EventuallyOptions {
3
3
  readonly interval?: Duration.Input;
4
+ readonly timeout?: Duration.Input;
4
5
  readonly times?: number;
6
+ readonly wait?: Effect.Effect<void>;
5
7
  }
6
8
  export declare const eventually: <A, E, R>(effect: Effect.Effect<A, E, R>, options?: EventuallyOptions | undefined) => Effect.Effect<A, E, R>;
7
9
  //# sourceMappingURL=eventually.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"eventually.d.ts","sourceRoot":"","sources":["../src/eventually.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,QAAQ,EAAE,MAAM,EAAoB,MAAM,QAAQ,CAAC;AAGxE,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACxB;AAsCD,eAAO,MAAM,UAAU,GAAgC,CAAC,EAAE,CAAC,EAAE,CAAC,oGAQ5D,CAAC"}
1
+ {"version":3,"file":"eventually.d.ts","sourceRoot":"","sources":["../src/eventually.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,QAAQ,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAGzD,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC;IACnC,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CACpC;AAED,eAAO,MAAM,UAAU,GAAgC,CAAC,EAAE,CAAC,EAAE,CAAC,oGAwB5D,CAAC"}
@@ -1,37 +1,25 @@
1
- import { Clock, Effect, Result, Schedule } from "effect";
1
+ import { Clock, Duration, Effect, Result } from "effect";
2
2
  import * as TestClock from "effect/testing/TestClock";
3
- const DEFAULT_INTERVAL = "10 millis";
4
- function isTestClock(clock) {
5
- return "adjust" in clock;
6
- }
7
- function retryOnLiveClock(effect, interval, times) {
8
- return times === undefined
9
- ? Effect.retry(effect, Schedule.spaced(interval))
10
- : Effect.retry(effect, {
11
- schedule: Schedule.spaced(interval),
12
- times,
13
- });
14
- }
15
- const retryOnTestClock = Effect.fnUntraced(function* (effect, interval, times) {
16
- let retriesLeft = times;
3
+ export const eventually = Effect.fnUntraced(function* (effect, options) {
4
+ const clock = yield* Clock.Clock;
5
+ const started = yield* Clock.currentTimeMillis;
6
+ const timeout = options?.timeout === undefined ? undefined : Duration.toMillis(Duration.fromInputUnsafe(options.timeout));
7
+ const interval = options?.interval ?? "10 millis";
8
+ const wait = options?.wait ?? ("adjust" in clock ? TestClock.adjust(interval) : Effect.sleep(interval));
9
+ let retriesLeft = options?.times;
17
10
  for (;;) {
18
11
  const result = yield* Effect.result(effect);
19
12
  if (Result.isSuccess(result)) {
20
13
  return result.success;
21
14
  }
22
- if (retriesLeft === 0) {
15
+ const elapsed = (yield* Clock.currentTimeMillis) - started;
16
+ if (retriesLeft === 0 || (timeout !== undefined && elapsed >= timeout)) {
23
17
  return yield* Effect.fail(result.failure);
24
18
  }
25
19
  if (retriesLeft !== undefined) {
26
20
  retriesLeft -= 1;
27
21
  }
28
- yield* TestClock.adjust(interval);
22
+ yield* wait;
29
23
  }
30
24
  });
31
- export const eventually = Effect.fnUntraced(function* (effect, options) {
32
- const interval = options?.interval ?? DEFAULT_INTERVAL;
33
- const clock = yield* Clock.Clock;
34
- const retry = isTestClock(clock) ? retryOnTestClock : retryOnLiveClock;
35
- return yield* retry(effect, interval, options?.times);
36
- });
37
25
  //# sourceMappingURL=eventually.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"eventually.js","sourceRoot":"","sources":["../src/eventually.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAiB,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACxE,OAAO,KAAK,SAAS,MAAM,0BAA0B,CAAC;AAOtD,MAAM,gBAAgB,GAAG,WAAW,CAAC;AAErC,SAAS,WAAW,CAAC,KAAkB;IACtC,OAAO,QAAQ,IAAI,KAAK,CAAC;AAC1B,CAAC;AAED,SAAS,gBAAgB,CAAU,MAA8B,EAAE,QAAwB,EAAE,KAAyB;IACrH,OAAO,KAAK,KAAK,SAAS;QACzB,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACjD,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE;YACrB,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC;YACnC,KAAK;SACL,CAAC,CAAC;AACN,CAAC;AAED,MAAM,gBAAgB,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EACnD,MAA8B,EAC9B,QAAwB,EACxB,KAAyB;IAEzB,IAAI,WAAW,GAAG,KAAK,CAAC;IACxB,SAAS,CAAC;QACT,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC5C,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;YAC9B,OAAO,MAAM,CAAC,OAAO,CAAC;QACvB,CAAC;QACD,IAAI,WAAW,KAAK,CAAC,EAAE,CAAC;YACvB,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC3C,CAAC;QACD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC/B,WAAW,IAAI,CAAC,CAAC;QAClB,CAAC;QACD,KAAK,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACnC,CAAC;AACF,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EACpD,MAA8B,EAC9B,OAA2B;IAE3B,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,gBAAgB,CAAC;IACvD,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;IACjC,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,gBAAgB,CAAC;IACvE,OAAO,KAAK,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;AACvD,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"eventually.js","sourceRoot":"","sources":["../src/eventually.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,KAAK,SAAS,MAAM,0BAA0B,CAAC;AAStD,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EACpD,MAA8B,EAC9B,OAA2B;IAE3B,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;IACjC,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC;IAC/C,MAAM,OAAO,GAAG,OAAO,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,eAAe,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IAC1H,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,WAAW,CAAC;IAClD,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;IACxG,IAAI,WAAW,GAAG,OAAO,EAAE,KAAK,CAAC;IACjC,SAAS,CAAC;QACT,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC5C,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;YAC9B,OAAO,MAAM,CAAC,OAAO,CAAC;QACvB,CAAC;QACD,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,GAAG,OAAO,CAAC;QAC3D,IAAI,WAAW,KAAK,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,IAAI,OAAO,CAAC,EAAE,CAAC;YACxE,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC3C,CAAC;QACD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC/B,WAAW,IAAI,CAAC,CAAC;QAClB,CAAC;QACD,KAAK,CAAC,CAAC,IAAI,CAAC;IACb,CAAC;AACF,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/effect-test",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Generic Effect Vitest runner with file-scoped Layers and TestClock",
5
5
  "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/effect-test#readme",
6
6
  "bugs": {
@@ -42,7 +42,7 @@
42
42
  ],
43
43
  "devDependencies": {
44
44
  "@effect/vitest": "4.0.5",
45
- "@shivaedev/quality": "0.38.0",
45
+ "@shivaedev/quality": "0.39.1",
46
46
  "@types/node": "24.19.2",
47
47
  "effect": "4.0.5",
48
48
  "typescript": "7.0.2",
package/src/eventually.ts CHANGED
@@ -1,53 +1,35 @@
1
- import { Clock, type Duration, Effect, Result, Schedule } from "effect";
1
+ import { Clock, Duration, Effect, Result } from "effect";
2
2
  import * as TestClock from "effect/testing/TestClock";
3
3
 
4
4
  export interface EventuallyOptions {
5
5
  readonly interval?: Duration.Input;
6
+ readonly timeout?: Duration.Input;
6
7
  readonly times?: number;
8
+ readonly wait?: Effect.Effect<void>;
7
9
  }
8
10
 
9
- const DEFAULT_INTERVAL = "10 millis";
10
-
11
- function isTestClock(clock: Clock.Clock): clock is TestClock.TestClock {
12
- return "adjust" in clock;
13
- }
14
-
15
- function retryOnLiveClock<A, E, R>(effect: Effect.Effect<A, E, R>, interval: Duration.Input, times: number | undefined): Effect.Effect<A, E, R> {
16
- return times === undefined
17
- ? Effect.retry(effect, Schedule.spaced(interval))
18
- : Effect.retry(effect, {
19
- schedule: Schedule.spaced(interval),
20
- times,
21
- });
22
- }
23
-
24
- const retryOnTestClock = Effect.fnUntraced(function* <A, E, R>(
11
+ export const eventually = Effect.fnUntraced(function* <A, E, R>(
25
12
  effect: Effect.Effect<A, E, R>,
26
- interval: Duration.Input,
27
- times: number | undefined,
13
+ options?: EventuallyOptions,
28
14
  ): Effect.fn.Return<A, E, R> {
29
- let retriesLeft = times;
15
+ const clock = yield* Clock.Clock;
16
+ const started = yield* Clock.currentTimeMillis;
17
+ const timeout = options?.timeout === undefined ? undefined : Duration.toMillis(Duration.fromInputUnsafe(options.timeout));
18
+ const interval = options?.interval ?? "10 millis";
19
+ const wait = options?.wait ?? ("adjust" in clock ? TestClock.adjust(interval) : Effect.sleep(interval));
20
+ let retriesLeft = options?.times;
30
21
  for (;;) {
31
22
  const result = yield* Effect.result(effect);
32
23
  if (Result.isSuccess(result)) {
33
24
  return result.success;
34
25
  }
35
- if (retriesLeft === 0) {
26
+ const elapsed = (yield* Clock.currentTimeMillis) - started;
27
+ if (retriesLeft === 0 || (timeout !== undefined && elapsed >= timeout)) {
36
28
  return yield* Effect.fail(result.failure);
37
29
  }
38
30
  if (retriesLeft !== undefined) {
39
31
  retriesLeft -= 1;
40
32
  }
41
- yield* TestClock.adjust(interval);
33
+ yield* wait;
42
34
  }
43
35
  });
44
-
45
- export const eventually = Effect.fnUntraced(function* <A, E, R>(
46
- effect: Effect.Effect<A, E, R>,
47
- options?: EventuallyOptions,
48
- ): Effect.fn.Return<A, E, R> {
49
- const interval = options?.interval ?? DEFAULT_INTERVAL;
50
- const clock = yield* Clock.Clock;
51
- const retry = isTestClock(clock) ? retryOnTestClock : retryOnLiveClock;
52
- return yield* retry(effect, interval, options?.times);
53
- });