@zudojs/plugins 1.2.0 → 1.3.0

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/README.md CHANGED
@@ -155,7 +155,31 @@ const manager = new PluginManager({
155
155
  });
156
156
  ```
157
157
 
158
- A throwing subscriber never fails the phase that emitted the event.
158
+ A throwing subscriber never fails the phase that emitted the event, and
159
+ neither does an async `emit` whose promise rejects: both are reported
160
+ (through `onError`, the logger, or a `ZudoPluginWarning`) instead of
161
+ propagating. Before 1.3 an async rejection escaped as an
162
+ `unhandledRejection`, which terminates Node by default, after `start()` had
163
+ already resolved.
164
+
165
+ ### Using an `@zudojs/events` bus
166
+
167
+ Both the manager's `events` option and `createPluginContext`'s `events`
168
+ accept an `EventBus` directly. It is adapted with `toPluginEvents`, so
169
+ `context.events` is still a `PluginEvents`: `emit(name, payload)` publishes
170
+ `{ type: name, payload }` (the bus normalises `plugin:started` to
171
+ `plugin.started`), `on(name, handler)` subscribes and hands the handler the
172
+ event's payload, and `off(name, handler)` cancels that subscription.
173
+
174
+ ```typescript
175
+ import { EventBus } from "@zudojs/events";
176
+
177
+ const bus = new EventBus();
178
+ bus.on("plugin.*", (event) => console.log(event.type, event.payload));
179
+
180
+ const manager = new PluginManager({ events: bus });
181
+ await manager.start(createPluginContext({ name: "host" }, { events: bus }));
182
+ ```
159
183
 
160
184
  ## Diagnostics
161
185
 
package/dist/index.d.ts CHANGED
@@ -55,11 +55,12 @@ export { LifecycleController } from "./pluginLifecycle/pluginLifecycle.core.js";
55
55
  export type { LifecycleControllerOptions } from "./pluginLifecycle/pluginLifecycle.core.js";
56
56
  export { PLUGIN_EVENTS, createPluginLifecycleEvent, } from "./pluginEvents/pluginEvent.core.js";
57
57
  export type { PluginLifecycleEvent } from "./pluginEvents/pluginEvent.core.js";
58
+ export { isPluginEventBus, toPluginEvents, } from "./pluginEvents/pluginEvent.bus.js";
58
59
  export { buildDiagnosticReport, createHealthyHealth, createDegradedHealth, createUnhealthyHealth, } from "./pluginDiagnostics/pluginDiagnostic.core.js";
59
60
  export type { PluginHealth, PluginHealthStatus, PluginDiagnostic, PluginDiagnosticReport, } from "./pluginDiagnostics/pluginDiagnostic.core.js";
60
61
  export { isValidTransition, VALID_STATE_TRANSITIONS, } from "./pluginTypes/pluginState.type.js";
61
62
  export { createPluginContext, createOwnedPluginContext, } from "./pluginIntegration/pluginContext.core.js";
62
63
  export type { CreatePluginContextOptions, OwnedPluginContext, } from "./pluginIntegration/pluginContext.core.js";
63
64
  export { PluginError, PluginRegistrationError, PluginAlreadyRegisteredError, PluginNotFoundError, PluginDependencyError, PluginDependencyCycleError, PluginInitializationError, PluginStartError, PluginStopError, PluginDisposeError, PluginTimeoutError, PluginStateError, createPluginError, isPluginError, } from "@zudojs/errors";
64
- export type { PluginState, PluginMetadata, PluginDependency, PluginContext, PluginContainer, PluginConfig, PluginLogger, PluginEvents, PluginDisposable, Plugin, PluginErrorOptions, } from "./pluginTypes/index.js";
65
+ export type { PluginState, PluginMetadata, PluginDependency, PluginContext, PluginContainer, PluginConfig, PluginLogger, PluginEvents, PluginEventBus, PluginEventSource, PluginDisposable, Plugin, PluginErrorOptions, } from "./pluginTypes/index.js";
65
66
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -49,6 +49,7 @@ export { parseVersion, compareVersions, satisfiesVersion, assertDependencyVersio
49
49
  export { PluginDependencyVersionError } from "./pluginDependencies/versionCheck.core.js";
50
50
  export { LifecycleController } from "./pluginLifecycle/pluginLifecycle.core.js";
51
51
  export { PLUGIN_EVENTS, createPluginLifecycleEvent, } from "./pluginEvents/pluginEvent.core.js";
52
+ export { isPluginEventBus, toPluginEvents, } from "./pluginEvents/pluginEvent.bus.js";
52
53
  export { buildDiagnosticReport, createHealthyHealth, createDegradedHealth, createUnhealthyHealth, } from "./pluginDiagnostics/pluginDiagnostic.core.js";
53
54
  export { isValidTransition, VALID_STATE_TRANSITIONS, } from "./pluginTypes/pluginState.type.js";
54
55
  export { createPluginContext, createOwnedPluginContext, } from "./pluginIntegration/pluginContext.core.js";
@@ -5,4 +5,6 @@
5
5
  */
6
6
  export type { PluginLifecycleEvent } from "./pluginEvent.core.js";
7
7
  export { PLUGIN_EVENTS, createPluginLifecycleEvent, } from "./pluginEvent.core.js";
8
+ export { isPluginEventBus, toPluginEvents } from "./pluginEvent.bus.js";
9
+ export { deliverPluginEvent } from "./pluginEvent.deliver.js";
8
10
  //# sourceMappingURL=index.d.ts.map
@@ -4,4 +4,6 @@
4
4
  * Plugin lifecycle event types and event names.
5
5
  */
6
6
  export { PLUGIN_EVENTS, createPluginLifecycleEvent, } from "./pluginEvent.core.js";
7
+ export { isPluginEventBus, toPluginEvents } from "./pluginEvent.bus.js";
8
+ export { deliverPluginEvent } from "./pluginEvent.deliver.js";
7
9
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,26 @@
1
+ import type { PluginEventBus, PluginEvents, PluginEventSource } from "../pluginTypes/pluginContext.type.js";
2
+ /**
3
+ * Whether an event source is an event bus (such as `@zudojs/events`'
4
+ * `EventBus`) rather than a {@link PluginEvents} sink.
5
+ *
6
+ * A bus publishes objects (`publishEvent({ type, payload })`); a sink
7
+ * emits a name and a payload. Calling a bus's `emit` with a bare name
8
+ * throws `InvalidEventError`, so the two must be told apart.
9
+ */
10
+ export declare function isPluginEventBus(source: PluginEventSource): source is PluginEventBus;
11
+ /**
12
+ * Adapts an event bus to the {@link PluginEvents} interface plugins use.
13
+ *
14
+ * - `emit(name, payload)` publishes `{ type: name, payload }` and returns
15
+ * the publish promise, so a rejection can be caught by the caller.
16
+ * - `on(name, handler)` subscribes on the bus and hands the handler the
17
+ * event's `payload` — the same value a sink's `emit` would pass.
18
+ * - `off(name, handler)` cancels that subscription.
19
+ *
20
+ * A {@link PluginEvents} sink is returned unchanged.
21
+ *
22
+ * @param source - An event bus or a plugin events sink.
23
+ * @returns A {@link PluginEvents} view of it.
24
+ */
25
+ export declare function toPluginEvents(source: PluginEventSource): PluginEvents;
26
+ //# sourceMappingURL=pluginEvent.bus.d.ts.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Whether an event source is an event bus (such as `@zudojs/events`'
3
+ * `EventBus`) rather than a {@link PluginEvents} sink.
4
+ *
5
+ * A bus publishes objects (`publishEvent({ type, payload })`); a sink
6
+ * emits a name and a payload. Calling a bus's `emit` with a bare name
7
+ * throws `InvalidEventError`, so the two must be told apart.
8
+ */
9
+ export function isPluginEventBus(source) {
10
+ return (typeof source.publishEvent === "function");
11
+ }
12
+ /**
13
+ * Adapts an event bus to the {@link PluginEvents} interface plugins use.
14
+ *
15
+ * - `emit(name, payload)` publishes `{ type: name, payload }` and returns
16
+ * the publish promise, so a rejection can be caught by the caller.
17
+ * - `on(name, handler)` subscribes on the bus and hands the handler the
18
+ * event's `payload` — the same value a sink's `emit` would pass.
19
+ * - `off(name, handler)` cancels that subscription.
20
+ *
21
+ * A {@link PluginEvents} sink is returned unchanged.
22
+ *
23
+ * @param source - An event bus or a plugin events sink.
24
+ * @returns A {@link PluginEvents} view of it.
25
+ */
26
+ export function toPluginEvents(source) {
27
+ if (!isPluginEventBus(source))
28
+ return source;
29
+ const bus = source;
30
+ const subscriptions = new Map();
31
+ return {
32
+ on(event, handler) {
33
+ const byHandler = subscriptions.get(event) ?? new Map();
34
+ subscriptions.set(event, byHandler);
35
+ if (byHandler.has(handler))
36
+ return;
37
+ byHandler.set(handler, bus.on(event, (published) => {
38
+ handler(published.payload);
39
+ }));
40
+ },
41
+ off(event, handler) {
42
+ const byHandler = subscriptions.get(event);
43
+ const subscription = byHandler?.get(handler);
44
+ if (!byHandler || !subscription)
45
+ return;
46
+ subscription.unsubscribe();
47
+ byHandler.delete(handler);
48
+ if (byHandler.size === 0)
49
+ subscriptions.delete(event);
50
+ },
51
+ emit(event, payload) {
52
+ return bus.publishEvent({ type: event, payload });
53
+ },
54
+ };
55
+ }
56
+ //# sourceMappingURL=pluginEvent.bus.js.map
@@ -0,0 +1,18 @@
1
+ import type { PluginEvents } from "../pluginTypes/pluginContext.type.js";
2
+ /**
3
+ * Emits an event and contains every way delivery can fail.
4
+ *
5
+ * A subscriber that throws synchronously, and an `emit` that returns a
6
+ * promise which later rejects, are both passed to `onFailure` and never
7
+ * propagate: a lifecycle phase must not fail because of a listener, and
8
+ * an unobserved rejection would otherwise surface as an
9
+ * `unhandledRejection` — which terminates Node by default — after the
10
+ * phase has already resolved.
11
+ *
12
+ * @param events - Where to emit.
13
+ * @param name - The event name.
14
+ * @param payload - The event payload.
15
+ * @param onFailure - Receives a synchronous throw or an async rejection.
16
+ */
17
+ export declare function deliverPluginEvent(events: PluginEvents, name: string, payload: unknown, onFailure: (error: unknown) => void): void;
18
+ //# sourceMappingURL=pluginEvent.deliver.d.ts.map
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Emits an event and contains every way delivery can fail.
3
+ *
4
+ * A subscriber that throws synchronously, and an `emit` that returns a
5
+ * promise which later rejects, are both passed to `onFailure` and never
6
+ * propagate: a lifecycle phase must not fail because of a listener, and
7
+ * an unobserved rejection would otherwise surface as an
8
+ * `unhandledRejection` — which terminates Node by default — after the
9
+ * phase has already resolved.
10
+ *
11
+ * @param events - Where to emit.
12
+ * @param name - The event name.
13
+ * @param payload - The event payload.
14
+ * @param onFailure - Receives a synchronous throw or an async rejection.
15
+ */
16
+ export function deliverPluginEvent(events, name, payload, onFailure) {
17
+ let result;
18
+ try {
19
+ result = events.emit(name, payload);
20
+ }
21
+ catch (error) {
22
+ onFailure(error);
23
+ return;
24
+ }
25
+ if (isThenable(result)) {
26
+ result.then(undefined, (error) => {
27
+ try {
28
+ onFailure(error);
29
+ }
30
+ catch {
31
+ // A throwing failure hook must not become an unhandled rejection.
32
+ }
33
+ });
34
+ }
35
+ }
36
+ function isThenable(value) {
37
+ return (value !== null &&
38
+ (typeof value === "object" || typeof value === "function") &&
39
+ typeof value.then === "function");
40
+ }
41
+ //# sourceMappingURL=pluginEvent.deliver.js.map
@@ -1,5 +1,5 @@
1
1
  import type { PluginMetadata } from "../pluginTypes/pluginMetadata.type.js";
2
- import type { PluginContext, PluginContainer, PluginConfig, PluginLogger, PluginEvents, PluginDisposable } from "../pluginTypes/pluginContext.type.js";
2
+ import type { PluginContext, PluginContainer, PluginConfig, PluginLogger, PluginEventSource, PluginDisposable } from "../pluginTypes/pluginContext.type.js";
3
3
  /**
4
4
  * Options for creating a plugin context.
5
5
  */
@@ -7,7 +7,11 @@ export interface CreatePluginContextOptions {
7
7
  readonly container?: PluginContainer;
8
8
  readonly config?: PluginConfig;
9
9
  readonly logger?: PluginLogger;
10
- readonly events?: PluginEvents;
10
+ /**
11
+ * Event sink, or an event bus such as `@zudojs/events`' `EventBus`,
12
+ * which is adapted so `context.events` is always a `PluginEvents`.
13
+ */
14
+ readonly events?: PluginEventSource;
11
15
  /**
12
16
  * Collection that receives everything the plugin registers for
13
17
  * cleanup. The plugin manager passes the registered plugin's own
@@ -1,3 +1,4 @@
1
+ import { toPluginEvents } from "../pluginEvents/pluginEvent.bus.js";
1
2
  /**
2
3
  * Creates a plugin context and returns it with its teardown handles.
3
4
  *
@@ -14,7 +15,7 @@ export function createOwnedPluginContext(plugin, options = {}) {
14
15
  container: options.container,
15
16
  config: options.config,
16
17
  logger: options.logger,
17
- events: options.events,
18
+ events: options.events ? toPluginEvents(options.events) : undefined,
18
19
  onDispose(handler) {
19
20
  disposables.push({ dispose: handler });
20
21
  },
@@ -3,25 +3,24 @@ import { PluginDisposeError, PluginStateError, PluginTimeoutError, } from "@zudo
3
3
  import { PLUGIN_EVENTS, createPluginLifecycleEvent, } from "../pluginEvents/pluginEvent.core.js";
4
4
  import { AbandonedHooks } from "./pluginLifecycle.abandoned.js";
5
5
  import { reportPluginFailure } from "./pluginLifecycle.report.js";
6
+ import { deliverPluginEvent } from "../pluginEvents/pluginEvent.deliver.js";
6
7
  /**
7
8
  * Emits a plugin lifecycle event if the context supports events.
8
9
  *
9
10
  * A throwing subscriber must not abort the lifecycle phase that emitted
10
- * the event, so delivery failures are contained here.
11
+ * the event, so delivery failures — a synchronous throw or an async
12
+ * `emit` that rejects — are contained and reported here.
11
13
  */
12
14
  function emitLifecycleEvent(context, eventName, plugin, state, previousState, error) {
13
15
  if (!context.events?.emit) {
14
16
  return;
15
17
  }
16
18
  const event = createPluginLifecycleEvent(plugin, state, previousState, error);
17
- try {
18
- context.events.emit(eventName, event);
19
- }
20
- catch (emitError) {
19
+ deliverPluginEvent(context.events, eventName, event, (emitError) => {
21
20
  queueMicrotask(() => {
22
21
  reportPluginFailure(context.logger, `Listener for "${eventName}" threw.`, emitError, { plugin: plugin.name, event: eventName });
23
22
  });
24
- }
23
+ });
25
24
  }
26
25
  /** Largest delay a timer can represent. */
27
26
  const MAX_TIMER_DELAY = 2_147_483_647;
@@ -1,5 +1,5 @@
1
1
  import type { Plugin } from "../pluginTypes/plugin.type.js";
2
- import type { PluginContext, PluginEvents } from "../pluginTypes/pluginContext.type.js";
2
+ import type { PluginContext, PluginEventSource } from "../pluginTypes/pluginContext.type.js";
3
3
  import type { PluginDiagnosticReport } from "../pluginDiagnostics/pluginDiagnostic.core.js";
4
4
  import type { PluginLogger } from "../pluginTypes/pluginContext.type.js";
5
5
  /**
@@ -39,8 +39,11 @@ export interface PluginManagerOptions {
39
39
  * lifecycle events emitted through `context.events` cannot cover it.
40
40
  * Without this, `PLUGIN_EVENTS.REGISTERED` was a name nothing ever
41
41
  * emitted.
42
+ *
43
+ * Accepts an event bus such as `@zudojs/events`' `EventBus` as well as a
44
+ * `PluginEvents` sink.
42
45
  */
43
- readonly events?: PluginEvents;
46
+ readonly events?: PluginEventSource;
44
47
  /**
45
48
  * Logger for teardown failures when `onError` is not supplied.
46
49
  * Defaults to the logger on the context passed to `start()`/`stop()`.
@@ -56,6 +59,8 @@ export declare class PluginManager {
56
59
  private readonly lifecycle;
57
60
  private readonly plugins;
58
61
  private readonly options;
62
+ /** `options.events`, adapted to the sink interface. */
63
+ private readonly events;
59
64
  /**
60
65
  * Dependency-first order from the last successful resolution.
61
66
  *
@@ -138,8 +143,9 @@ export declare class PluginManager {
138
143
  */
139
144
  private abortContext;
140
145
  /**
141
- * Emits `plugin:registered`, containing a throwing subscriber so a bad
142
- * listener cannot fail the registration that triggered it.
146
+ * Emits `plugin:registered`, containing a throwing subscriber — or a
147
+ * rejecting async `emit` — so a bad listener cannot fail the
148
+ * registration that triggered it.
143
149
  */
144
150
  private emitRegistered;
145
151
  private report;
@@ -1,3 +1,5 @@
1
+ import { toPluginEvents } from "../pluginEvents/pluginEvent.bus.js";
2
+ import { deliverPluginEvent } from "../pluginEvents/pluginEvent.deliver.js";
1
3
  import { PluginRegistryImpl } from "../pluginRegistry/pluginRegistry.core.js";
2
4
  import { DependencyResolver, assertResolutionValid, } from "../pluginDependencies/dependencyResolver.core.js";
3
5
  import { assertDependencyVersions } from "../pluginDependencies/versionCheck.core.js";
@@ -25,6 +27,8 @@ export class PluginManager {
25
27
  lifecycle;
26
28
  plugins = new Map();
27
29
  options;
30
+ /** `options.events`, adapted to the sink interface. */
31
+ events;
28
32
  /**
29
33
  * Dependency-first order from the last successful resolution.
30
34
  *
@@ -52,6 +56,7 @@ export class PluginManager {
52
56
  ? { hookTimeout: options.hookTimeout }
53
57
  : {});
54
58
  this.options = options;
59
+ this.events = options.events ? toPluginEvents(options.events) : undefined;
55
60
  }
56
61
  /**
57
62
  * Registers a plugin.
@@ -312,20 +317,15 @@ export class PluginManager {
312
317
  this.contexts.get(name)?.abort(reason);
313
318
  }
314
319
  /**
315
- * Emits `plugin:registered`, containing a throwing subscriber so a bad
316
- * listener cannot fail the registration that triggered it.
320
+ * Emits `plugin:registered`, containing a throwing subscriber — or a
321
+ * rejecting async `emit` — so a bad listener cannot fail the
322
+ * registration that triggered it.
317
323
  */
318
324
  emitRegistered(plugin) {
319
- const events = this.options.events;
320
- if (!events) {
325
+ if (!this.events) {
321
326
  return;
322
327
  }
323
- try {
324
- events.emit(PLUGIN_EVENTS.REGISTERED, createPluginLifecycleEvent(plugin.metadata, "registered"));
325
- }
326
- catch (error) {
327
- this.report(error, plugin.metadata.name);
328
- }
328
+ deliverPluginEvent(this.events, PLUGIN_EVENTS.REGISTERED, createPluginLifecycleEvent(plugin.metadata, "registered"), (error) => this.report(error, plugin.metadata.name));
329
329
  }
330
330
  report(error, pluginName) {
331
331
  if (this.options.onError) {
@@ -6,7 +6,7 @@
6
6
  export type { PluginState } from "./pluginState.type.js";
7
7
  export type { PluginMetadata } from "./pluginMetadata.type.js";
8
8
  export type { PluginDependency } from "./pluginDependency.type.js";
9
- export type { PluginContext, PluginContainer, PluginConfig, PluginLogger, PluginEvents, PluginDisposable, } from "./pluginContext.type.js";
9
+ export type { PluginContext, PluginContainer, PluginConfig, PluginLogger, PluginEvents, PluginEventBus, PluginEventSource, PluginDisposable, } from "./pluginContext.type.js";
10
10
  export type { Plugin } from "./plugin.type.js";
11
11
  export type { PluginErrorOptions } from "@zudojs/errors";
12
12
  //# sourceMappingURL=index.d.ts.map
@@ -21,12 +21,37 @@ export interface PluginLogger {
21
21
  }
22
22
  /**
23
23
  * Minimal events interface for plugin context.
24
+ *
25
+ * `emit` may return a promise. A rejection is contained and reported, just
26
+ * like a synchronous throw — it never fails the phase that emitted.
24
27
  */
25
28
  export interface PluginEvents {
26
29
  on(event: string, handler: (event: unknown) => void): void;
27
30
  off(event: string, handler: (event: unknown) => void): void;
28
- emit(event: string, payload: unknown): void;
31
+ emit(event: string, payload: unknown): void | PromiseLike<unknown>;
29
32
  }
33
+ /**
34
+ * The part of an event bus the plugin system uses — satisfied by
35
+ * `@zudojs/events`' `EventBus`, which can be passed wherever
36
+ * {@link PluginEventSource} is accepted.
37
+ */
38
+ export interface PluginEventBus {
39
+ publishEvent(input: {
40
+ readonly type: string;
41
+ readonly payload: unknown;
42
+ }): Promise<unknown>;
43
+ on(eventType: string, handler: (event: {
44
+ readonly payload: unknown;
45
+ }) => unknown): {
46
+ unsubscribe(): void;
47
+ };
48
+ }
49
+ /**
50
+ * What the plugin system accepts as an event source: a
51
+ * {@link PluginEvents} sink, or an event bus it adapts with
52
+ * `toPluginEvents`.
53
+ */
54
+ export type PluginEventSource = PluginEvents | PluginEventBus;
30
55
  /**
31
56
  * Something a plugin registers so the manager releases it on shutdown.
32
57
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/plugins",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Plugin system for extending Zudojs applications with modular capabilities.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -24,12 +24,13 @@
24
24
  "!dist/.tsbuildinfo"
25
25
  ],
26
26
  "dependencies": {
27
- "@zudojs/errors": "1.1.0"
27
+ "@zudojs/errors": "1.3.0"
28
28
  },
29
29
  "devDependencies": {
30
- "@types/node": "^26.4.1",
30
+ "@types/node": "^26.6.2",
31
+ "@zudojs/events": "1.3.0",
31
32
  "typescript": "7.0.2",
32
- "vitest": "^4.1.11"
33
+ "vitest": "^5.0.1"
33
34
  },
34
35
  "engines": {
35
36
  "node": ">=24.0.0"
@@ -42,7 +43,7 @@
42
43
  "plugins",
43
44
  "extensions"
44
45
  ],
45
- "homepage": "https://github.com/oyinlola-tech/zudo#readme",
46
+ "homepage": "https://zudojs.oyinlola.site/docs/packages-plugins",
46
47
  "bugs": {
47
48
  "url": "https://github.com/oyinlola-tech/zudo/issues"
48
49
  },