|
1 | | -import { Effect } from "effect"; |
| 1 | +import { Effect, Layer, ManagedRuntime, Context } from "effect"; |
2 | 2 | import { SqliteClient } from "@effect/sql-sqlite-bun"; |
3 | 3 | import * as SqlClient from "@effect/sql/SqlClient"; |
4 | 4 | import { NodeFileSystem } from "@effect/platform-node"; |
@@ -45,81 +45,103 @@ const resolveDbPath = (): string => { |
45 | 45 | }; |
46 | 46 |
|
47 | 47 | // --------------------------------------------------------------------------- |
48 | | -// Create a local executor — returns the full typed executor |
| 48 | +// Local plugins — defined once, used for both the layer and type inference |
49 | 49 | // --------------------------------------------------------------------------- |
50 | 50 |
|
51 | | -export const createLocalExecutor = async () => { |
| 51 | +const createLocalPlugins = ( |
| 52 | + scopedKv: ReturnType<typeof makeScopedKv>, |
| 53 | + configPath: string, |
| 54 | + fsLayer: typeof NodeFileSystem.layer, |
| 55 | +) => |
| 56 | + [ |
| 57 | + openApiPlugin({ |
| 58 | + operationStore: withConfigFile.openapi( |
| 59 | + makeKvOperationStore(scopedKv, "openapi"), |
| 60 | + configPath, |
| 61 | + fsLayer, |
| 62 | + ), |
| 63 | + }), |
| 64 | + mcpPlugin({ |
| 65 | + bindingStore: withConfigFile.mcp( |
| 66 | + makeKvBindingStore(scopedKv, "mcp"), |
| 67 | + configPath, |
| 68 | + fsLayer, |
| 69 | + ), |
| 70 | + }), |
| 71 | + googleDiscoveryPlugin({ |
| 72 | + bindingStore: makeKvGoogleDiscoveryBindingStore( |
| 73 | + scopedKv, |
| 74 | + "google-discovery", |
| 75 | + ), |
| 76 | + }), |
| 77 | + graphqlPlugin({ |
| 78 | + operationStore: withConfigFile.graphql( |
| 79 | + makeKvGraphqlOperationStore(scopedKv, "graphql"), |
| 80 | + configPath, |
| 81 | + fsLayer, |
| 82 | + ), |
| 83 | + }), |
| 84 | + keychainPlugin(), |
| 85 | + fileSecretsPlugin(), |
| 86 | + onepasswordPlugin({ |
| 87 | + kv: scopeKv(scopedKv, "onepassword"), |
| 88 | + }), |
| 89 | + ] as const; |
| 90 | + |
| 91 | +// Full typed executor — inferred from plugin list |
| 92 | +type LocalPlugins = ReturnType<typeof createLocalPlugins>; |
| 93 | + |
| 94 | +// Private tag preserving the full plugin type |
| 95 | +class LocalExecutorTag extends Context.Tag("@executor/local/Executor")< |
| 96 | + LocalExecutorTag, |
| 97 | + Effect.Effect.Success<ReturnType<typeof createExecutor<LocalPlugins>>> |
| 98 | +>() {} |
| 99 | + |
| 100 | +export type LocalExecutor = Context.Tag.Service<typeof LocalExecutorTag>; |
| 101 | + |
| 102 | +// --------------------------------------------------------------------------- |
| 103 | +// Layer — SQLite-backed, keeps connection alive via ManagedRuntime |
| 104 | +// --------------------------------------------------------------------------- |
| 105 | + |
| 106 | +const createLocalExecutorLayer = () => { |
52 | 107 | const dbPath = resolveDbPath(); |
53 | 108 |
|
54 | | - const program = Effect.gen(function* () { |
55 | | - const sql = yield* SqlClient.SqlClient; |
56 | | - |
57 | | - yield* migrate.pipe(Effect.catchAll((e) => Effect.die(e))); |
58 | | - |
59 | | - const cwd = process.env.EXECUTOR_SCOPE_DIR || process.cwd(); |
60 | | - const kv = makeSqliteKv(sql); |
61 | | - const config = makeKvConfig(kv, { cwd }); |
62 | | - const scopedKv = makeScopedKv(kv, cwd); |
63 | | - |
64 | | - const configPath = join(cwd, "executor.jsonc"); |
65 | | - const fsLayer = NodeFileSystem.layer; |
66 | | - |
67 | | - return yield* createExecutor({ |
68 | | - ...config, |
69 | | - plugins: [ |
70 | | - openApiPlugin({ |
71 | | - operationStore: withConfigFile.openapi( |
72 | | - makeKvOperationStore(scopedKv, "openapi"), |
73 | | - configPath, |
74 | | - fsLayer, |
75 | | - ), |
76 | | - }), |
77 | | - mcpPlugin({ |
78 | | - bindingStore: withConfigFile.mcp( |
79 | | - makeKvBindingStore(scopedKv, "mcp"), |
80 | | - configPath, |
81 | | - fsLayer, |
82 | | - ), |
83 | | - }), |
84 | | - googleDiscoveryPlugin({ |
85 | | - bindingStore: makeKvGoogleDiscoveryBindingStore( |
86 | | - scopedKv, |
87 | | - "google-discovery", |
88 | | - ), |
89 | | - }), |
90 | | - graphqlPlugin({ |
91 | | - operationStore: withConfigFile.graphql( |
92 | | - makeKvGraphqlOperationStore(scopedKv, "graphql"), |
93 | | - configPath, |
94 | | - fsLayer, |
95 | | - ), |
96 | | - }), |
97 | | - keychainPlugin(), |
98 | | - fileSecretsPlugin(), |
99 | | - onepasswordPlugin({ |
100 | | - kv: scopeKv(scopedKv, "onepassword"), |
101 | | - }), |
102 | | - ] as const, |
103 | | - }); |
104 | | - }).pipe( |
105 | | - Effect.provide(SqliteClient.layer({ filename: dbPath })), |
106 | | - ); |
107 | | - |
108 | | - return Effect.runPromise(program); |
| 109 | + return Layer.effect( |
| 110 | + LocalExecutorTag, |
| 111 | + Effect.gen(function* () { |
| 112 | + const sql = yield* SqlClient.SqlClient; |
| 113 | + |
| 114 | + yield* migrate.pipe(Effect.catchAll((e) => Effect.die(e))); |
| 115 | + |
| 116 | + const cwd = process.env.EXECUTOR_SCOPE_DIR || process.cwd(); |
| 117 | + const kv = makeSqliteKv(sql); |
| 118 | + const config = makeKvConfig(kv, { cwd }); |
| 119 | + const scopedKv = makeScopedKv(kv, cwd); |
| 120 | + const configPath = join(cwd, "executor.jsonc"); |
| 121 | + const fsLayer = NodeFileSystem.layer; |
| 122 | + |
| 123 | + return yield* createExecutor({ |
| 124 | + ...config, |
| 125 | + plugins: createLocalPlugins(scopedKv, configPath, fsLayer), |
| 126 | + }); |
| 127 | + }), |
| 128 | + ).pipe(Layer.provide(SqliteClient.layer({ filename: dbPath }))); |
109 | 129 | }; |
110 | 130 |
|
111 | 131 | // --------------------------------------------------------------------------- |
112 | | -// Shared singleton for production, scoped handles for dev HMR |
| 132 | +// Handle — keeps runtime alive, returns fully typed executor |
113 | 133 | // --------------------------------------------------------------------------- |
114 | 134 |
|
115 | | -export type LocalExecutor = Awaited<ReturnType<typeof createLocalExecutor>>; |
116 | | - |
117 | 135 | export const createExecutorHandle = async () => { |
118 | | - const executor = await createLocalExecutor(); |
| 136 | + const layer = createLocalExecutorLayer(); |
| 137 | + const runtime = ManagedRuntime.make(layer); |
| 138 | + const executor = await runtime.runPromise(LocalExecutorTag); |
| 139 | + |
119 | 140 | return { |
120 | 141 | executor, |
121 | 142 | dispose: async () => { |
122 | 143 | await Effect.runPromise(executor.close()).catch(() => undefined); |
| 144 | + await runtime.dispose().catch(() => undefined); |
123 | 145 | }, |
124 | 146 | }; |
125 | 147 | }; |
|
0 commit comments