Try out v4.1 alpha#1096
Draft
cowboyd wants to merge 6 commits into
Draft
Conversation
commit: |
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.
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.
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
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
nextdisc-tag.Alpha PRS can be made onto this branch. Then, when we are satisfied, we can revive those PRs against the main
v4branch.