Skip to content

Try out v4.1 alpha#1096

Draft
cowboyd wants to merge 6 commits into
v4from
v4-1-alpha
Draft

Try out v4.1 alpha#1096
cowboyd wants to merge 6 commits into
v4from
v4-1-alpha

Conversation

@cowboyd

@cowboyd cowboyd commented Feb 3, 2026

Copy link
Copy Markdown
Member

Motivation

We've been accumulating features for Effection 4.1.0 that will feature both middleware and the inspector. This branch will be a clearing house for features that can be released to npm.

Approach

This creates an "experimental" export where we can release unstable apis and sets up NPM publishing to send preview releases to the next disc-tag.

Alpha PRS can be made onto this branch. Then, when we are satisfied, we can revive those PRs against the main v4 branch.

@pkg-pr-new

pkg-pr-new Bot commented Feb 3, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/effection@1096

commit: 0121973

@cowboyd cowboyd changed the title Add "experimental" entry point for new apis and publish to "next" Try out v4.1 alpha Feb 5, 2026
@codspeed-hq

codspeed-hq Bot commented Jun 4, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 6 untouched benchmarks


Comparing v4-1-alpha (0121973) with v4 (846b97e)

Open in CodSpeed

cowboyd added 4 commits June 5, 2026 22:31
One of the most powerful patterns that we've uncovered in the past
couple of years of writing Effection code in production is the ability
to contextualize an API so that they can be decorated in order to
alter its behavior at any point. In other circles, this capability is
known as "Algebraic Effects" and "Contextual Effects".

With them, we can build all manner of constructs with a single primitive that
would otherwise require many unique mechanisms. These include things
like:

- dependency injection
- mocking inside tests with test doubles
- adding instrumentation such as OTEL spans and metrics colleciton to
- existing interfaces
- wrapping stuff in database transactions

This functionality was available as an external
extension (https://frontside.com/effection/x/context-api), but the
pattern has proven so powerful that we're bringing it directly into
Effection core. Among other things, this will allow us to provide the
type of orthogonal observability that we need to build the Effection
inspector without having to change the library itself in order to
accomodate it.

This change brings the context API functionality directly into
Effection. To create an API, call `createApi()` with the "core"
functionality, where the "core" is how it will behave without any
modification.

```ts
// logging.ts
export const Logging = createApi("Logging", {
  *log(...values: unknown[]) {
    console.log(...values);
  }
})

// export member operations so they can be use standalone
export const { log } = Logging.operations;
```

```ts
import { log } from "./logging.ts"

export function* example() {
 // do stuff
 yield* log("just did stuff");
}
```

```ts
// Override it contextually only inside this scope
yield* logging.around({
  *log(args, next) {
    yield* next(...args.map(v => `[CUSTOM] ${v}`));
  }
});
```

Apis can be enhanced directly from a `Scope` as well:

```ts
import { Logging } from "./logging.ts";

function enhanceLogging(scope: Scope) {
  scope.around(Logging, {
    *log(args, next) {
      yield* next(...args.map(v => `[CUSTOM] ${v}`));
    }
  });
}

```

As an example, and as the api necessary to implement the inspector,
this provides a Scope api which provides instrumentation for core
Effection operations. Such as create(), destroy(), set(), and delete()
for scopes.
In order to implement the inspector, we need to be able to wrap code
around the main function that will run around the main entry point.
This lets you do one time setup and one-time teardown.
The computational complexity of reifying an api handle was being
deferred until the moment the api was being called, and then every
invocation paid the cost of calculating the _entire_ middleware stack
from the root all they way back to the decorate scope. The handle was
cached for subsequent invocations, but any new `around()` invalidated
that scope and every descendant, which meant that every single one
had to walk the entire scope chain on the next invocation.

The handle is now materialized at decoration time. Each api context
carries `local` (this scope's contributions), `total` (the inherited
stack), and `handle` (the assembled api). `around()` appends the new
decorator onto `local` and then walks the subtree in `install()`,
rebuilding each descendant's handle in place. Critically, the
inherited middleware stack is cached at every scope that has
middleware, so any subsequent calls to around() can begin from that
total, and do not need to walk back up the entire scope chain.

In all cases, because the api handle is computed eagerly, `invoke()`
is single context lookup and dispatch.
We were installing an api decorator as the method for implementing the
destroy method on scope, but this required recomputing the middleware
stack for every scope creation which is totally unecessary work.

This just puts it on a prototype method of ScopeInternal. That way,
there is one version of the function, and the default api handle, just
invokes it straight away.
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.

1 participant