opinionated-machine 11.1.0 → 11.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # opinionated-machine
2
2
 
3
+ ## 11.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9637451: Add `SSERoomEventPublisher`: fire-and-forget room broadcasting for domain code. It validates a payload against the event's own schema before broadcasting, and broadcasts the parsed value so schema defaults reach the wire. `publish` throws on a payload that violates its schema, since nobody receives that event and a producer should not believe otherwise; `safePublish` returns `Either<InternalError, true>` instead, for a caller that cannot absorb a throw. A failed broadcast is logged by both, as it happens after the call returns. Accepts the caller's context (anything with a `logger`, such as a `RequestContext`) so failures carry a correlation id.
8
+
3
9
  ## 11.1.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -56,6 +56,7 @@ Very opinionated DI framework for fastify, built on top of awilix
56
56
  - [Session Room Operations](#session-room-operations)
57
57
  - [Broadcasting to Rooms](#broadcasting-to-rooms)
58
58
  - [Room Broadcaster (Decoupled Broadcasting)](#room-broadcaster-decoupled-broadcasting)
59
+ - [Room Event Publisher (Fire-and-Forget)](#room-event-publisher-fire-and-forget)
59
60
  - [Room Name Helpers](#room-name-helpers)
60
61
  - [Room Query Methods](#room-query-methods)
61
62
  - [Auto-Leave on Disconnect](#auto-leave-on-disconnect)
@@ -2026,6 +2027,103 @@ class MetricsService {
2026
2027
 
2027
2028
  The broadcaster provides `broadcastToRoom()` (with `defineEvent()`-based type safety), `broadcastMessage()` (raw SSEMessage), plus room query methods (`getConnectionsInRoom`, `getConnectionCountInRoom`). Multiple controllers register their `sendEvent` with the same broadcaster — the first to recognize a connection handles delivery.
2028
2029
 
2030
+ #### Room Event Publisher (Fire-and-Forget)
2031
+
2032
+ `broadcastToRoom()` returns a promise, and most producers of a room event have nothing to do with
2033
+ it. An event listener or message queue handler has already committed its primary work by the time
2034
+ it broadcasts: it cannot retry a dropped hint, has nowhere to report one, and awaiting the fan-out
2035
+ would tie its latency to the number of open connections. `SSERoomEventPublisher` is the broadcaster
2036
+ without the promise.
2037
+
2038
+ ```ts
2039
+ import { defineEvent, SSERoomEventPublisher } from 'opinionated-machine'
2040
+ import { z } from 'zod'
2041
+
2042
+ const metricsUpdateEvent = defineEvent(
2043
+ 'metricsUpdate',
2044
+ z.object({ cpu: z.number(), memory: z.number() }),
2045
+ )
2046
+
2047
+ // Register alongside the broadcaster it wraps; it expects 'sseRoomBroadcaster' and 'logger'
2048
+ // in the cradle, so the names must match exactly.
2049
+ class DashboardModule extends AbstractModule {
2050
+ resolveDependencies() {
2051
+ return {
2052
+ sseRoomManager: asValue(new SSERoomManager()),
2053
+ sseRoomBroadcaster: asSingletonClass(SSERoomBroadcaster),
2054
+ sseRoomEventPublisher: asSingletonClass(SSERoomEventPublisher),
2055
+ metricsService: asSingletonClass(MetricsService),
2056
+ }
2057
+ }
2058
+ }
2059
+
2060
+ class MetricsService {
2061
+ private publisher: SSERoomEventPublisher
2062
+
2063
+ constructor(deps: { sseRoomEventPublisher: SSERoomEventPublisher }) {
2064
+ this.publisher = deps.sseRoomEventPublisher
2065
+ }
2066
+
2067
+ onMetricsUpdate(
2068
+ dashboardId: string,
2069
+ metrics: { cpu: number; memory: number },
2070
+ requestContext: RequestContext,
2071
+ ) {
2072
+ // No await: a failure is logged, not returned. The context is passed whole; only its
2073
+ // logger is read, so a dropped event carries the correlation id of whatever produced it.
2074
+ this.publisher.publish(
2075
+ `dashboard:${dashboardId}`,
2076
+ metricsUpdateEvent,
2077
+ metrics,
2078
+ requestContext,
2079
+ )
2080
+ }
2081
+ }
2082
+ ```
2083
+
2084
+ The context parameter is typed as `SSELogContext` (`{ logger: SSELogger }`) rather than any
2085
+ concrete context type, so `@lokalise/fastify-extras`' `RequestContext` satisfies it structurally
2086
+ and this package needs no dependency on it. A job or consumer context of your own works the same
2087
+ way, and a caller that has none omits the argument and falls back to the injected logger.
2088
+
2089
+ Two things it does beyond hiding the promise:
2090
+
2091
+ - **Validates before broadcasting, and throws.** A payload that violates its own event schema is
2092
+ a bug in the producer, and nobody receives the event, so dropping it quietly means believing
2093
+ you published something you did not. Delivery-time validation cannot give you this: it runs
2094
+ once per connection, so it reports the mismatch once per open connection on every node, names
2095
+ the event but not the code that produced it, does not run at all when nobody has joined the
2096
+ room, and by then the call has long returned.
2097
+ - **Puts the parsed value on the wire,** so a schema default is filled in once here rather than
2098
+ left to every client. Delivery-time validation discards its own result and serializes what it
2099
+ was handed, so `broadcastToRoom()` sends the unparsed input.
2100
+
2101
+ #### `publish` vs `safePublish`
2102
+
2103
+ They differ in one thing: what a malformed payload does.
2104
+
2105
+ | | malformed payload | failed broadcast |
2106
+ | --- | --- | --- |
2107
+ | `publish` | throws `InternalError` | logged |
2108
+ | `safePublish` | returns `{ error }`, and logs | logged |
2109
+
2110
+ Reach for `safePublish` in a producer that cannot absorb a throw: a message handler whose primary
2111
+ work has already committed would be retried in full and redo it, and the retry cannot succeed
2112
+ anyway, since a malformed payload fails the same way every time. Prefer `publish` everywhere
2113
+ else.
2114
+
2115
+ ```ts
2116
+ const outcome = this.publisher.safePublish(room, event, payload, requestContext)
2117
+ if (outcome.error) {
2118
+ // decide for yourself: metric, Bugsnag, a compensating write
2119
+ }
2120
+ ```
2121
+
2122
+ `{ result: true }` means the payload was validated and handed to the broadcaster. That is
2123
+ acceptance, not delivery: the fan-out has not run yet, and neither method reports its outcome,
2124
+ because it happens after the call has returned. Use the broadcaster directly when the delivered
2125
+ count matters, or when a failed delivery is something the caller can act on.
2126
+
2029
2127
  #### Room Name Helpers
2030
2128
 
2031
2129
  Room names are plain strings (like Socket.IO), but `defineRoom()` adds type-safe resolvers that ensure consistent naming across controllers and domain services:
@@ -4,7 +4,7 @@ export { type BuildFastifySSERoutesReturnType, buildFastifyRoute, buildHandler,
4
4
  export { AbstractSSEController, type SSEControllerConfig, type SSEEventSender, type SSELogger, type SSEMessage, } from './AbstractSSEController.js';
5
5
  export { defineEvent, type SSEEventDefinition } from './defineEvent.js';
6
6
  export { type AsyncEventIdSequence, type CreateEventIdSequenceOptions, compareEventIds, createEventIdSequence, type EventIdSequence, formatEventId, MAX_EVENT_ID_COUNTER, } from './eventIds.js';
7
- export { defineRoom, InMemoryAdapter, type PreDeliveryFilter, type RoomBroadcastOptions, type RoomNameResolver, type SSERoomAdapter, SSERoomBroadcaster, SSERoomManager, type SSERoomManagerConfig, type SSERoomMessageHandler, type SSERoomOperations, } from './rooms/index.js';
7
+ export { defineRoom, InMemoryAdapter, type PreDeliveryFilter, type RoomBroadcastOptions, type RoomNameResolver, type SSELogContext, type SSERoomAdapter, SSERoomBroadcaster, SSERoomEventPublisher, type SSERoomEventPublisherDependencies, type SSERoomEventPublishOptions, SSERoomManager, type SSERoomManagerConfig, type SSERoomMessageHandler, type SSERoomOperations, } from './rooms/index.js';
8
8
  export { type SpiedSSESession, type SSESessionEvent, SSESessionSpy } from './SSESessionSpy.js';
9
9
  export { SSE_DIAGNOSTICS_HEADER, type SSEDiagnosticsScope, type SSESendFailure, } from './sseSendDiagnostics.js';
10
10
  export { defineEventMetadata, type ExtractMetadata, type FilterVerdict, type IncomingEvent, type MetadataGuard, type MetadataGuards, type PublishResult, type ResolverResult, SSESubscriptionManager, type SSESubscriptionManagerConfig, type SubscriptionContext, type SubscriptionPolicy, type SubscriptionResolver, } from './subscriptions/index.js';
@@ -8,7 +8,7 @@ export { AbstractSSEController, } from './AbstractSSEController.js';
8
8
  export { defineEvent } from './defineEvent.js';
9
9
  export { compareEventIds, createEventIdSequence, formatEventId, MAX_EVENT_ID_COUNTER, } from './eventIds.js';
10
10
  // Re-export room types and classes
11
- export { defineRoom, InMemoryAdapter, SSERoomBroadcaster, SSERoomManager, } from './rooms/index.js';
11
+ export { defineRoom, InMemoryAdapter, SSERoomBroadcaster, SSERoomEventPublisher, SSERoomManager, } from './rooms/index.js';
12
12
  export { SSESessionSpy } from './SSESessionSpy.js';
13
13
  export { SSE_DIAGNOSTICS_HEADER, } from './sseSendDiagnostics.js';
14
14
  // SSE Subscriptions
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/sse/index.ts"],"names":[],"mappings":"AAUA,6EAA6E;AAC7E,+EAA+E;AAC/E,4BAA4B;AAC5B,OAAO,EACL,qBAAqB,EAIrB,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,cAAc,GAIf,MAAM,iCAAiC,CAAA;AACxC,2CAA2C;AAC3C,OAAO,EAEL,iBAAiB,EACjB,YAAY,GAUb,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,qBAAqB,GAKtB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,WAAW,EAA2B,MAAM,kBAAkB,CAAA;AACvE,OAAO,EAGL,eAAe,EACf,qBAAqB,EAErB,aAAa,EACb,oBAAoB,GACrB,MAAM,eAAe,CAAA;AACtB,mCAAmC;AACnC,OAAO,EACL,UAAU,EACV,eAAe,EAKf,kBAAkB,EAClB,cAAc,GAIf,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAA8C,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAC9F,OAAO,EACL,sBAAsB,GAGvB,MAAM,yBAAyB,CAAA;AAChC,oBAAoB;AACpB,OAAO,EACL,mBAAmB,EAQnB,sBAAsB,GAKvB,MAAM,0BAA0B,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/sse/index.ts"],"names":[],"mappings":"AAUA,6EAA6E;AAC7E,+EAA+E;AAC/E,4BAA4B;AAC5B,OAAO,EACL,qBAAqB,EAIrB,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,cAAc,GAIf,MAAM,iCAAiC,CAAA;AACxC,2CAA2C;AAC3C,OAAO,EAEL,iBAAiB,EACjB,YAAY,GAUb,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,qBAAqB,GAKtB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,WAAW,EAA2B,MAAM,kBAAkB,CAAA;AACvE,OAAO,EAGL,eAAe,EACf,qBAAqB,EAErB,aAAa,EACb,oBAAoB,GACrB,MAAM,eAAe,CAAA;AACtB,mCAAmC;AACnC,OAAO,EACL,UAAU,EACV,eAAe,EAMf,kBAAkB,EAClB,qBAAqB,EAGrB,cAAc,GAIf,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAA8C,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAC9F,OAAO,EACL,sBAAsB,GAGvB,MAAM,yBAAyB,CAAA;AAChC,oBAAoB;AACpB,OAAO,EACL,mBAAmB,EAQnB,sBAAsB,GAKvB,MAAM,0BAA0B,CAAA"}
@@ -0,0 +1,121 @@
1
+ import { type Either, InternalError } from '@lokalise/node-core';
2
+ import type { z } from 'zod';
3
+ import type { SSEEventDefinition } from '../defineEvent.js';
4
+ import type { SSELogger } from '../sseTypes.js';
5
+ import type { SSERoomBroadcaster } from './SSERoomBroadcaster.js';
6
+ import type { RoomBroadcastOptions } from './types.js';
7
+ export type SSERoomEventPublisherDependencies = {
8
+ sseRoomBroadcaster: SSERoomBroadcaster;
9
+ logger: SSELogger;
10
+ };
11
+ /**
12
+ * Whatever the caller is working on behalf of, narrowed to the one thing the publisher needs
13
+ * from it. `@lokalise/fastify-extras`' `RequestContext` satisfies this structurally, as does a
14
+ * job or consumer context of your own, so neither this package nor its callers need an adapter.
15
+ */
16
+ export type SSELogContext = {
17
+ logger: SSELogger;
18
+ };
19
+ /**
20
+ * Everything {@link SSERoomBroadcaster.broadcastToRoom} accepts, so the publisher is not a lossy
21
+ * wrapper over it.
22
+ */
23
+ export type SSERoomEventPublishOptions = RoomBroadcastOptions & {
24
+ /**
25
+ * The SSE `id:` put on the wire. Defaults to a random UUID. Set it to a value a client can
26
+ * order, such as the sequence from `createEventIdSequence()`, when consumers deduplicate or
27
+ * resume by event id.
28
+ */
29
+ id?: string;
30
+ /** The SSE `retry:` hint, in milliseconds: how long a client waits before reconnecting. */
31
+ retry?: number;
32
+ /**
33
+ * Per-broadcast context handed to the pre-delivery filter, and to other nodes alongside the
34
+ * message. Only a filter reads it (`SSESubscriptionManager` installs one to evaluate its
35
+ * resolver pipeline), so it is redundant unless one is installed.
36
+ */
37
+ metadata?: Record<string, unknown>;
38
+ };
39
+ /**
40
+ * Fire-and-forget broadcasting for domain code: a room event goes out and the caller does not
41
+ * await the fan-out.
42
+ *
43
+ * The typical producer is an event listener or a message handler whose primary work has already
44
+ * committed. It cannot retry a dropped hint, and awaiting the fan-out would tie its latency to
45
+ * the number of open connections. Code that needs the delivered count, or that can act on a
46
+ * delivery failure, should use {@link SSERoomBroadcaster} directly.
47
+ *
48
+ * The two ways publishing fails are treated differently, because they are different kinds of
49
+ * problem:
50
+ *
51
+ * - **A payload that violates its own event schema is a bug in the producer.** Nobody receives
52
+ * the event, so swallowing it means believing you published something you did not.
53
+ * {@link publish} throws. {@link safePublish} returns the error instead, for a caller that
54
+ * cannot absorb a throw.
55
+ * - **A failed broadcast is the world's problem**, and no caller here can retry it, so both
56
+ * methods log it. It also happens after the call has returned, which is why neither method can
57
+ * report it in a return value.
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * // In your DI module
62
+ * sseRoomEventPublisher: asSingletonClass(SSERoomEventPublisher),
63
+ *
64
+ * // In a listener. `requestContext` is passed straight through: the publisher reads its
65
+ * // logger, so a dropped event carries the correlation id of whatever produced it.
66
+ * this.publisher.publish(
67
+ * ProjectSse.roomResolver(projectId),
68
+ * ProjectSse.events.updated,
69
+ * payload,
70
+ * requestContext,
71
+ * )
72
+ * ```
73
+ */
74
+ export declare class SSERoomEventPublisher {
75
+ private readonly sseRoomBroadcaster;
76
+ private readonly logger;
77
+ constructor({ sseRoomBroadcaster, logger }: SSERoomEventPublisherDependencies);
78
+ /**
79
+ * Validate `data` against the event's own schema and broadcast it to `room`, throwing if it
80
+ * fails.
81
+ *
82
+ * Validating here rather than leaving it to delivery is what makes the throw possible at all.
83
+ * Delivery-time validation runs once per connection, so it reports a mismatch once per open
84
+ * connection on every node, names the event but not the code that produced it, and does not
85
+ * run at all when nobody has joined the room. By then the call has long returned and there is
86
+ * nothing left to throw to.
87
+ *
88
+ * What goes on the wire is the parsed value, so a schema default is filled in once here
89
+ * instead of being left to every client. Delivery-time validation discards its own result and
90
+ * serializes what it was handed, so calling {@link SSERoomBroadcaster.broadcastToRoom}
91
+ * directly sends the unparsed input.
92
+ *
93
+ * `context` is whatever the caller is acting on behalf of; only its logger is read, so a
94
+ * request context goes in as-is. Omit it in a caller that has none and the injected logger is
95
+ * used, which costs a logged failure its correlation id and nothing else.
96
+ *
97
+ * @throws {InternalError} if `data` does not satisfy the event's schema.
98
+ */
99
+ publish<T extends z.ZodType>(room: string | string[], event: SSEEventDefinition<string, T>, data: z.input<T>, context?: SSELogContext, options?: SSERoomEventPublishOptions): void;
100
+ /**
101
+ * {@link publish}, but a payload that fails its schema comes back as an error instead of being
102
+ * thrown.
103
+ *
104
+ * For a producer that cannot absorb a throw: a message handler whose primary work has already
105
+ * committed would be retried in full and redo it, and the retry cannot succeed anyway, since a
106
+ * malformed payload fails the same way every time. Prefer {@link publish} everywhere else.
107
+ *
108
+ * The failure is logged as well as returned, so a caller that ignores the result still leaves
109
+ * a trace rather than silence.
110
+ *
111
+ * @returns `{ result: true }` once the payload has been validated and handed to the
112
+ * broadcaster. That is acceptance, not delivery: the fan-out has not happened yet, and its
113
+ * own failure is logged rather than returned.
114
+ */
115
+ safePublish<T extends z.ZodType>(room: string | string[], event: SSEEventDefinition<string, T>, data: z.input<T>, context?: SSELogContext, options?: SSERoomEventPublishOptions): Either<InternalError, true>;
116
+ /**
117
+ * The shared tail of both methods. A failed broadcast is logged rather than surfaced: it
118
+ * happens after the caller has returned, and none of these callers could retry it anyway.
119
+ */
120
+ private broadcast;
121
+ }
@@ -0,0 +1,119 @@
1
+ import { InternalError } from '@lokalise/node-core';
2
+ /** The same error either way, so a caller switching between the two methods sees one shape. */
3
+ const validationError = (room, event, error) => new InternalError({
4
+ message: `SSE event validation failed for event "${event}": ${error.message}`,
5
+ errorCode: 'SSE_EVENT_VALIDATION_FAILED',
6
+ details: { room: Array.isArray(room) ? room.join(',') : room, event },
7
+ });
8
+ /**
9
+ * Fire-and-forget broadcasting for domain code: a room event goes out and the caller does not
10
+ * await the fan-out.
11
+ *
12
+ * The typical producer is an event listener or a message handler whose primary work has already
13
+ * committed. It cannot retry a dropped hint, and awaiting the fan-out would tie its latency to
14
+ * the number of open connections. Code that needs the delivered count, or that can act on a
15
+ * delivery failure, should use {@link SSERoomBroadcaster} directly.
16
+ *
17
+ * The two ways publishing fails are treated differently, because they are different kinds of
18
+ * problem:
19
+ *
20
+ * - **A payload that violates its own event schema is a bug in the producer.** Nobody receives
21
+ * the event, so swallowing it means believing you published something you did not.
22
+ * {@link publish} throws. {@link safePublish} returns the error instead, for a caller that
23
+ * cannot absorb a throw.
24
+ * - **A failed broadcast is the world's problem**, and no caller here can retry it, so both
25
+ * methods log it. It also happens after the call has returned, which is why neither method can
26
+ * report it in a return value.
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * // In your DI module
31
+ * sseRoomEventPublisher: asSingletonClass(SSERoomEventPublisher),
32
+ *
33
+ * // In a listener. `requestContext` is passed straight through: the publisher reads its
34
+ * // logger, so a dropped event carries the correlation id of whatever produced it.
35
+ * this.publisher.publish(
36
+ * ProjectSse.roomResolver(projectId),
37
+ * ProjectSse.events.updated,
38
+ * payload,
39
+ * requestContext,
40
+ * )
41
+ * ```
42
+ */
43
+ export class SSERoomEventPublisher {
44
+ sseRoomBroadcaster;
45
+ logger;
46
+ constructor({ sseRoomBroadcaster, logger }) {
47
+ this.sseRoomBroadcaster = sseRoomBroadcaster;
48
+ this.logger = logger;
49
+ }
50
+ /**
51
+ * Validate `data` against the event's own schema and broadcast it to `room`, throwing if it
52
+ * fails.
53
+ *
54
+ * Validating here rather than leaving it to delivery is what makes the throw possible at all.
55
+ * Delivery-time validation runs once per connection, so it reports a mismatch once per open
56
+ * connection on every node, names the event but not the code that produced it, and does not
57
+ * run at all when nobody has joined the room. By then the call has long returned and there is
58
+ * nothing left to throw to.
59
+ *
60
+ * What goes on the wire is the parsed value, so a schema default is filled in once here
61
+ * instead of being left to every client. Delivery-time validation discards its own result and
62
+ * serializes what it was handed, so calling {@link SSERoomBroadcaster.broadcastToRoom}
63
+ * directly sends the unparsed input.
64
+ *
65
+ * `context` is whatever the caller is acting on behalf of; only its logger is read, so a
66
+ * request context goes in as-is. Omit it in a caller that has none and the injected logger is
67
+ * used, which costs a logged failure its correlation id and nothing else.
68
+ *
69
+ * @throws {InternalError} if `data` does not satisfy the event's schema.
70
+ */
71
+ publish(room, event, data, context, options) {
72
+ const validation = event.schema.safeParse(data);
73
+ if (!validation.success) {
74
+ throw validationError(room, event.event, validation.error);
75
+ }
76
+ this.broadcast(room, event, validation.data, context, options);
77
+ }
78
+ /**
79
+ * {@link publish}, but a payload that fails its schema comes back as an error instead of being
80
+ * thrown.
81
+ *
82
+ * For a producer that cannot absorb a throw: a message handler whose primary work has already
83
+ * committed would be retried in full and redo it, and the retry cannot succeed anyway, since a
84
+ * malformed payload fails the same way every time. Prefer {@link publish} everywhere else.
85
+ *
86
+ * The failure is logged as well as returned, so a caller that ignores the result still leaves
87
+ * a trace rather than silence.
88
+ *
89
+ * @returns `{ result: true }` once the payload has been validated and handed to the
90
+ * broadcaster. That is acceptance, not delivery: the fan-out has not happened yet, and its
91
+ * own failure is logged rather than returned.
92
+ */
93
+ safePublish(room, event, data, context, options) {
94
+ const validation = event.schema.safeParse(data);
95
+ if (!validation.success) {
96
+ const logger = context?.logger ?? this.logger;
97
+ logger.error({ room, event: event.event, issues: validation.error.issues }, 'Refusing to broadcast an SSE event that fails its own schema');
98
+ return { error: validationError(room, event.event, validation.error) };
99
+ }
100
+ this.broadcast(room, event, validation.data, context, options);
101
+ return { result: true };
102
+ }
103
+ /**
104
+ * The shared tail of both methods. A failed broadcast is logged rather than surfaced: it
105
+ * happens after the caller has returned, and none of these callers could retry it anyway.
106
+ */
107
+ broadcast(room, event,
108
+ // Already schema-valid, so the re-parse at delivery accepts it. The cast below is only
109
+ // needed because `z.output` is not `z.input` for a schema that defaults or transforms.
110
+ parsed, context, options) {
111
+ const logger = context?.logger ?? this.logger;
112
+ this.sseRoomBroadcaster
113
+ .broadcastToRoom(room, event, parsed, options)
114
+ .catch((error) => {
115
+ logger.error({ error, room, event: event.event }, 'Failed to broadcast SSE event');
116
+ });
117
+ }
118
+ }
119
+ //# sourceMappingURL=SSERoomEventPublisher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SSERoomEventPublisher.js","sourceRoot":"","sources":["../../../../lib/sse/rooms/SSERoomEventPublisher.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  export { InMemoryAdapter } from './adapters/InMemoryAdapter.js';
2
2
  export { defineRoom } from './defineRoom.js';
3
3
  export { SSERoomBroadcaster } from './SSERoomBroadcaster.js';
4
+ export { type SSELogContext, SSERoomEventPublisher, type SSERoomEventPublisherDependencies, type SSERoomEventPublishOptions, } from './SSERoomEventPublisher.js';
4
5
  export { SSERoomManager } from './SSERoomManager.js';
5
6
  export type { PreDeliveryFilter, RoomBroadcastOptions, RoomNameResolver, SSERoomAdapter, SSERoomManagerConfig, SSERoomMessageHandler, SSERoomOperations, } from './types.js';
@@ -3,5 +3,6 @@
3
3
  export { InMemoryAdapter } from './adapters/InMemoryAdapter.js';
4
4
  export { defineRoom } from './defineRoom.js';
5
5
  export { SSERoomBroadcaster } from './SSERoomBroadcaster.js';
6
+ export { SSERoomEventPublisher, } from './SSERoomEventPublisher.js';
6
7
  export { SSERoomManager } from './SSERoomManager.js';
7
8
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../lib/sse/rooms/index.ts"],"names":[],"mappings":"AAAA,oBAAoB;AAEpB,WAAW;AACX,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../lib/sse/rooms/index.ts"],"names":[],"mappings":"AAAA,oBAAoB;AAEpB,WAAW;AACX,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC5D,OAAO,EAEL,qBAAqB,GAGtB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opinionated-machine",
3
- "version": "11.1.0",
3
+ "version": "11.2.0",
4
4
  "description": "Very opinionated DI framework for fastify, built on top of awilix ",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,13 +48,13 @@
48
48
  "@types/node": "^22.19.7",
49
49
  "@vitest/coverage-v8": "^4.1.11",
50
50
  "awilix": "^13.0.0",
51
- "awilix-manager": "^7.0.0",
52
- "fastify": "^5.7.4",
51
+ "awilix-manager": "^7.0.1",
52
+ "fastify": "^5.12.4",
53
53
  "fastify-type-provider-zod": "^7.0.0",
54
54
  "rimraf": "^6.1.2",
55
55
  "typescript": "^5.9.3",
56
56
  "vitest": "^4.1.11",
57
- "zod": "^4.5.4",
57
+ "zod": "^4.6.2",
58
58
  "@opinionated-machine/sse-fallback": "0.1.1"
59
59
  },
60
60
  "private": false,