✨ Export reducer internals via experimental and label infrastructure effects#1127
Open
taras wants to merge 5 commits intothefrontside:v4-1-alphafrom
Open
✨ Export reducer internals via experimental and label infrastructure effects#1127taras wants to merge 5 commits intothefrontside:v4-1-alphafrom
taras wants to merge 5 commits intothefrontside:v4-1-alphafrom
Conversation
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.
commit: |
Member
|
This should be solved with and API/middleware. |
Member
Author
|
Can you say more? Do you expect this to already be solvable with middleware? or is something missing that would make it possible with middleware? |
Member
|
Extensions to the core should always be done with an |
Member
Author
|
@cowboyd we already have a reducer context and the reducer is an interface so that makes sense. we'll need to see what the performance implications are. |
9675d87 to
26afaed
Compare
Add api.Reducer as a middleware interception point for effect reduction, enabling external packages (e.g., @effectionx/durably) to intercept instruction processing without forking Effection. Changes: - lib/api.ts: Define ReducerApi interface and api.Reducer with createApi - lib/coroutine.ts: Route reduce calls through reducerApi.invoke() instead of directly calling ReducerContext, using routine.scope for correct middleware chain resolution - lib/scope-internal.ts: Install stock Reducer as base-layer api.Reducer middleware on the global scope - experimental.ts: Export InstructionQueue, Instruction type, and DelimiterContext for custom reducer implementations - lib/callcc.ts, lib/delimiter.ts, lib/each.ts, lib/future.ts, lib/scope-internal.ts: Add description labels to withResolvers() calls so custom reducers can distinguish infrastructure effects from user-facing effects
26afaed to
bc757a0
Compare
9fe00aa to
9dbdf93
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
External packages (like
@effectionx/durable) need to build custom reducers that intercept effect processing — for example, to record effect resolutions to a durable stream and replay them on resume. Currently, the types and contexts required to build a custom reducer (ReducerContext,Instruction,InstructionQueue,DelimiterContext) are internal to Effection and not accessible through any public entrypoint.Additionally, custom reducers need to distinguish infrastructure effects (like
useCoroutine(),useScope(), scope destruction) from user-facing effects (likesleep(),call(),action()). Infrastructure effects that usewithResolvers()currently have no description label, making them indistinguishable from user code.This PR makes durable execution (and other custom reducer patterns) completely additive to Effection — no fork required.
Approach
1. Export reducer internals via
experimental.ts(7 lines added)Re-export
Reducer,ReducerContext,InstructionQueue, and theInstructiontype fromlib/reducer.ts, plusDelimiterContextfromlib/delimiter.ts, through the existingexperimentalentry point. This follows the established pattern —experimental.tsalready exportsapifor the same reason (enabling external middleware).2. Add
exporttoInstructionandInstructionQueue(2 lines changed inlib/reducer.ts)These were module-private. Making them
exportallows the re-export inexperimental.ts.3. Label infrastructure
withResolvers()calls (5 files, 1 line each)Add description strings to
withResolvers()calls in core infrastructure:lib/callcc.ts:"await callcc"lib/delimiter.ts:"await delimiter"lib/each.ts:"await each done","await each context"lib/future.ts:"await future"lib/scope-internal.ts:"await destruction"The
descriptionparameter already exists onwithResolvers()and is already used elsewhere. These labels are purely additive — they make infrastructure effects identifiable by description, which is useful for debugging, inspection, and custom reducers.Zero behavior changes. All 121 test steps across 15 core test suites pass unchanged.