every-plugin 2.9.1 → 2.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/plugin.cjs +2 -2
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +22 -10
- package/dist/plugin.d.cts.map +1 -1
- package/dist/plugin.d.mts +22 -10
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +2 -2
- package/dist/plugin.mjs.map +1 -1
- package/dist/runtime/errors.cjs +2 -1
- package/dist/runtime/errors.cjs.map +1 -1
- package/dist/runtime/errors.d.cts.map +1 -1
- package/dist/runtime/errors.d.mts.map +1 -1
- package/dist/runtime/errors.mjs +2 -1
- package/dist/runtime/errors.mjs.map +1 -1
- package/dist/runtime/index.cjs +12 -4
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +1 -1
- package/dist/runtime/index.d.cts.map +1 -1
- package/dist/runtime/index.d.mts +1 -1
- package/dist/runtime/index.d.mts.map +1 -1
- package/dist/runtime/index.mjs +12 -4
- package/dist/runtime/index.mjs.map +1 -1
- package/dist/runtime/mf-config.cjs +1 -1
- package/dist/runtime/mf-config.mjs +1 -1
- package/dist/runtime/services/plugin-lifecycle.service.cjs +1 -0
- package/dist/runtime/services/plugin-lifecycle.service.cjs.map +1 -1
- package/dist/runtime/services/plugin-lifecycle.service.mjs +1 -0
- package/dist/runtime/services/plugin-lifecycle.service.mjs.map +1 -1
- package/dist/runtime/services/plugin-loader.service.cjs +2 -1
- package/dist/runtime/services/plugin-loader.service.cjs.map +1 -1
- package/dist/runtime/services/plugin-loader.service.d.cts.map +1 -1
- package/dist/runtime/services/plugin-loader.service.d.mts.map +1 -1
- package/dist/runtime/services/plugin-loader.service.mjs +3 -2
- package/dist/runtime/services/plugin-loader.service.mjs.map +1 -1
- package/dist/runtime/services/plugin.service.cjs +1 -0
- package/dist/runtime/services/plugin.service.cjs.map +1 -1
- package/dist/runtime/services/plugin.service.d.cts +1 -0
- package/dist/runtime/services/plugin.service.d.cts.map +1 -1
- package/dist/runtime/services/plugin.service.d.mts +1 -0
- package/dist/runtime/services/plugin.service.d.mts.map +1 -1
- package/dist/runtime/services/plugin.service.mjs +1 -0
- package/dist/runtime/services/plugin.service.mjs.map +1 -1
- package/package.json +1 -1
- package/skills/plugin-development/SKILL.md +53 -7
- package/skills/plugin-testing/SKILL.md +55 -0
- package/src/plugin.ts +27 -10
- package/src/runtime/errors.ts +4 -1
- package/src/runtime/index.ts +7 -3
- package/src/runtime/services/plugin-lifecycle.service.ts +5 -0
- package/src/runtime/services/plugin-loader.service.ts +22 -2
- package/src/runtime/services/plugin.service.ts +1 -0
|
@@ -109,7 +109,7 @@ export default createPlugin({
|
|
|
109
109
|
}),
|
|
110
110
|
contract,
|
|
111
111
|
|
|
112
|
-
initialize: (config) =>
|
|
112
|
+
initialize: (config, _plugins, tools) =>
|
|
113
113
|
Effect.gen(function* () {
|
|
114
114
|
const service = new MyService(config.variables.baseUrl, config.secrets.apiKey);
|
|
115
115
|
yield* service.ping();
|
|
@@ -118,8 +118,8 @@ export default createPlugin({
|
|
|
118
118
|
|
|
119
119
|
shutdown: () => Effect.void,
|
|
120
120
|
|
|
121
|
-
createRouter: (
|
|
122
|
-
const { service } =
|
|
121
|
+
createRouter: (deps, builder) => {
|
|
122
|
+
const { service } = deps;
|
|
123
123
|
|
|
124
124
|
const requireAuth = builder.middleware(async ({ context, next }) => {
|
|
125
125
|
if (!context.userId) throw new ORPCError("UNAUTHORIZED", { message: "Auth required" });
|
|
@@ -148,12 +148,12 @@ import type { PluginsClient } from "./lib/plugins-types.gen";
|
|
|
148
148
|
export default createPlugin.withPlugins<PluginsClient>()({
|
|
149
149
|
variables: z.object({ demoMessage: z.string().optional() }),
|
|
150
150
|
contract,
|
|
151
|
-
initialize: (config, plugins) =>
|
|
151
|
+
initialize: (config, plugins, _tools) =>
|
|
152
152
|
Effect.sync(() => ({ plugins, demoMessage: config.variables.demoMessage ?? "not configured" })),
|
|
153
|
-
createRouter: (
|
|
153
|
+
createRouter: (deps, builder) => ({
|
|
154
154
|
pluginDemo: builder.pluginDemo.handler(async () => {
|
|
155
|
-
const status = await
|
|
156
|
-
return { apiVariable:
|
|
155
|
+
const status = await deps.plugins.registry().getRegistryStatus();
|
|
156
|
+
return { apiVariable: deps.demoMessage, registryStatus: status };
|
|
157
157
|
}),
|
|
158
158
|
}),
|
|
159
159
|
});
|
|
@@ -163,6 +163,51 @@ export default createPlugin.withPlugins<PluginsClient>()({
|
|
|
163
163
|
- Call `services.plugins.{key}()` to execute plugin routers in-process — no HTTP roundtrip
|
|
164
164
|
- The host loads non-API plugins first (Phase 1), then loads the API with `pluginsClient` injected (Phase 2)
|
|
165
165
|
|
|
166
|
+
## Long-Lived Scoped Resources
|
|
167
|
+
|
|
168
|
+
For database pools, caches, publisher channels, or any resource that should live for the plugin's lifetime, use `tools.buildService(tag, layer)` inside `initialize`:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { createPlugin } from "every-plugin";
|
|
172
|
+
import { Effect, Layer } from "every-plugin/effect";
|
|
173
|
+
|
|
174
|
+
export default createPlugin({
|
|
175
|
+
// ...
|
|
176
|
+
initialize: (config, _plugins, tools) =>
|
|
177
|
+
Effect.gen(function* () {
|
|
178
|
+
const repo = yield* tools.buildService(
|
|
179
|
+
MyRepoTag,
|
|
180
|
+
MyRepoLive.pipe(Layer.provide(DatabaseLive(config.secrets.MY_DATABASE_URL))),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const publisher = new MemoryPublisher({ resumeRetentionSeconds: 120 });
|
|
184
|
+
|
|
185
|
+
return { repo, publisher };
|
|
186
|
+
}),
|
|
187
|
+
// ...
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`tools.buildService(tag, layer)` binds the layer's resources to the plugin's lifecycle scope.
|
|
192
|
+
Resources persist until the plugin shuts down and are automatically cleaned up during `runtime.shutdown()`.
|
|
193
|
+
|
|
194
|
+
**Bad — creates a transient scope that closes immediately:**
|
|
195
|
+
```typescript
|
|
196
|
+
const svc = yield* Effect.provide(MyTag, MyLive.pipe(Layer.provide(DatabaseLive(url))))
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Good — resources persist for the plugin's lifetime:**
|
|
200
|
+
```typescript
|
|
201
|
+
const svc = yield* tools.buildService(MyTag, MyLive.pipe(Layer.provide(DatabaseLive(url))))
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Key rules:
|
|
205
|
+
- Use `tools.buildService(...)` for any `Layer.scoped(...)` resource that should survive initialization
|
|
206
|
+
- Plain class construction (new Service(...)) is still fine directly in `initialize`
|
|
207
|
+
- `createRouter(deps)` receives whatever `initialize` returns — same mental model as before
|
|
208
|
+
- Do not use `Effect.provide(Tag, Layer.scoped(...))` for persistent dependencies inside `initialize`
|
|
209
|
+
- `tools` is provided by the framework and does not need to be imported
|
|
210
|
+
|
|
166
211
|
## Dev Server Config (plugin.dev.ts)
|
|
167
212
|
|
|
168
213
|
```typescript
|
|
@@ -214,3 +259,4 @@ export default {
|
|
|
214
259
|
- Forgetting `.errors(Errors)` on routes that can throw ORPCError — untyped errors
|
|
215
260
|
- Using `Effect.runPromise` inside `Effect.gen` — use `yield*` instead for proper error channel
|
|
216
261
|
- Putting business logic in `createRouter` — keep it in the service class, router is just glue
|
|
262
|
+
- Using `Effect.provide(Tag, Layer.scoped(...))` inside `initialize` for long-lived resources — creates a transient scope that releases the resource immediately after initialization. Use `tools.buildService(Tag, Layer.scoped(...))` instead
|
|
@@ -124,6 +124,61 @@ describe("API with mock registry", () => {
|
|
|
124
124
|
});
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
+
## Testing Scope Lifecycle
|
|
128
|
+
|
|
129
|
+
When a plugin uses `tools.buildService(...)` inside `initialize`, verify that scoped resources persist after initialization and are released during shutdown:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
133
|
+
import { createPlugin, createPluginRuntime } from "every-plugin";
|
|
134
|
+
import { Context, Effect, Layer } from "every-plugin/effect";
|
|
135
|
+
|
|
136
|
+
let released = false;
|
|
137
|
+
|
|
138
|
+
class TestTag extends Context.Tag("TestTag")<TestTag, { value: string }>() {}
|
|
139
|
+
|
|
140
|
+
describe("scope lifecycle", () => {
|
|
141
|
+
let runtime: ReturnType<typeof createPluginRuntime>;
|
|
142
|
+
|
|
143
|
+
beforeAll(async () => {
|
|
144
|
+
const plugin = createPlugin({
|
|
145
|
+
// ... variables, secrets, contract ...
|
|
146
|
+
initialize: (_config, _plugins, tools) =>
|
|
147
|
+
Effect.gen(function* () {
|
|
148
|
+
const svc = yield* tools.buildService(TestTag, TestLive);
|
|
149
|
+
return { svc };
|
|
150
|
+
}),
|
|
151
|
+
createRouter: (deps) => ({
|
|
152
|
+
// ... routes ...
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
runtime = createPluginRuntime({
|
|
157
|
+
registry: { "lifecycle-test": { module: plugin } },
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await runtime.usePlugin("lifecycle-test", {
|
|
161
|
+
variables: {}, secrets: {},
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("resource persists after initialization", () => {
|
|
166
|
+
expect(released).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
afterAll(async () => {
|
|
170
|
+
await runtime.shutdown();
|
|
171
|
+
expect(released).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Key assertions:
|
|
177
|
+
- Resource should NOT be released after `usePlugin()` completes
|
|
178
|
+
- Resource SHOULD be released after `runtime.shutdown()`
|
|
179
|
+
- Multiple independent plugins have independent scope lifetimes
|
|
180
|
+
- `runtime.shutdown()` calls cleanup for all registered plugins
|
|
181
|
+
|
|
127
182
|
## Testing Streaming (eventIterator)
|
|
128
183
|
|
|
129
184
|
Use `for await` to collect streaming results:
|
package/src/plugin.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { AnyContractRouter, AnySchema, InferSchemaOutput } from "@orpc/contract";
|
|
2
2
|
import { ORPCError } from "@orpc/contract";
|
|
3
|
-
import type {
|
|
3
|
+
import type { Implementer, Router } from "@orpc/server";
|
|
4
4
|
import { implement, onError } from "@orpc/server";
|
|
5
|
-
import { Effect, type Scope } from "effect";
|
|
5
|
+
import { Effect, type Context as EffectContext, type Layer, type Scope } from "effect";
|
|
6
6
|
import { extractFromFiberFailure, formatORPCError } from "./runtime/errors";
|
|
7
7
|
|
|
8
8
|
type ContextOutput<T> = T extends AnySchema ? InferSchemaOutput<T> : Record<string, never>;
|
|
@@ -22,21 +22,36 @@ type PluginInitializeInput<V extends AnySchema, S extends AnySchema> = {
|
|
|
22
22
|
secrets: InferSchemaOutput<S>;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Tools provided to plugin initialize/services for building long-lived scoped resources.
|
|
27
|
+
*/
|
|
28
|
+
type ServiceOf<T> = T extends EffectContext.Tag<any, infer S> ? S : never;
|
|
29
|
+
|
|
30
|
+
export type PluginServicesTools = {
|
|
31
|
+
buildService: <T>(tag: T, layer: Layer.Layer<any, any, any>) => Effect.Effect<ServiceOf<T>>;
|
|
32
|
+
};
|
|
33
|
+
|
|
25
34
|
type PluginDefinition<
|
|
26
35
|
V extends AnySchema,
|
|
27
36
|
S extends AnySchema,
|
|
28
37
|
TContract extends AnyContractRouter,
|
|
29
38
|
TRequestContext extends AnySchema | undefined,
|
|
30
|
-
TDeps extends
|
|
39
|
+
TDeps extends Record<string, any>,
|
|
31
40
|
P extends Record<string, unknown>,
|
|
32
41
|
> = {
|
|
33
42
|
variables: V;
|
|
34
43
|
secrets: S;
|
|
35
44
|
contract: TContract;
|
|
36
45
|
context?: TRequestContext;
|
|
46
|
+
/**
|
|
47
|
+
* Initialize the plugin and build dependencies.
|
|
48
|
+
* A third argument `tools` is provided for building long-lived scoped resources
|
|
49
|
+
* (e.g. database pools) via `tools.buildService(tag, layer)`.
|
|
50
|
+
*/
|
|
37
51
|
initialize?: (
|
|
38
52
|
config: PluginInitializeInput<V, S>,
|
|
39
53
|
plugins: P,
|
|
54
|
+
tools: PluginServicesTools,
|
|
40
55
|
) => Effect.Effect<TDeps, Error, Scope.Scope>;
|
|
41
56
|
createRouter: (
|
|
42
57
|
deps: TDeps,
|
|
@@ -53,7 +68,7 @@ export interface LoadedPluginWithBinding<
|
|
|
53
68
|
TVariables extends AnySchema,
|
|
54
69
|
TSecrets extends AnySchema,
|
|
55
70
|
TRequestContext extends AnySchema | undefined,
|
|
56
|
-
TDeps extends
|
|
71
|
+
TDeps extends Record<string, any> = Record<never, never>,
|
|
57
72
|
> {
|
|
58
73
|
new (): Plugin<TContract, TVariables, TSecrets, TRequestContext, TDeps>;
|
|
59
74
|
binding: {
|
|
@@ -72,7 +87,7 @@ export interface Plugin<
|
|
|
72
87
|
TVariables extends AnySchema,
|
|
73
88
|
TSecrets extends AnySchema,
|
|
74
89
|
TRequestContext extends AnySchema | undefined,
|
|
75
|
-
TDeps extends
|
|
90
|
+
TDeps extends Record<string, any> = Record<never, never>,
|
|
76
91
|
> {
|
|
77
92
|
readonly id: string;
|
|
78
93
|
readonly contract: TContract;
|
|
@@ -81,6 +96,7 @@ export interface Plugin<
|
|
|
81
96
|
initialize(
|
|
82
97
|
config: PluginInitializeInput<TVariables, TSecrets>,
|
|
83
98
|
plugins: Record<string, unknown>,
|
|
99
|
+
tools: PluginServicesTools,
|
|
84
100
|
): Effect.Effect<TDeps, unknown, Scope.Scope>;
|
|
85
101
|
|
|
86
102
|
shutdown(): Effect.Effect<void, never>;
|
|
@@ -100,7 +116,7 @@ export interface CreatePluginFn {
|
|
|
100
116
|
S extends AnySchema,
|
|
101
117
|
TContract extends AnyContractRouter,
|
|
102
118
|
TRequestContext extends AnySchema | undefined = undefined,
|
|
103
|
-
TDeps extends
|
|
119
|
+
TDeps extends Record<string, any> = Record<never, never>,
|
|
104
120
|
P extends Record<string, unknown> = Record<string, never>,
|
|
105
121
|
>(
|
|
106
122
|
config: PluginDefinition<V, S, TContract, TRequestContext, TDeps, P>,
|
|
@@ -114,7 +130,7 @@ export const createPlugin: CreatePluginFn = function createPlugin<
|
|
|
114
130
|
S extends AnySchema,
|
|
115
131
|
TContract extends AnyContractRouter,
|
|
116
132
|
TRequestContext extends AnySchema | undefined = undefined,
|
|
117
|
-
TDeps extends
|
|
133
|
+
TDeps extends Record<string, any> = Record<never, never>,
|
|
118
134
|
P extends Record<string, unknown> = Record<string, never>,
|
|
119
135
|
>(config: PluginDefinition<V, S, TContract, TRequestContext, TDeps, P>) {
|
|
120
136
|
const configSchema: PluginConfigFor<V, S, TRequestContext> = {
|
|
@@ -134,10 +150,11 @@ export const createPlugin: CreatePluginFn = function createPlugin<
|
|
|
134
150
|
initialize(
|
|
135
151
|
pluginConfig: PluginInitializeInput<V, S>,
|
|
136
152
|
plugins: Record<string, unknown> = {},
|
|
153
|
+
tools: PluginServicesTools,
|
|
137
154
|
): Effect.Effect<TDeps, unknown, Scope.Scope> {
|
|
138
155
|
const init = config.initialize ?? (() => Effect.succeed({} as TDeps));
|
|
139
156
|
|
|
140
|
-
return init(pluginConfig, plugins as P).pipe(
|
|
157
|
+
return init(pluginConfig, plugins as P, tools).pipe(
|
|
141
158
|
Effect.tap((deps) =>
|
|
142
159
|
Effect.sync(() => {
|
|
143
160
|
this._deps = deps;
|
|
@@ -209,7 +226,7 @@ export type CreatePluginWithPlugins<P extends Record<string, unknown>> = <
|
|
|
209
226
|
S extends AnySchema,
|
|
210
227
|
TContract extends AnyContractRouter,
|
|
211
228
|
TRequestContext extends AnySchema | undefined = undefined,
|
|
212
|
-
TDeps extends
|
|
229
|
+
TDeps extends Record<string, any> = Record<never, never>,
|
|
213
230
|
>(
|
|
214
231
|
config: PluginDefinition<V, S, TContract, TRequestContext, TDeps, P>,
|
|
215
232
|
) => LoadedPluginWithBinding<TContract, V, S, TRequestContext, TDeps>;
|
|
@@ -220,7 +237,7 @@ export function withPlugins<P extends Record<string, unknown>>(): CreatePluginWi
|
|
|
220
237
|
S extends AnySchema,
|
|
221
238
|
TContract extends AnyContractRouter,
|
|
222
239
|
TRequestContext extends AnySchema | undefined = undefined,
|
|
223
|
-
TDeps extends
|
|
240
|
+
TDeps extends Record<string, any> = Record<never, never>,
|
|
224
241
|
>(
|
|
225
242
|
config: PluginDefinition<V, S, TContract, TRequestContext, TDeps, P>,
|
|
226
243
|
) => createPlugin<V, S, TContract, TRequestContext, TDeps, P>(config as any);
|
package/src/runtime/errors.ts
CHANGED
|
@@ -132,7 +132,10 @@ export const formatORPCError = (error: any): string | null => {
|
|
|
132
132
|
const message = error.message || "An error occurred";
|
|
133
133
|
|
|
134
134
|
lines.push(`\n╭─ oRPC Error ${"─".repeat(40)}`);
|
|
135
|
-
|
|
135
|
+
const messageLines = message.split("\n");
|
|
136
|
+
for (const line of messageLines) {
|
|
137
|
+
lines.push(`│ ${line}`);
|
|
138
|
+
}
|
|
136
139
|
lines.push(`│ Code: ${code} (${status})`);
|
|
137
140
|
lines.push(`│`);
|
|
138
141
|
|
package/src/runtime/index.ts
CHANGED
|
@@ -78,10 +78,11 @@ export class PluginRuntime<R = RegisteredPlugins> {
|
|
|
78
78
|
const pluginService = yield* PluginService;
|
|
79
79
|
const validatedId = yield* this.validatePluginId(pluginId);
|
|
80
80
|
|
|
81
|
-
// Load → Instantiate → Initialize
|
|
81
|
+
// Load → Instantiate → Initialize → Register
|
|
82
82
|
const ctor = yield* pluginService.loadPlugin(validatedId);
|
|
83
83
|
const instance = yield* pluginService.instantiatePlugin(pluginId, ctor);
|
|
84
84
|
const initialized = yield* pluginService.initializePlugin(instance, config, plugins);
|
|
85
|
+
yield* pluginService.registerPlugin(initialized);
|
|
85
86
|
|
|
86
87
|
return initialized;
|
|
87
88
|
}).pipe(Effect.annotateLogs({ plugin: pluginId }), Effect.provide(this.runtime));
|
|
@@ -134,7 +135,9 @@ export class PluginRuntime<R = RegisteredPlugins> {
|
|
|
134
135
|
): Promise<InitializedPlugin<T>> {
|
|
135
136
|
const effect = Effect.gen(function* () {
|
|
136
137
|
const pluginService = yield* PluginService;
|
|
137
|
-
|
|
138
|
+
const initialized = yield* pluginService.initializePlugin(instance, config, plugins);
|
|
139
|
+
yield* pluginService.registerPlugin(initialized);
|
|
140
|
+
return initialized;
|
|
138
141
|
}).pipe(Effect.annotateLogs({ plugin: instance.plugin.id }));
|
|
139
142
|
return this.runPromise(effect);
|
|
140
143
|
}
|
|
@@ -151,8 +154,9 @@ export class PluginRuntime<R = RegisteredPlugins> {
|
|
|
151
154
|
async evictPlugin<K extends keyof R & string>(
|
|
152
155
|
pluginId: K,
|
|
153
156
|
config: PluginConfigInput<R[K]>,
|
|
157
|
+
plugins?: Record<string, unknown>,
|
|
154
158
|
): Promise<void> {
|
|
155
|
-
const cacheKey = this.generateCacheKey(pluginId, config);
|
|
159
|
+
const cacheKey = this.generateCacheKey(pluginId, { ...config, __plugins: plugins ?? {} });
|
|
156
160
|
|
|
157
161
|
const effect = Effect.gen(this, function* () {
|
|
158
162
|
const pluginService = yield* PluginService;
|
|
@@ -49,6 +49,11 @@ export class PluginLifecycleService extends Effect.Service<PluginLifecycleServic
|
|
|
49
49
|
cleanup: () =>
|
|
50
50
|
Effect.gen(function* () {
|
|
51
51
|
const plugins = yield* Ref.get(activePlugins);
|
|
52
|
+
if (typeof process !== "undefined" && process.env?.EVERY_PLUGIN_DEBUG_SCOPES === "1") {
|
|
53
|
+
yield* Effect.logWarning(
|
|
54
|
+
`[PluginLifecycle] cleaning up ${plugins.size} active plugin(s)`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
52
57
|
|
|
53
58
|
yield* Effect.forEach(
|
|
54
59
|
plugins,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { InferSchemaInput, InferSchemaOutput } from "@orpc/contract";
|
|
2
|
-
import { Context, Effect, Scope } from "effect";
|
|
2
|
+
import { Context, Effect, Context as EffectContext, Layer, Scope } from "effect";
|
|
3
3
|
import type { z } from "zod";
|
|
4
|
+
import type { PluginServicesTools } from "../../plugin";
|
|
4
5
|
import type {
|
|
5
6
|
AnyPlugin,
|
|
6
7
|
AnyPluginConstructor,
|
|
@@ -237,9 +238,28 @@ export class PluginLoaderService extends Effect.Service<PluginLoaderService>()(
|
|
|
237
238
|
// Create a long-lived scope for this plugin instance
|
|
238
239
|
const scope = yield* Scope.make();
|
|
239
240
|
|
|
241
|
+
// Create a per-plugin MemoMap so tool-built layers are memoized
|
|
242
|
+
const memoMap = yield* Layer.makeMemoMap;
|
|
243
|
+
|
|
244
|
+
const tools: PluginServicesTools = {
|
|
245
|
+
buildService: (tag: any, layer: any) =>
|
|
246
|
+
(Layer.buildWithMemoMap(layer, memoMap, scope) as any).pipe(
|
|
247
|
+
Effect.map((ctx: any) => EffectContext.get(ctx, tag)) as any,
|
|
248
|
+
),
|
|
249
|
+
};
|
|
250
|
+
|
|
240
251
|
// Initialize plugin within the scope
|
|
252
|
+
// The plugin's initialize method handles:
|
|
253
|
+
// 1. Building services (DB pools, caches) via tools.buildService
|
|
254
|
+
// 2. Shaping final deps via initialize
|
|
255
|
+
// Any Layer.scoped resources built through tools.buildService are
|
|
256
|
+
// bound to the plugin scope and persist until the plugin is shutdown.
|
|
241
257
|
const context = yield* plugin
|
|
242
|
-
.initialize(
|
|
258
|
+
.initialize(
|
|
259
|
+
{ variables: _variables, secrets: hydratedConfig.secrets },
|
|
260
|
+
plugins ?? {},
|
|
261
|
+
tools,
|
|
262
|
+
)
|
|
243
263
|
.pipe(
|
|
244
264
|
Effect.provideService(Scope.Scope, scope),
|
|
245
265
|
Effect.tapError((_error) =>
|
|
@@ -25,6 +25,7 @@ export class PluginService extends Effect.Service<PluginService>()("PluginServic
|
|
|
25
25
|
loadPlugin: loader.loadPlugin,
|
|
26
26
|
instantiatePlugin: loader.instantiatePlugin,
|
|
27
27
|
initializePlugin: loader.initializePlugin,
|
|
28
|
+
registerPlugin: (plugin: InitializedPlugin<AnyPlugin>) => lifecycle.register(plugin),
|
|
28
29
|
shutdownPlugin: (plugin: InitializedPlugin<AnyPlugin>) =>
|
|
29
30
|
Effect.gen(function* () {
|
|
30
31
|
yield* plugin.plugin
|