Skip to content

feat(fetch): add Effection-native fetch package#150

Open
taras wants to merge 5 commits intomainfrom
feat/fetch-package
Open

feat(fetch): add Effection-native fetch package#150
taras wants to merge 5 commits intomainfrom
feat/fetch-package

Conversation

@taras
Copy link
Member

@taras taras commented Feb 10, 2026

Motivation

Make it easier for people to use fetch with Effection by providing an Effection-native HTTP client with:

  • Automatic cancellation tied to Effection scope
  • Streaming response body support via Stream<Uint8Array, void>
  • Fluent API for concise one-liners: yield* fetch(...).json()

Approach

  • Add new @effectionx/fetch package with fetch() returning a chainable FetchOperation
  • Support both fluent (yield* fetch(...).json()) and traditional (yield* (yield* fetch(...)).json()) APIs
  • Use useAbortSignal() for scope-aware cancellation, merged with user-provided signals via AbortSignal.any()
  • Use Effection's stream() to convert ReadableStream to Stream<Uint8Array, void> for body streaming
  • Add expect() method that throws HttpError on non-2xx responses
  • Add json(parse) overload for runtime validation during parsing
  • Wire package into monorepo (pnpm-workspace.yaml, tsconfig.json)
  • Add migration note to fx/README.md pointing to this package for streaming use cases

Summary by CodeRabbit

  • New Features

    • Added an Effection-native HTTP client with a fluent API, streaming response bodies, JSON parsing, HttpError handling, response consumption/cloning semantics, and cancellation support.
  • Documentation

    • Added full module docs: installation, usage and examples (fluent API, streaming, concurrent requests), API reference, JSON validation, error handling, and migration guidance.
  • Tests

    • Added comprehensive tests covering JSON parsing, streaming, consumption semantics, error propagation, and abort behavior.
  • Chores

    • Added package manifest, build config, module barrel, and workspace/project references.

@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new @effectionx/fetch package implementing an Effection-native, fluent fetch Operation with streaming body support, HttpError/expect handling, single-use body consumption guards, tests, README docs, and workspace/TypeScript integration.

Changes

Cohort / File(s) Summary
Fetch Library Core
fetch/fetch.ts, fetch/mod.ts
New Effection-based fetch implementation and barrel export: fetch() Operation, FetchResponse facade, HttpError, single-use body consumption guards, Stream-backed body() access, and merged AbortSignal handling.
Tests
fetch/fetch.test.ts
New test suite exercising response-style and fluent APIs: JSON/text parsing (including parser-based json()), streaming body consumption, expect()/HttpError propagation, single-use body guards, and local HTTP endpoints for coverage.
Documentation
fetch/README.md, fx/README.md
Adds detailed README for @effectionx/fetch with installation, API usage, streaming/cancellation examples; updates fx/README.md with a migration note referencing the new client.
Package & Build config
fetch/package.json, fetch/tsconfig.json
New package manifest for @effectionx/fetch (v0.1.0) with exports/entry points and peer dependency on effection; TypeScript project config (outDir dist, ES2022/DOM libs).
Workspace & Root TS config
pnpm-workspace.yaml, tsconfig.json
Adds fetch to pnpm workspace packages and adds a project reference for the fetch project in the root tsconfig.

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant FetchOp as Fetch\nOperation
    participant Scope as Effection\nScope
    participant Native as Native\nFetch API
    participant Server as HTTP\nServer

    App->>FetchOp: yield* fetch(url, init?)
    FetchOp->>Scope: obtain scope AbortSignal
    FetchOp->>FetchOp: merge init.signal with scope.signal
    FetchOp->>Native: native fetch(url, { signal: merged })
    Native->>Server: HTTP request
    Server-->>Native: HTTP response
    Native-->>FetchOp: Response
    FetchOp-->>App: returns FetchResponse facade

    App->>FetchOp: yield* response.json()/text()/body()/expect()
    FetchOp->>FetchOp: check bodyUsed / consumed guard
    FetchOp->>Native: read/stream body with AbortSignal
    Native-->>FetchOp: chunks / parsed value
    FetchOp-->>App: Operation result or thrown HttpError
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble bytes and follow streams,
Guards in place for single dreams,
Abort the hop, then stitch the thread,
Fluent hops where data's led,
A tiny rabbit fetches code with beams.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding a new Effection-native fetch package as a feature.
Description check ✅ Passed The description includes both required sections (Motivation and Approach) with detailed, complete information about the feature and implementation strategy.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/fetch-package

No actionable comments were generated in the recent review. 🎉


Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 10, 2026

Open in StackBlitz

npm i https://pkg.pr.new/thefrontside/effectionx/@effectionx/fetch@150
npm i https://pkg.pr.new/thefrontside/effectionx/@effectionx/fx@150

commit: 9af0ece

@taras taras requested a review from cowboyd February 10, 2026 19:08
- fetch().json(), fetch().text(), fetch().body() for single yield*
- fetch().expect().json() for validation before consumption
- Rename ensureOk() to expect()
- Remove clone() method
- Update tests and README with fluent API examples
Copy link
Member

@cowboyd cowboyd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a resource?

Also, we should have documentation on the individual symbols

});
});

function* captureError(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

box()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - now using box() with Effection's Ok/Err/Result types for error testing.

fetch/fetch.ts Outdated
Comment on lines 58 to 60
let signal = init?.signal
? AbortSignal.any([init.signal, scopeSignal])
: scopeSignal;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we even allow an ignore any signal passed in? Even if we don't, we should decide that before we create an unnecessary abort signal.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just removed it and removed signal from options type.

fetch/fetch.ts Outdated
Comment on lines 67 to 74
let consumed = false;

let guardBody = () => {
if (consumed || response.bodyUsed) {
throw new Error("Body has already been consumed");
}
consumed = true;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we tracking consumption state ourselves and then adding an error?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed - now letting the native Response handle body consumption tracking. It throws its own error if you try to consume twice. Also removed the corresponding test.

fetch/fetch.ts Outdated
Comment on lines 135 to 145
*ensureOk(): Operation<FetchResponse> {
if (!response.ok) {
throw new HttpError(
response.status,
response.statusText,
response.url,
self,
);
}
return self;
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep the api as 1:1 as we can. I could see adding this as a standalone function that would accept a response, but even so, it does not need to be an operation

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept as Operation per user preference. Renamed from ensureOk() to expect() for fluent API brevity: fetch().expect().json(). The Operation wrapper is minimal - it just checks response.ok and throws HttpError if false.

…lation

- Add FetchInit type (Omit<RequestInit, 'signal'> & { signal?: never })
- Remove signal merging logic, always use Effection scope signal
- Remove signal-related test
- Update README to clarify cancellation is via structured concurrency
- Replace captureError() with box() for error testing
- Remove 'prevents consuming body twice' test (let native Response handle it)
- Add comprehensive JSDoc documentation to all exports
- Remove manual consumed tracking and guardBody()
- Remove signal from options (use Effection scope signal only)
@taras
Copy link
Member Author

taras commented Feb 10, 2026

Addressed all feedback in the latest commit:

  1. box() - Now using box() for error testing instead of captureError()
  2. signal - Removed from options type, using only Effection scope signal
  3. guardBody - Removed manual body consumption tracking, letting native Response handle it
  4. ensureOk -> expect() - Kept as Operation per user preference; renamed to expect() for fluent API brevity
  5. JSDoc - Added documentation to all exported symbols

Also removed the "prevents consuming body twice" test since the native Response handles that now.

@taras
Copy link
Member Author

taras commented Feb 10, 2026

Regarding "Should this be a resource?":

fetch() returns an Operation, not a Resource. This is intentional:

  • Operations are appropriate because the HTTP request is a one-shot action with a result, not a long-lived entity that needs lifecycle management
  • The Response body streaming uses stream() which is also an Operation (stateless iterator pattern per the codebase policy)
  • Cancellation is already handled via the Effection scope signal - when the scope exits, the request aborts

A Resource would make sense if we were managing something like a connection pool or persistent WebSocket, but for individual fetch requests, Operation is the right abstraction.

@taras taras requested a review from cowboyd February 10, 2026 20:26
@cowboyd
Copy link
Member

cowboyd commented Feb 10, 2026

Regarding "Should this be a resource?":

fetch() returns an Operation, not a Resource. This is intentional:

  • Operations are appropriate because the HTTP request is a one-shot action with a result, not a long-lived entity that needs lifecycle management
  • The Response body streaming uses stream() which is also an Operation (stateless iterator pattern per the codebase policy)
  • Cancellation is already handled via the Effection scope signal - when the scope exits, the request aborts

A Resource would make sense if we were managing something like a connection pool or persistent WebSocket, but for individual fetch requests, Operation is the right abstraction.

I'm not sure what this means. A resource is an operation.

@taras
Copy link
Member Author

taras commented Feb 10, 2026

Sorry about that. It was AI slop slop I didn't see that it left this comment. It seems like it should be a resource, but there is no obvious lifecycle that's not covered by the abort signal. It might be more straightforward once we have the inspector so we can actually see what it looks like there. I'm planning to change this to be contextapi so I don't know if any of those factors impact this decision.

@cowboyd
Copy link
Member

cowboyd commented Feb 10, 2026

It seems like it should be a resource, but there is no obvious lifecycle that's not covered by the abort signal.

Yeah, you're right. really the abort signal is the resource. The only reason really that it should be its own resource is to give it a name in order to contextualize the abort signal in the inspector:

+ MyTask
| 
- +  Fetch 
   |
   - + AbortSignal

I'm planning to change this to be contextapi so I don't know if any of those factors impact this decision.

This is a great idea.

Also, thinking that this should make its way into Effection core at some point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants