-
Notifications
You must be signed in to change notification settings - Fork 146
Expand file tree
/
Copy pathexecution-stack.ts
More file actions
124 lines (109 loc) · 5.43 KB
/
Copy pathexecution-stack.ts
File metadata and controls
124 lines (109 loc) · 5.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// ---------------------------------------------------------------------------
// Shared execution stack — turn a (user, org) into a runnable executor + engine.
//
// Cloud and self-host both had an identical `makeExecutionStack`:
// createScopedExecutor -> createExecutionEngine({ executor, codeExecutor }) ->
// { executor, engine }
// differing only in (a) the code substrate (cloud's Cloudflare dynamic-worker vs
// self-host's in-process QuickJS) and (b) cloud's usage-metering decorator
// (an app-only billing overlay), absent on self-host.
//
// This factory owns the common body. The two differences are injected:
// - `CodeExecutorProvider` — the `codeExecutor` value. Cloud's Layer wraps
// `makeDynamicWorkerExecutor({ loader: env.LOADER })`; self-host's wraps
// `makeQuickJsExecutor()`.
// - `EngineDecorator` — `decorate(engine) => engine`. Cloud's app layer applies
// a usage-metering overlay; the default Layer is a no-op (self-host, local,
// tests, and cloud's non-metering MCP session path).
//
// The per-(user, org) executor itself comes from `makeScopedExecutor` (sdk),
// which reads the DB handle / plugins / host config from its own seams. This
// lives in `@executor-js/api` because it is the only package that depends on
// both `@executor-js/sdk` (for `makeScopedExecutor`) and `@executor-js/execution`
// (for `createExecutionEngine`).
// ---------------------------------------------------------------------------
import { Context, Effect, Layer } from "effect";
import type * as Cause from "effect/Cause";
import { composeExecutionObservers } from "@executor-js/sdk";
import type { AnyPlugin, Executor, StorageFailure } from "@executor-js/sdk";
import {
createExecutionEngine,
type ExecutionEngine,
type ExecutionEngineConfig,
} from "@executor-js/execution";
import { DbProvider } from "./executor-fuma-db";
import { HostConfig, PluginsProvider, makeScopedExecutor } from "./scoped-executor";
// ---------------------------------------------------------------------------
// CodeExecutorProvider seam — the host's code-execution substrate. Typed to the
// widened `Cause.YieldableError` channel (matching `ExecutionEngineService`) so
// a runtime-specific tagged error (DynamicWorkerExecutionError, QuickJS errors)
// assigns structurally.
// ---------------------------------------------------------------------------
export type CodeExecutor = ExecutionEngineConfig<Cause.YieldableError>["codeExecutor"];
export class CodeExecutorProvider extends Context.Service<CodeExecutorProvider, CodeExecutor>()(
"@executor-js/api/CodeExecutorProvider",
) {}
// ---------------------------------------------------------------------------
// EngineDecorator seam — wrap the freshly built engine (e.g. with usage
// metering). `decorate` receives the same `(accountId, organizationId,
// organizationName)` identity the stack was built for, so a host can bind the
// decorator to the org (cloud's per-org usage metering needs the org id). The
// default Layer is a no-op so hosts that do not decorate (self-host, local,
// tests) get an identity transform for free.
// ---------------------------------------------------------------------------
export interface EngineStackIdentity {
readonly accountId: string;
readonly organizationId: string;
readonly organizationName: string;
}
export interface EngineDecoratorShape {
readonly decorate: <E extends Cause.YieldableError>(
engine: ExecutionEngine<E>,
identity: EngineStackIdentity,
) => ExecutionEngine<E>;
}
export class EngineDecorator extends Context.Service<EngineDecorator, EngineDecoratorShape>()(
"@executor-js/api/EngineDecorator",
) {}
/** No-op decorator: the engine passes through unchanged. */
export const EngineDecoratorNoop: Layer.Layer<EngineDecorator> = Layer.succeed(EngineDecorator)({
decorate: (engine) => engine,
});
// ---------------------------------------------------------------------------
// makeExecutionStack — shared (user, org) -> { executor, engine }.
//
// Reads `makeScopedExecutor` (sdk), the code substrate from
// `CodeExecutorProvider`, and the engine wrap from `EngineDecorator`. The
// returned engine error channel is widened to `Cause.YieldableError`, matching
// `ExecutionEngineService` and the runtime-specific code executors.
// ---------------------------------------------------------------------------
export const makeExecutionStack = <
const TPlugins extends readonly AnyPlugin[] = readonly AnyPlugin[],
>(
accountId: string,
organizationId: string,
organizationName: string,
): Effect.Effect<
{ readonly executor: Executor<TPlugins>; readonly engine: ExecutionEngine<Cause.YieldableError> },
StorageFailure,
DbProvider | PluginsProvider | HostConfig | CodeExecutorProvider | EngineDecorator
> =>
Effect.gen(function* () {
const executor = yield* makeScopedExecutor<TPlugins>(
accountId,
organizationId,
organizationName,
);
const codeExecutor = yield* CodeExecutorProvider;
const { decorate } = yield* EngineDecorator;
const { plugins } = yield* PluginsProvider;
// PluginsProvider erases the tuple to AnyPlugin[]; recover the caller's
// TPlugins phantom so the extensions arg (the executor) lines up.
const observer = composeExecutionObservers(plugins() as TPlugins, executor);
const engine = decorate(createExecutionEngine({ executor, codeExecutor, observer }), {
accountId,
organizationId,
organizationName,
});
return { executor, engine };
});