every-plugin 0.1.0 → 0.1.2

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.
Files changed (65) hide show
  1. package/README.md +1 -329
  2. package/dist/errors.d.ts +11 -7
  3. package/dist/errors.d.ts.map +1 -1
  4. package/dist/errors.js +3 -14
  5. package/dist/index.d.ts +2 -4
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -3
  8. package/dist/plugin.d.ts +149 -103
  9. package/dist/plugin.d.ts.map +1 -1
  10. package/dist/plugin.js +155 -23
  11. package/dist/runtime/errors.d.ts +41 -53
  12. package/dist/runtime/errors.d.ts.map +1 -1
  13. package/dist/runtime/errors.js +48 -8
  14. package/dist/runtime/index.d.ts +17 -66
  15. package/dist/runtime/index.d.ts.map +1 -1
  16. package/dist/runtime/index.js +22 -126
  17. package/dist/runtime/services/module-federation.service.d.ts +10 -38
  18. package/dist/runtime/services/module-federation.service.d.ts.map +1 -1
  19. package/dist/runtime/services/module-federation.service.js +131 -124
  20. package/dist/runtime/services/plugin.service.d.ts +21 -67
  21. package/dist/runtime/services/plugin.service.d.ts.map +1 -1
  22. package/dist/runtime/services/plugin.service.js +265 -328
  23. package/dist/runtime/services/secrets.service.d.ts +9 -29
  24. package/dist/runtime/services/secrets.service.js +26 -30
  25. package/dist/runtime/services.d.ts +1 -1
  26. package/dist/runtime/streaming/index.d.ts +4 -0
  27. package/dist/runtime/streaming/index.d.ts.map +1 -0
  28. package/dist/runtime/streaming/index.js +2 -0
  29. package/dist/runtime/streaming/state-effects.d.ts +28 -0
  30. package/dist/runtime/streaming/state-effects.d.ts.map +1 -0
  31. package/dist/runtime/streaming/state-effects.js +30 -0
  32. package/dist/runtime/streaming/streaming.d.ts +8 -0
  33. package/dist/runtime/streaming/streaming.d.ts.map +1 -0
  34. package/dist/runtime/streaming/streaming.js +113 -0
  35. package/dist/runtime/streaming/types.d.ts +19 -0
  36. package/dist/runtime/streaming/types.d.ts.map +1 -0
  37. package/dist/runtime/streaming/types.js +1 -0
  38. package/dist/runtime/test-utils.d.ts +1 -1
  39. package/dist/runtime/test-utils.js +4 -10
  40. package/dist/runtime/types.d.ts +28 -28
  41. package/dist/runtime/types.d.ts.map +1 -1
  42. package/dist/runtime/validation.d.ts +2 -7
  43. package/dist/runtime/validation.js +11 -14
  44. package/dist/testing/index.d.ts +25 -0
  45. package/dist/testing/index.d.ts.map +1 -0
  46. package/dist/testing/index.js +40 -0
  47. package/dist/testing/mocks/module-federation.service.d.ts +8 -0
  48. package/dist/testing/mocks/module-federation.service.d.ts.map +1 -0
  49. package/dist/testing/mocks/module-federation.service.js +35 -0
  50. package/package.json +27 -14
  51. package/dist/runtime/mf.test.d.ts +0 -2
  52. package/dist/runtime/mf.test.d.ts.map +0 -1
  53. package/dist/runtime/mf.test.js +0 -135
  54. package/dist/runtime/runtime.test.d.ts +0 -2
  55. package/dist/runtime/runtime.test.d.ts.map +0 -1
  56. package/dist/runtime/runtime.test.js +0 -378
  57. package/dist/runtime/streaming.d.ts +0 -29
  58. package/dist/runtime/streaming.d.ts.map +0 -1
  59. package/dist/runtime/streaming.js +0 -59
  60. package/dist/runtime/streaming.test.d.ts +0 -2
  61. package/dist/runtime/streaming.test.d.ts.map +0 -1
  62. package/dist/runtime/streaming.test.js +0 -271
  63. package/dist/source.d.ts +0 -70
  64. package/dist/source.d.ts.map +0 -1
  65. package/dist/source.js +0 -37
@@ -1,338 +1,275 @@
1
+ import { call } from "@orpc/server";
1
2
  import { Cache, Duration, Effect, Hash, Layer, Schedule } from "effect";
2
3
  import { PluginLoggerTag } from "../../plugin";
3
4
  import { PluginRuntimeError } from "../errors";
5
+ import { createSourceStream } from "../streaming";
4
6
  import { validate } from "../validation";
5
7
  import { ModuleFederationService } from "./module-federation.service";
6
8
  import { SecretsService } from "./secrets.service";
7
9
  export class PluginService extends Effect.Tag("PluginService")() {
8
- static Live = (registry, secrets) =>
9
- Layer.effect(
10
- PluginService,
11
- Effect.gen(function* () {
12
- const moduleFederationService = yield* ModuleFederationService;
13
- const secretsService = yield* SecretsService;
14
- const logger = yield* PluginLoggerTag;
15
- const activePlugins = new Set();
16
- const retrySchedule = Schedule.exponential(Duration.millis(100)).pipe(
17
- Schedule.compose(Schedule.recurs(2)),
18
- );
19
- const resolveUrl = (baseUrl, version) =>
20
- version && version !== "latest"
21
- ? baseUrl.replace("@latest", `@${version}`)
22
- : baseUrl;
23
- // Implementation functions
24
- const loadPluginImpl = (pluginId) =>
25
- Effect.gen(function* () {
26
- const metadata = registry[pluginId];
27
- if (!metadata) {
28
- return yield* Effect.fail(
29
- new PluginRuntimeError({
30
- pluginId,
31
- operation: "load-plugin",
32
- cause: new Error(`Plugin ${pluginId} not found in registry`),
33
- retryable: false,
34
- }),
35
- );
36
- }
37
- const url = resolveUrl(metadata.remoteUrl);
38
- // Register and load the remote constructor
39
- yield* moduleFederationService.registerRemote(pluginId, url).pipe(
40
- Effect.mapError(
41
- (error) =>
42
- new PluginRuntimeError({
43
- pluginId,
44
- operation: "register-remote",
45
- cause: error.cause,
46
- retryable: true,
47
- }),
48
- ),
49
- );
50
- logger.logInfo(`Trying to load ${pluginId}:${url}`);
51
- const ctor = yield* moduleFederationService
52
- .loadRemoteConstructor(pluginId, url)
53
- .pipe(
54
- Effect.mapError(
55
- (error) =>
56
- new PluginRuntimeError({
57
- pluginId,
58
- operation: "load-remote",
59
- cause: error.cause,
60
- retryable: false,
61
- }),
62
- ),
63
- );
64
- return {
65
- ctor,
66
- metadata: {
67
- pluginId,
68
- version: metadata.version,
69
- description: metadata.description,
70
- type: metadata.type,
71
- },
72
- };
73
- });
74
- const instantiatePluginImpl = (pluginConstructor) =>
75
- Effect.gen(function* () {
76
- const instance = yield* Effect.try({
77
- try: () => new pluginConstructor.ctor(),
78
- catch: (error) =>
79
- new PluginRuntimeError({
80
- pluginId: pluginConstructor.metadata.pluginId,
81
- operation: "instantiate-plugin",
82
- cause:
83
- error instanceof Error ? error : new Error(String(error)),
84
- retryable: false,
85
- }),
86
- });
87
- // Validate plugin ID matches
88
- if (instance.id !== pluginConstructor.metadata.pluginId) {
89
- return yield* Effect.fail(
90
- new PluginRuntimeError({
91
- pluginId: pluginConstructor.metadata.pluginId,
92
- operation: "validate-plugin-id",
93
- cause: new Error(
94
- `Plugin ID mismatch: expected ${pluginConstructor.metadata.pluginId}, got ${instance.id}`,
95
- ),
96
- retryable: false,
97
- }),
98
- );
99
- }
100
- return {
101
- plugin: instance,
102
- metadata: pluginConstructor.metadata,
103
- };
104
- });
105
- const initializePluginImpl = (pluginInstance, config) =>
106
- Effect.gen(function* () {
107
- const { plugin } = pluginInstance;
108
- // Validate raw config using plugin's Zod schema
109
- const validatedConfig = yield* validate(
110
- plugin.configSchema,
111
- config,
112
- plugin.id,
113
- "config",
114
- ).pipe(
115
- Effect.mapError(
116
- (validationError) =>
117
- new PluginRuntimeError({
118
- pluginId: plugin.id,
119
- operation: "validate-config",
120
- cause: validationError.zodError,
121
- retryable: false,
122
- }),
123
- ),
124
- );
125
- // Hydrate secrets
126
- const hydratedConfig = yield* secretsService.hydrateSecrets(
127
- validatedConfig,
128
- secrets,
129
- );
130
- // Validate hydrated config
131
- const finalConfig = yield* validate(
132
- plugin.configSchema,
133
- hydratedConfig,
134
- plugin.id,
135
- "config",
136
- ).pipe(
137
- Effect.mapError(
138
- (validationError) =>
139
- new PluginRuntimeError({
140
- pluginId: plugin.id,
141
- operation: "validate-hydrated-config",
142
- cause: validationError.zodError,
143
- retryable: false,
144
- }),
145
- ),
146
- );
147
- // Initialize plugin with retry logic
148
- const pluginLayer = Layer.succeed(PluginLoggerTag, logger);
149
- yield* plugin.initialize(finalConfig).pipe(
150
- Effect.mapError(
151
- (error) =>
152
- new PluginRuntimeError({
153
- pluginId: plugin.id,
154
- operation: "initialize-plugin",
155
- cause: error,
156
- retryable: error.retryable ?? false,
157
- }),
158
- ),
159
- Effect.retry(
160
- retrySchedule.pipe(
161
- Schedule.whileInput((error) => error.retryable),
162
- ),
163
- ),
164
- Effect.provide(pluginLayer),
165
- );
166
- const initializedPlugin = {
167
- plugin,
168
- metadata: pluginInstance.metadata,
169
- config: finalConfig, // TODO: config type
170
- };
171
- activePlugins.add(initializedPlugin);
172
- return initializedPlugin;
173
- });
174
- // Cache functionality
175
- const generateConfigHash = (config) => {
176
- return Hash.structure(config).toString();
177
- };
178
- // Store original config by hash for cache lookup
179
- const configByHash = new Map();
180
- const cache = yield* Cache.make({
181
- capacity: 1024,
182
- timeToLive: Duration.minutes(60),
183
- lookup: (cacheKey) => {
184
- const [pluginId, configHash] = cacheKey.split(":");
185
- if (!pluginId || !configHash) {
186
- return Effect.fail(
187
- new PluginRuntimeError({
188
- operation: "cache-lookup",
189
- cause: new Error(`Invalid cache key format: ${cacheKey}`),
190
- retryable: false,
191
- }),
192
- );
193
- }
194
- const config = configByHash.get(configHash);
195
- if (!config) {
196
- return Effect.fail(
197
- new PluginRuntimeError({
198
- operation: "cache-lookup",
199
- cause: new Error(`No config found for hash: ${configHash}`),
200
- retryable: false,
201
- }),
202
- );
203
- }
204
- return Effect.gen(function* () {
205
- const ctor = yield* loadPluginImpl(pluginId);
206
- const instance = yield* instantiatePluginImpl(ctor);
207
- return yield* initializePluginImpl(instance, config);
208
- });
209
- },
210
- });
211
- return {
212
- loadPlugin: loadPluginImpl,
213
- instantiatePlugin: instantiatePluginImpl,
214
- initializePlugin: initializePluginImpl,
215
- executePlugin: (initializedPlugin, input) =>
216
- Effect.gen(function* () {
217
- const { plugin } = initializedPlugin;
218
- // Validate input
219
- const validatedInput = yield* validate(
220
- plugin.inputSchema,
221
- input,
222
- plugin.id,
223
- "input",
224
- ).pipe(
225
- Effect.mapError((validationError) => {
226
- console.log("got input error", validationError.zodError);
227
- return new PluginRuntimeError({
228
- pluginId: plugin.id,
229
- operation: "validate-input",
230
- cause: validationError.zodError,
231
- retryable: false,
232
- });
233
- }),
234
- );
235
- // Execute plugin
236
- const pluginLayer = Layer.succeed(PluginLoggerTag, logger);
237
- const output = yield* plugin.execute(validatedInput).pipe(
238
- Effect.mapError(
239
- (error) =>
240
- new PluginRuntimeError({
241
- pluginId: plugin.id,
242
- operation: "execute-plugin",
243
- cause: error,
244
- retryable: error.retryable ?? false,
245
- }),
246
- ),
247
- Effect.retry(
248
- retrySchedule.pipe(
249
- Schedule.whileInput((error) => error.retryable),
250
- ),
251
- ),
252
- Effect.provide(pluginLayer),
253
- );
254
- // Validate output
255
- const validatedOutput = yield* validate(
256
- plugin.outputSchema,
257
- output,
258
- plugin.id,
259
- "output",
260
- ).pipe(
261
- Effect.mapError((validationError) => {
262
- console.log("got output error", validationError.zodError);
263
- return new PluginRuntimeError({
264
- pluginId: plugin.id,
265
- operation: "validate-output",
266
- cause: validationError.zodError,
267
- retryable: false,
268
- });
269
- }),
270
- );
271
- return validatedOutput;
272
- }),
273
- usePlugin: (pluginId, config) => {
274
- const configHash = generateConfigHash(config);
275
- const cacheKey = `${pluginId}:${configHash}`;
276
- // Store config for cache lookup
277
- configByHash.set(configHash, config);
278
- return cache.get(cacheKey).pipe(Effect.map((plugin) => plugin));
279
- },
280
- shutdownPlugin: (plugin) =>
281
- Effect.gen(function* () {
282
- const pluginLayer = Layer.succeed(PluginLoggerTag, logger);
283
- // Remove from active plugins
284
- activePlugins.delete(plugin);
285
- // Try to invalidate from cache if it exists there
286
- const configHash = generateConfigHash(plugin.config);
287
- const cacheKey = `${plugin.plugin.id}:${configHash}`;
288
- yield* cache.invalidate(cacheKey).pipe(
289
- Effect.catchAll(() => Effect.void), // Ignore if not in cache
290
- );
291
- // Clean up config hash
292
- configByHash.delete(configHash);
293
- // Shutdown the plugin
294
- yield* plugin.plugin.shutdown().pipe(
295
- Effect.mapError(
296
- (error) =>
297
- new PluginRuntimeError({
298
- pluginId: plugin.plugin.id,
299
- operation: "shutdown-plugin",
300
- cause: error,
301
- retryable: false,
302
- }),
303
- ),
304
- Effect.provide(pluginLayer),
305
- );
306
- }),
307
- clearCache: () =>
308
- Effect.sync(() => {
309
- configByHash.clear();
310
- }),
311
- cleanup: () =>
312
- Effect.gen(function* () {
313
- const pluginLayer = Layer.succeed(PluginLoggerTag, logger);
314
- // Call shutdown on all active plugins
315
- for (const initializedPlugin of activePlugins) {
316
- yield* initializedPlugin.plugin.shutdown().pipe(
317
- Effect.catchAll((error) => {
318
- // Log shutdown errors but don't fail the cleanup
319
- return logger.logError(
320
- `Failed to shutdown plugin ${initializedPlugin.plugin.id}`,
321
- error,
322
- { pluginId: initializedPlugin.plugin.id },
323
- );
324
- }),
325
- Effect.provide(pluginLayer),
326
- );
327
- }
328
- // Clear the active plugins set
329
- activePlugins.clear();
330
- // Clear cache
331
- configByHash.clear();
332
- }),
333
- };
334
- }),
335
- );
10
+ static Live = (registry, secrets) => Layer.effect(PluginService, Effect.gen(function* () {
11
+ const moduleFederationService = yield* ModuleFederationService;
12
+ const secretsService = yield* SecretsService;
13
+ const logger = yield* PluginLoggerTag;
14
+ const activePlugins = new Set();
15
+ const retrySchedule = Schedule.exponential(Duration.millis(100)).pipe(Schedule.compose(Schedule.recurs(2)));
16
+ const resolveUrl = (baseUrl, version) => version && version !== "latest"
17
+ ? baseUrl.replace("@latest", `@${version}`)
18
+ : baseUrl;
19
+ // Implementation functions
20
+ const loadPluginImpl = (pluginId) => Effect.gen(function* () {
21
+ const metadata = registry[pluginId];
22
+ if (!metadata) {
23
+ return yield* Effect.fail(new PluginRuntimeError({
24
+ pluginId,
25
+ operation: "load-plugin",
26
+ cause: new Error(`Plugin ${pluginId} not found in registry`),
27
+ retryable: false,
28
+ }));
29
+ }
30
+ const url = resolveUrl(metadata.remoteUrl);
31
+ // Register and load the remote constructor
32
+ yield* moduleFederationService.registerRemote(pluginId, url).pipe(Effect.mapError((error) => new PluginRuntimeError({
33
+ pluginId,
34
+ operation: "register-remote",
35
+ cause: error.cause,
36
+ retryable: true,
37
+ })));
38
+ logger.logDebug(`Trying to load ${pluginId}:${url}`);
39
+ const ctor = yield* moduleFederationService.loadRemoteConstructor(pluginId, url).pipe(Effect.mapError((error) => new PluginRuntimeError({
40
+ pluginId,
41
+ operation: "load-remote",
42
+ cause: error.cause,
43
+ retryable: false
44
+ })));
45
+ return {
46
+ ctor,
47
+ metadata: {
48
+ pluginId,
49
+ version: metadata.version,
50
+ description: metadata.description,
51
+ type: metadata.type,
52
+ },
53
+ };
54
+ });
55
+ const instantiatePluginImpl = (pluginConstructor) => Effect.gen(function* () {
56
+ const instance = yield* Effect.try({
57
+ try: () => new pluginConstructor.ctor(),
58
+ catch: (error) => new PluginRuntimeError({
59
+ pluginId: pluginConstructor.metadata.pluginId,
60
+ operation: "instantiate-plugin",
61
+ cause: error instanceof Error ? error : new Error(String(error)),
62
+ retryable: false,
63
+ }),
64
+ });
65
+ // Validate plugin ID matches
66
+ if (instance.id !== pluginConstructor.metadata.pluginId) {
67
+ return yield* Effect.fail(new PluginRuntimeError({
68
+ pluginId: pluginConstructor.metadata.pluginId,
69
+ operation: "validate-plugin-id",
70
+ cause: new Error(`Plugin ID mismatch: expected ${pluginConstructor.metadata.pluginId}, got ${instance.id}`),
71
+ retryable: false,
72
+ }));
73
+ }
74
+ return {
75
+ plugin: instance,
76
+ metadata: pluginConstructor.metadata,
77
+ };
78
+ });
79
+ const initializePluginImpl = (pluginInstance, config) => Effect.gen(function* () {
80
+ const { plugin } = pluginInstance;
81
+ // Validate raw config using plugin's Zod schema
82
+ const validatedConfig = yield* validate(plugin.configSchema, config, plugin.id, "config").pipe(Effect.mapError((validationError) => new PluginRuntimeError({
83
+ pluginId: plugin.id,
84
+ operation: "validate-config",
85
+ cause: validationError.zodError,
86
+ retryable: false,
87
+ })));
88
+ // Hydrate secrets
89
+ const hydratedConfig = yield* secretsService.hydrateSecrets(validatedConfig, secrets);
90
+ // Validate hydrated config
91
+ const finalConfig = yield* validate(plugin.configSchema, hydratedConfig, plugin.id, "config").pipe(Effect.mapError((validationError) => new PluginRuntimeError({
92
+ pluginId: plugin.id,
93
+ operation: "validate-hydrated-config",
94
+ cause: validationError.zodError,
95
+ retryable: false,
96
+ })));
97
+ // Initialize plugin with retry logic
98
+ const pluginLayer = Layer.succeed(PluginLoggerTag, logger);
99
+ yield* plugin.initialize(finalConfig).pipe(Effect.mapError((error) => new PluginRuntimeError({
100
+ pluginId: plugin.id,
101
+ operation: "initialize-plugin",
102
+ cause: error,
103
+ retryable: error.retryable ?? false,
104
+ })), Effect.retry(retrySchedule.pipe(Schedule.whileInput((error) => error.retryable))), Effect.provide(pluginLayer));
105
+ const initializedPlugin = {
106
+ plugin,
107
+ metadata: pluginInstance.metadata,
108
+ config: finalConfig, // TODO: config type
109
+ };
110
+ activePlugins.add(initializedPlugin);
111
+ return initializedPlugin;
112
+ });
113
+ // Cache functionality
114
+ const generateConfigHash = (config) => {
115
+ return Hash.structure(config).toString();
116
+ };
117
+ // Store original config by hash for cache lookup
118
+ const configByHash = new Map();
119
+ const cache = yield* Cache.make({
120
+ capacity: 1024,
121
+ timeToLive: Duration.minutes(60),
122
+ lookup: (cacheKey) => {
123
+ const [pluginId, configHash] = cacheKey.split(':');
124
+ if (!pluginId || !configHash) {
125
+ return Effect.fail(new PluginRuntimeError({
126
+ operation: "cache-lookup",
127
+ cause: new Error(`Invalid cache key format: ${cacheKey}`),
128
+ retryable: false,
129
+ }));
130
+ }
131
+ const config = configByHash.get(configHash);
132
+ if (!config) {
133
+ return Effect.fail(new PluginRuntimeError({
134
+ operation: "cache-lookup",
135
+ cause: new Error(`No config found for hash: ${configHash}`),
136
+ retryable: false,
137
+ }));
138
+ }
139
+ return Effect.gen(function* () {
140
+ const ctor = yield* loadPluginImpl(pluginId);
141
+ const instance = yield* instantiatePluginImpl(ctor);
142
+ return yield* initializePluginImpl(instance, config);
143
+ });
144
+ },
145
+ });
146
+ const usePluginImpl = (pluginId, config) => {
147
+ const configHash = generateConfigHash(config);
148
+ const cacheKey = `${pluginId}:${configHash}`;
149
+ // Store config for cache lookup
150
+ configByHash.set(configHash, config);
151
+ return cache.get(cacheKey).pipe(Effect.map(plugin => plugin));
152
+ };
153
+ // Extract executePlugin implementation for reuse
154
+ const executePluginImpl = (initializedPlugin, input) => Effect.gen(function* () {
155
+ const { plugin } = initializedPlugin;
156
+ // Validate input
157
+ const validatedInput = yield* validate(plugin.inputSchema, input, plugin.id, "input").pipe(Effect.mapError((validationError) => {
158
+ return new PluginRuntimeError({
159
+ pluginId: plugin.id,
160
+ operation: "validate-input",
161
+ cause: validationError.zodError,
162
+ retryable: false,
163
+ });
164
+ }));
165
+ // Get oRPC router and call procedure directly
166
+ const router = plugin.createRouter();
167
+ const procedureName = validatedInput.procedure;
168
+ if (!(procedureName in router)) {
169
+ return yield* Effect.fail(new PluginRuntimeError({
170
+ pluginId: plugin.id,
171
+ operation: "execute-plugin",
172
+ cause: new Error(`Unknown procedure: ${procedureName}`),
173
+ retryable: false,
174
+ }));
175
+ }
176
+ const context = 'state' in validatedInput ? { state: validatedInput.state } : {};
177
+ const output = yield* Effect.tryPromise({
178
+ try: () => {
179
+ const procedure = router[procedureName];
180
+ ;
181
+ if (!procedure) {
182
+ throw new Error(`Procedure ${procedureName} not found in router`);
183
+ }
184
+ // Use oRPC call utility as per docs
185
+ return call(procedure, validatedInput.input, { context });
186
+ },
187
+ catch: (error) => new PluginRuntimeError({
188
+ pluginId: plugin.id,
189
+ operation: "execute-plugin",
190
+ cause: error instanceof Error ? error : new Error(String(error)),
191
+ retryable: true,
192
+ }),
193
+ }).pipe(Effect.retry(retrySchedule.pipe(Schedule.whileInput((error) => error.retryable))));
194
+ return output;
195
+ });
196
+ return {
197
+ loadPlugin: loadPluginImpl,
198
+ instantiatePlugin: instantiatePluginImpl,
199
+ initializePlugin: initializePluginImpl,
200
+ executePlugin: executePluginImpl,
201
+ usePlugin: usePluginImpl,
202
+ streamPlugin: (pluginId, config, input, options) => Effect.gen(function* () {
203
+ const initializedPlugin = yield* usePluginImpl(pluginId, config);
204
+ // Check if procedure is streamable and validate state
205
+ const procedureName = input.procedure;
206
+ const isStreamable = initializedPlugin.plugin.isStreamable(procedureName);
207
+ if (!isStreamable) {
208
+ return yield* Effect.fail(new PluginRuntimeError({
209
+ pluginId,
210
+ operation: "stream-plugin-validate",
211
+ cause: new Error(`Procedure ${procedureName} is not streamable`),
212
+ retryable: false,
213
+ }));
214
+ }
215
+ // Check stateSchema exists for streamable procedures
216
+ if (!('stateSchema' in initializedPlugin.plugin) || !initializedPlugin.plugin.stateSchema) {
217
+ return yield* Effect.fail(new PluginRuntimeError({
218
+ pluginId: initializedPlugin.plugin.id,
219
+ operation: "validate-state",
220
+ cause: new Error(`Streamable plugin ${initializedPlugin.plugin.id} must have a stateSchema`),
221
+ retryable: false,
222
+ }));
223
+ }
224
+ // Validate initial state - allow null for initial calls
225
+ const nullableStateSchema = initializedPlugin.plugin.stateSchema.nullable();
226
+ yield* validate(nullableStateSchema, input.state, initializedPlugin.plugin.id, "state").pipe(Effect.mapError((validationError) => new PluginRuntimeError({
227
+ pluginId: initializedPlugin.plugin.id,
228
+ operation: "validate-state",
229
+ cause: validationError.zodError,
230
+ retryable: false,
231
+ })));
232
+ // Create and return the stream
233
+ return createSourceStream(initializedPlugin, executePluginImpl, input, options);
234
+ }),
235
+ shutdownPlugin: (plugin) => Effect.gen(function* () {
236
+ const pluginLayer = Layer.succeed(PluginLoggerTag, logger);
237
+ // Remove from active plugins
238
+ activePlugins.delete(plugin);
239
+ // Try to invalidate from cache if it exists there
240
+ const configHash = generateConfigHash(plugin.config);
241
+ const cacheKey = `${plugin.plugin.id}:${configHash}`;
242
+ yield* cache.invalidate(cacheKey).pipe(Effect.catchAll(() => Effect.void) // Ignore if not in cache
243
+ );
244
+ // Clean up config hash
245
+ configByHash.delete(configHash);
246
+ // Shutdown the plugin
247
+ yield* plugin.plugin.shutdown().pipe(Effect.mapError((error) => new PluginRuntimeError({
248
+ pluginId: plugin.plugin.id,
249
+ operation: "shutdown-plugin",
250
+ cause: error,
251
+ retryable: false,
252
+ })), Effect.provide(pluginLayer));
253
+ }),
254
+ clearCache: () => Effect.sync(() => {
255
+ configByHash.clear();
256
+ }),
257
+ cleanup: () => Effect.gen(function* () {
258
+ const pluginLayer = Layer.succeed(PluginLoggerTag, logger);
259
+ // Call shutdown on all active plugins
260
+ for (const initializedPlugin of activePlugins) {
261
+ yield* initializedPlugin.plugin.shutdown().pipe(Effect.catchAll((error) => {
262
+ // Log shutdown errors but don't fail the cleanup
263
+ return logger.logError(`Failed to shutdown plugin ${initializedPlugin.plugin.id}`, error, { pluginId: initializedPlugin.plugin.id });
264
+ }), Effect.provide(pluginLayer));
265
+ }
266
+ // Clear the active plugins set
267
+ activePlugins.clear();
268
+ // Clear cache
269
+ configByHash.clear();
270
+ }),
271
+ };
272
+ }));
336
273
  }
337
274
  export const PluginServiceTag = PluginService;
338
275
  export const PluginServiceLive = PluginService.Live;