| @@ -86,6 +86,18 @@ class PluginResolutionError extends Schema.TaggedErrorClass<PluginResolutionErro · 85 unmodified lines |
| 86 | } | 86 | } |
| 87 | } | 87 | } |
| 88 | | 88 | |
| | 89 | class PluginActivationContextExpiredError extends Schema.TaggedErrorClass<PluginActivationContextExpiredError>()( |
| | 90 | "PluginActivationContextExpiredError", |
| | 91 | { |
| | 92 | method: Schema.Literals(["resolve", "register", "onDispose"]), |
| | 93 | pluginId: Schema.String, |
| | 94 | }, |
| | 95 | ) { |
| | 96 | override get message(): string { |
| | 97 | return `activation context for ${this.pluginId} is no longer active (${this.method})`; |
| | 98 | } |
| | 99 | } |
| | 100 | |
| 89 | class PluginCallbackError extends Schema.TaggedErrorClass<PluginCallbackError>()( | 101 | class PluginCallbackError extends Schema.TaggedErrorClass<PluginCallbackError>()( |
| 90 | "PluginCallbackError", | 102 | "PluginCallbackError", |
| 91 | { | 103 | { |
| @@ -148,6 +160,15 @@ class PluginRuntimeCleanupError extends Schema.TaggedErrorClass<PluginRuntimeCle · 56 unmodified lines |
| 148 | | 160 | |
| 149 | type PluginPlanningError = DuplicatePluginIdError | DuplicateCapabilityError | DependencyCycleError; | 161 | type PluginPlanningError = DuplicatePluginIdError | DuplicateCapabilityError | DependencyCycleError; |
| 150 | | 162 | |
| | 163 | const isDuplicatePluginIdError = Schema.is(DuplicatePluginIdError); |
| | 164 | const isDuplicateCapabilityError = Schema.is(DuplicateCapabilityError); |
| | 165 | const isDependencyCycleError = Schema.is(DependencyCycleError); |
| | 166 | |
| | 167 | const isPluginPlanningError = (error: unknown): error is PluginPlanningError => |
| | 168 | isDuplicatePluginIdError(error) || |
| | 169 | isDuplicateCapabilityError(error) || |
| | 170 | isDependencyCycleError(error); |
| | 171 | |
| 151 | type PluginReconcileError = | 172 | type PluginReconcileError = |
| 152 | | PluginPlanningError | 173 | | PluginPlanningError |
| 153 | | PluginCallbackError | 174 | | PluginCallbackError |
| @@ -446,21 +467,27 @@ export const createEffectScopeRuntime: PluginRuntimeFactory = (options = {}): Pl · 292 unmodified lines |
| 446 | callback: PluginCallback, | 467 | callback: PluginCallback, |
| 447 | pluginId: string, | 468 | pluginId: string, |
| 448 | invoke: () => Result | PromiseLike<Result>, | 469 | invoke: () => Result | PromiseLike<Result>, |
| | 470 | onSettled?: () => void, |
| 449 | ): Effect.Effect<Result, PluginCallbackError> => | 471 | ): Effect.Effect<Result, PluginCallbackError> => |
| 450 | Effect.tryPromise({ | 472 | Effect.tryPromise({ |
| 451 | try: async () => { | 473 | try: async () => { |
| 452 | const callbackState: PluginCallbackContext = { active: true, callback, pluginId }; | 474 | const callbackState: PluginCallbackContext = { active: true, callback, pluginId }; |
| | 475 | let settled = false; |
| | 476 | const settle = () => { |
| | 477 | if (settled) return; |
| | 478 | settled = true; |
| | 479 | callbackState.active = false; |
| | 480 | onSettled?.(); |
| | 481 | }; |
| 453 | try { | 482 | try { |
| 454 | const result = callbackContext.run(callbackState, invoke); | 483 | const result = callbackContext.run(callbackState, invoke); |
| 455 | if (typeof result === "object" && result !== null && "then" in result) { | 484 | if (typeof result === "object" && result !== null && "then" in result) { |
| 456 | return await Promise.resolve(result).finally(() => { | 485 | return await Promise.resolve(result).finally(settle); |
| 457 | callbackState.active = false; | | |
| 458 | }); | | |
| 459 | } | 486 | } |
| 460 | callbackState.active = false; | 487 | settle(); |
| 461 | return result; | 488 | return result; |
| 462 | } catch (error) { | 489 | } catch (error) { |
| 463 | callbackState.active = false; | 490 | settle(); |
| 464 | throw error; | 491 | throw error; |
| 465 | } | 492 | } |
| 466 | }, | 493 | }, |
| @@ -479,36 +506,42 @@ export const createEffectScopeRuntime: PluginRuntimeFactory = (options = {}): Pl · 12 unmodified lines |
| 479 | const finalizers: Array<() => void | Promise<void>> = []; | 506 | const finalizers: Array<() => void | Promise<void>> = []; |
| 480 | const plugin: LivePlugin = { definition, scope, contributions, cleanupErrors }; | 507 | const plugin: LivePlugin = { definition, scope, contributions, cleanupErrors }; |
| 481 | let activating = true; | 508 | let activating = true; |
| 482 | const assertActivating = () => { | 509 | const assertActivating = (method: "resolve" | "register" | "onDispose") => { |
| 483 | if (!activating) { | 510 | if (!activating) { |
| 484 | throw new Error(`activation context for ${definition.id} is no longer active`); | 511 | throw new PluginActivationContextExpiredError({ method, pluginId: definition.id }); |
| 485 | } | 512 | } |
| 486 | }; | 513 | }; |
| 487 | | 514 | |
| 488 | const context: PluginActivationContext = { | 515 | const context: PluginActivationContext = { |
| 489 | resolve: <Service>(capability: string): Service => { | 516 | resolve: <Service>(capability: string): Service => { |
| 490 | assertActivating(); | 517 | assertActivating("resolve"); |
| 491 | if (!capabilities.has(capability)) { | 518 | if (!capabilities.has(capability)) { |
| 492 | throw new PluginResolutionError({ capability, pluginId: definition.id }); | 519 | throw new PluginResolutionError({ capability, pluginId: definition.id }); |
| 493 | } | 520 | } |
| 494 | return capabilities.get(capability) as Service; | 521 | return capabilities.get(capability) as Service; |
| 495 | }, | 522 | }, |
| 496 | register: (slot, contribution) => { | 523 | register: (slot, contribution) => { |
| 497 | assertActivating(); | 524 | assertActivating("register"); |
| 498 | const values = contributions.get(slot) ?? []; | 525 | const values = contributions.get(slot) ?? []; |
| 499 | values.push(contribution); | 526 | values.push(contribution); |
| 500 | contributions.set(slot, values); | 527 | contributions.set(slot, values); |
| 501 | }, | 528 | }, |
| 502 | onDispose: (finalizer) => { | 529 | onDispose: (finalizer) => { |
| 503 | assertActivating(); | 530 | assertActivating("onDispose"); |
| 504 | finalizers.push(finalizer); | 531 | finalizers.push(finalizer); |
| 505 | }, | 532 | }, |
| 506 | }; | 533 | }; |
| 507 | | 534 | |
| 508 | const activationExit = yield* Effect.exit( | 535 | const activationExit = yield* Effect.exit( |
| 509 | invokePluginCallback("activate", definition.id, () => definition.activate(context)), | 536 | invokePluginCallback( |
| | 537 | "activate", |
| | 538 | definition.id, |
| | 539 | () => definition.activate(context), |
| | 540 | () => { |
| | 541 | activating = false; |
| | 542 | }, |
| | 543 | ), |
| 510 | ); | 544 | ); |
| 511 | activating = false; | | |
| 512 | for (const finalizer of finalizers) { | 545 | for (const finalizer of finalizers) { |
| 513 | const finalizerEffect = invokePluginCallback("finalizer", definition.id, finalizer).pipe( | 546 | const finalizerEffect = invokePluginCallback("finalizer", definition.id, finalizer).pipe( |
| 514 | Effect.catch((error) => | 547 | Effect.catch((error) => |
| @@ -538,7 +571,10 @@ export const createEffectScopeRuntime: PluginRuntimeFactory = (options = {}): Pl · 23 unmodified lines |
| 538 | | 571 | |
| 539 | const plan = yield* Effect.try({ | 572 | const plan = yield* Effect.try({ |
| 540 | try: () => planComposition(definitions), | 573 | try: () => planComposition(definitions), |
| 541 | catch: (error) => error as PluginPlanningError, | 574 | catch: (error) => { |
| | 575 | if (!isPluginPlanningError(error)) throw error; |
| | 576 | return error; |
| | 577 | }, |
| 542 | }); | 578 | }); |
| 543 | const affected = affectedPluginIds(current.plugins, plan.definitions); | 579 | const affected = affectedPluginIds(current.plugins, plan.definitions); |
| 544 | const currentById = new Map(current.plugins.map((plugin) => [plugin.definition.id, plugin])); | 580 | const currentById = new Map(current.plugins.map((plugin) => [plugin.definition.id, plugin])); |