pure-effect 0.14.0 → 0.16.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
@@ -2,6 +2,54 @@
2
2
 
3
3
  All notable changes to pure-effect are recorded here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and from 1.0.0 the project follows [Semantic Versioning](https://semver.org/). Before 1.0.0 a minor version could change behaviour; each such change is marked below.
4
4
 
5
+ ## [0.16.0] - 2026-10-01
6
+
7
+ ### Added
8
+
9
+ - **`commandName` gives a Command's name as a trace records it:** `meta.name`, else the function's name, else `'anonymous'`. A test that walks a flow checks each step with it. Reading `step.cmd.name`, as the README's tests did, could not check a Command named through `meta.name` whose function is an inline arrow, since such a function has no name. Anything but a Command throws an `EffectTypeError`.
10
+ - **`timeTravel` warns about anonymous steps.** A trace whose steps include any named `'anonymous'`, usually inline arrow Commands, gets a warning in the narration, since a replay tells steps apart by name and two such steps that trade places after a refactor replay without complaint.
11
+ - **`recordEffect`'s trace keeps the input and context types.** `TraceLog` takes the input and context as type parameters, and `recordEffect` and `recorder().toTrace` fill them in, so `replayEffect(flow(trace.initialInput), trace)` needs no cast straight after recording. A trace read back from storage is typed as before.
12
+ - **`EffectValue`, `EffectError` and `EffectContext` read the value, error and context of an Effect**, as in `EffectError<ReturnType<typeof checkoutFlow>>`. `Parallel`'s declaration is built on them, alongside `ParallelBranches`, `ParallelValues` and `ParallelContext`.
13
+
14
+ ### Changed
15
+
16
+ - **`replayEffect` refuses `onMissing: 'execute'` on a trace that dropped entries.** A trace whose `dropped` is above 0 now rejects with a `ReplayError` before anything runs; replay it without the option to stop at the first step it lacks, or record the flow with a higher `maxEntries`. The message for a missing step now says whether the cap may have dropped it, and `'execute'` is documented for Commands that reach test doubles or only read.
17
+ - **An error union survives a pipeline without return annotations.** A step that cannot return a `Failure`, such as a Command with the default `next`, a Command whose `next` only succeeds, or a pure step that only returns `Success`, declared its error as `unknown`, which absorbed every other step's. So the Quick Start written in TypeScript typed `result.error` as `unknown`, and so did every pipeline that retried a Command in the recommended shape. Such a step now declares `never`, and the union holds the `Failure`s the steps can return: `Retry(Command(fn))` declares `RetryExhaustedError` alone, and a `Retry` whose fallback cannot fail declares nothing of its own. A `Command` call that gives some type arguments, such as `Command<User | null>(fetchJson, next)`, keeps the types it had, since TypeScript infers none of the others in such a call. A `Command` without a `next` takes only its result's type as a type argument. `Ask` and `Retry` given only some type arguments, as in `Ask<Product>(...)`, now refuse a step that can fail; give every type argument or none. A variable that holds the results of two different flows, which both used to type as `FailureState<unknown>`, may need a wider annotation. A Command whose function throws still ends the run with a `Failure` the union does not name, so where a flow's declared error is now only strings, or nothing, `result.error instanceof Error` stops compiling: copy the error into a variable typed `unknown` first, as in `const error: unknown = result.error`.
18
+ - **A `Parallel` declares its branches' errors.** Its branches were typed with one shared error that nothing inferred, so every `Parallel` declared `unknown`, and so did every pipeline holding one, including every loop written as the README writes it. A `Parallel` now declares each branch's error together with what `next` can return, and each outcome a settled `Parallel` hands `next` carries its own branch's error. A settled `Parallel` declares only `next`'s error, since a branch's failure reaches `next` as an outcome rather than escaping. `ParallelOutcomes` takes the branches instead of the values and one error, so `ParallelOutcomes<[User, Order], MyError>` is now written `ParallelOutcomes<[Effect<User, MyError>, Effect<Order, MyError>]>`, and `ParallelState` takes a sixth, optional parameter for the branches' error. `Parallel`'s type parameter is now the branches rather than their values, so an explicit `Parallel<[number]>(...)` stops compiling; leave it to be inferred.
19
+ - **The type declarations need TypeScript 5.1 or later.** The new `Parallel` declaration relies on 5.1, the first version that treats a mapped type over the branches as an array. TypeScript 5.1 is now checked on every change.
20
+ - **`Failure` keeps the literal type of its error.** Its type parameter is `const`, so `Failure('invalid_email')` is typed `'invalid_email'` rather than `string`, and an inferred error union stays exact with no `as const`. An object error is typed as readonly with literal properties, which is still assignable to a mutable object type, and an array error as a readonly tuple, which a mutable array type does not accept. A value typed `string` stays `string`.
21
+ - **A `next` that can only fail no longer widens a step's value.** `Command` with an explicit `next` declared its function's result as its value when `next` could only fail, and `Ask`, `Parallel` and `effectPipe` declared `unknown`, so a step that either succeeds or compensates and then fails, as in `ok ? Success(order) : Command(refund, () => Failure('declined'))`, did not compile in a pipeline, and a compensation written as a sub-pipeline failed the same way. Such a `next` now declares `never`, which adds nothing to the step's value.
22
+ - **`Parallel` refuses a third argument after its options.** Nothing reads a third argument once the options come second, so `Parallel(effects, { limit: 1 }, next)` ran without its `next` and handed the branch values on as they were, which only TypeScript refused. It now throws an `EffectTypeError` that gives the order, and so does a second options object passed there. `undefined` or `null` in that place still counts as absent.
23
+ - **An error for a `next` that returned something other than an Effect names whose `next` it was.** It said only that the flow or a continuation had returned it, so finding the culprit meant reading every `next`. It now reads, for example, `The next of Command 'cmdChargeCard' returned the number 2`, naming a Command by its name and an `Ask` or `Parallel` by its type.
24
+ - **Node 22 or later.** `engines` says `>=22`, where it said `>=18`: Node 18 and 20 have reached their end of life, and the tests run on 22, 24 and 26. Nothing in the library needs a newer Node, so code on an older one keeps running, but it is no longer tested there.
25
+
26
+ ### Fixed
27
+
28
+ - **A `Parallel` requires the context its branches read.** The same shared type made its context `unknown`, so `runEffect` accepted a flow with no context, or the wrong one, whenever only a `Parallel`'s branches read it, and `Ask` then got an empty object: a query filtered on `ctx.tenant` received `undefined`. A `Parallel` now needs every context its branches and its `next` read, as a pipeline needs its steps', whether the branches are listed, built with `map`, settled, or inside a pipeline.
29
+ - **A function that returns `Failure`s of different shapes compiles without an annotation.** A pipeline step, a `next` given to `Command`, `Ask` or `Parallel`, or an `onExhausted` fallback that could fail with, say, `Failure('cart_empty')` and `Failure({ code: 'invalid_quantity', sku })` did not compile unless its return type was written out. Such a function is now typed from its whole return, so its errors join into a union, each kept as written, and its values join the same way. Explicit type arguments type a call as before. A step that returns something other than an Effect is now reported as not assignable to `AnyEffect`, the type every step returns.
30
+ - **A stored trace imported as a JSON module type-checks.** `TraceEntry.threw` was declared `true`, and a JSON import widens `true` to `boolean`, so `replayEffect(flow(incident.initialInput), incident)` with `incident` imported from a trace file was refused whenever the trace held a failed step, which is how the README turns an incident into a regression test. It is declared `boolean` now; the recorder still writes only `true`.
31
+ - **A typed context accepts `flowName`.** `runEffect`, `recordEffect`, `replayEffect` and `timeTravel` refused `{ tenant, flowName }` for a flow whose context type does not declare `flowName`, though that key is how a run is named in traces and spans, so the README's `recordEffect` example did not compile against a typed flow. The context they take is now `RunContext`, the flow's context with an optional `flowName`.
32
+ - **A test walking a flow through a `Retry` can hand its `next` what the retried Command returned.** A `Retry` that starts a pipeline typed its `next` as taking the pipeline's final value, although it receives the retried Command's, so such a walk did not compile without a cast. `RetryState` takes a fourth, optional parameter for the value `next` receives, which defaults to the first.
33
+
34
+ ## [0.15.0] - 2026-09-27
35
+
36
+ ### Added
37
+
38
+ - **`timeTravel` and the reference recording wiring warn about a trace with no input.** A recorder installed as a hook reads the input off the flow, where only `effectPipe` puts it, so a flow whose outermost node is a bare `Command`, `Ask`, `Retry` or `Parallel` recorded none, and `timeTravel` rebuilt it from `undefined` without saying so. `timeTravel` now logs a warning when a trace holds no `initialInput`, and `recordingHooks` reports the first kept trace of such a flow through a new `onWarning` option, once per flow, which defaults to `console.warn`.
39
+
40
+ ### Changed
41
+
42
+ - **The building blocks check their arguments when the flow is built.** `Command`, `Ask`, `Retry`, `Parallel` and `effectPipe` throw an `EffectTypeError` naming the mistake for an argument they cannot use, before any of the flow's I/O runs. `Command(db.findUser(email))` made the query while the flow was being built and then counted as an I/O fault, so a `Retry` around it ran it again and a replay repeated it; `Command(fn, { name })` ran the function, charging the card, before the run rejected with `effect.next is not a function`; and an `effectPipe` step that was `undefined` failed only when the flow reached it. A flow that passed such an argument now throws where it is built. A `null` next counts as omitted in `Command`, as it does in `Parallel`.
43
+ - **An `onStep` hook that calls `op` and returns nothing makes the run reject.** A hook that awaited `op()` and forgot to return its result handed the flow `undefined` in place of the Command's value, so a metrics hook written that way turned an email that was taken into one that was free and saved a duplicate account, while a recorder installed inside it kept the real answer and the trace contradicted production. The run now rejects with a `TypeError` naming the Command when a hook returns `undefined` after the Command's function returned a value. A hook that returns a copy of the result, one that never calls `op`, as replay does, and one wrapping a Command that itself returned `undefined` are unaffected.
44
+ - **`Retry` treats an option set to `undefined` as unset, and refuses a wait it cannot keep.** An undefined option overrode its default, so `attempts: config.attempts` with the key absent threw, and `backoff: undefined` made every wait after the first `NaN` milliseconds, which is no wait: a flapping dependency was called back to back. An undefined option now keeps its default, and a `delay` or `backoff` that is not a finite number of 0 or more throws a `TypeError` when the `Retry` runs, as `attempts` already did. A `Retry` given `delay: -1`, `NaN`, or a string such as `'250'` now throws instead of waiting no time.
45
+ - **A replay that refuses to run an unrecorded step no longer recommends `onMissing: 'execute'` without a warning.** The message said to pass it, but a step with no entry may be one production never ran: a Command that an `onBeforeCommand` hook vetoed leaves no entry, so following that advice while replaying a rate-limited run performed the I/O the limiter had refused and replayed as `Success`. The message now says so, and names the option for a trace that was cut short, as by `maxEntries`.
46
+
47
+ ### Fixed
48
+
49
+ - **A Command whose error's `cause` chain loops back is recorded.** Serializing the error followed the cause forever, and the stack overflow was dropped with the recorder's other failures, so the step vanished from the trace, `dropped` still said 0, and the replay reported a missing entry instead of the failure. A chain that comes back to an error it passed through is now cut there, and the step is recorded like any other.
50
+ - **An error whose `name` or `cause` was assigned replays as the same error.** `e.name = 'TimeoutError'` and `Object.assign(error, { name })` make `name` an own enumerable property, and revival always made it non-enumerable, so the README's check `assert.deepEqual(replayed, result)` failed on a plain `Error`. `e.cause = inner`, the idiom from before the `cause` option, lost the cause entirely: it was copied as the raw `Error`, which JSON stores as `{}`. Both now come back as the Command threw them.
51
+ - **`Parallel` keeps its options when `next` is passed as `undefined` or `null`.** The options were read from the second argument whenever it was not a function, so a call that forwarded an absent `next`, as in `Parallel(branches, config.summarize, { limit: 5, settled: true })`, lost them silently: every branch started at once, and one failing branch cancelled the rest of the batch instead of reaching `next` as an outcome. A skipped `next` now leaves the options third, where the documented signature `Parallel(effects, next?, options?)` puts them.
52
+
5
53
  ## [0.14.0] - 2026-09-24
6
54
 
7
55
  ### Added
@@ -175,6 +223,8 @@ All notable changes to pure-effect are recorded here. The format follows [Keep a
175
223
 
176
224
  - Initial release: `Success`, `Failure`, `Command`, `effectPipe`, and `runEffect`.
177
225
 
226
+ [0.16.0]: https://github.com/aycangulez/pure-effect/compare/v0.15.0...v0.16.0
227
+ [0.15.0]: https://github.com/aycangulez/pure-effect/compare/v0.14.0...v0.15.0
178
228
  [0.14.0]: https://github.com/aycangulez/pure-effect/compare/v0.13.0...v0.14.0
179
229
  [0.13.0]: https://github.com/aycangulez/pure-effect/compare/v0.12.0...v0.13.0
180
230
  [0.12.0]: https://github.com/aycangulez/pure-effect/compare/v0.11.0...v0.12.0