| @@ -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 | 101 | class PluginCallbackError extends Schema.TaggedErrorClass<PluginCallbackError>()( |
| 90 | 102 | "PluginCallbackError", |
| 91 | 103 | { |
| @@ -148,6 +160,15 @@ class PluginRuntimeCleanupError extends Schema.TaggedErrorClass<PluginRuntimeCle · 56 unmodified lines |
| 148 | 160 | |
| 149 | 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 | 172 | type PluginReconcileError = |
| 152 | 173 | | PluginPlanningError |
| 153 | 174 | | PluginCallbackError |
| @@ -446,21 +467,27 @@ export const createEffectScopeRuntime: PluginRuntimeFactory = (options = {}): Pl · 292 unmodified lines |
| 446 | 467 | callback: PluginCallback, |
| 447 | 468 | pluginId: string, |
| 448 | 469 | invoke: () => Result | PromiseLike<Result>, |
| 470 | + onSettled?: () => void, |
| 449 | 471 | ): Effect.Effect<Result, PluginCallbackError> => |
| 450 | 472 | Effect.tryPromise({ |
| 451 | 473 | try: async () => { |
| 452 | 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 | 482 | try { |
| 454 | 483 | const result = callbackContext.run(callbackState, invoke); |
| 455 | 484 | if (typeof result === "object" && result !== null && "then" in result) { |
| 456 | | − return await Promise.resolve(result).finally(() => { |
| 457 | | − callbackState.active = false; |
| 458 | | − }); |
| 485 | + return await Promise.resolve(result).finally(settle); |
| 459 | 486 | } |
| 460 | | − callbackState.active = false; |
| 487 | + settle(); |
| 461 | 488 | return result; |
| 462 | 489 | } catch (error) { |
| 463 | | − callbackState.active = false; |
| 490 | + settle(); |
| 464 | 491 | throw error; |
| 465 | 492 | } |
| 466 | 493 | }, |
| @@ -479,36 +506,42 @@ export const createEffectScopeRuntime: PluginRuntimeFactory = (options = {}): Pl · 12 unmodified lines |
| 479 | 506 | const finalizers: Array<() => void | Promise<void>> = []; |
| 480 | 507 | const plugin: LivePlugin = { definition, scope, contributions, cleanupErrors }; |
| 481 | 508 | let activating = true; |
| 482 | | − const assertActivating = () => { |
| 509 | + const assertActivating = (method: "resolve" | "register" | "onDispose") => { |
| 483 | 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 | 515 | const context: PluginActivationContext = { |
| 489 | 516 | resolve: <Service>(capability: string): Service => { |
| 490 | | − assertActivating(); |
| 517 | + assertActivating("resolve"); |
| 491 | 518 | if (!capabilities.has(capability)) { |
| 492 | 519 | throw new PluginResolutionError({ capability, pluginId: definition.id }); |
| 493 | 520 | } |
| 494 | 521 | return capabilities.get(capability) as Service; |
| 495 | 522 | }, |
| 496 | 523 | register: (slot, contribution) => { |
| 497 | | − assertActivating(); |
| 524 | + assertActivating("register"); |
| 498 | 525 | const values = contributions.get(slot) ?? []; |
| 499 | 526 | values.push(contribution); |
| 500 | 527 | contributions.set(slot, values); |
| 501 | 528 | }, |
| 502 | 529 | onDispose: (finalizer) => { |
| 503 | | − assertActivating(); |
| 530 | + assertActivating("onDispose"); |
| 504 | 531 | finalizers.push(finalizer); |
| 505 | 532 | }, |
| 506 | 533 | }; |
| 507 | 534 | |
| 508 | 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 | 545 | for (const finalizer of finalizers) { |
| 513 | 546 | const finalizerEffect = invokePluginCallback("finalizer", definition.id, finalizer).pipe( |
| 514 | 547 | Effect.catch((error) => |
| @@ -538,7 +571,10 @@ export const createEffectScopeRuntime: PluginRuntimeFactory = (options = {}): Pl · 23 unmodified lines |
| 538 | 571 | |
| 539 | 572 | const plan = yield* Effect.try({ |
| 540 | 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 | 579 | const affected = affectedPluginIds(current.plugins, plan.definitions); |
| 544 | 580 | const currentById = new Map(current.plugins.map((plugin) => [plugin.definition.id, plugin])); |