@zucker-framework/event 1.0.0 → 1.0.5

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/dist/index.d.mts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { ClassConstructor } from 'class-transformer';
2
2
  import { OnModuleInit, OnModuleDestroy, DynamicModule } from '@nestjs/common';
3
+ import { ReplaySubject, Observable } from 'rxjs';
4
+ import { EventEmitter } from 'node:events';
3
5
 
4
6
  declare enum EntityEventType {
5
7
  CREATE = "create",
@@ -810,4 +812,77 @@ declare class OperationHookRegistry {
810
812
  clear(): void;
811
813
  }
812
814
 
813
- export { ALL_EVENT_PHASES, type AsyncEvent, CLUSTER_TRANSPORT, CREATE_GROUP, type ClusterTransport, CompositeEventListener, CreatorEventListener, type CurrentUser, DefaultTopicEventBus, ENTITY_EVENT_CONFIG_KEY, ENTITY_EVENT_LISTENER_CUSTOMIZER, EVENT_STORE, EnableEntityEvent, type EntityEvent, type EntityEventConfig, type EntityEventListener, EntityEventListenerConfigure, type EntityEventListenerCustomizer, EntityEventPhase, EntityEventType, type EventHandler, type EventQueryOptions, type EventStore, type HookHandler, type HookPhase, type LegacyEventHandler, type OperationContext, OperationHookRegistry, SAVE_GROUP, SUBSCRIBE_KEY, SUBSCRIBE_METHODS_KEY, type StoredEvent, Subscribe, SubscribeFeature, type SubscribeOptions, type Subscription, SubscriptionBuilder, type SubscriptionInfo, type SubscriptionInfoBuilder, TOPIC_EVENT_BUS, type TopicEventBus, type TopicPayload, UPDATE_GROUP, UnifiedEventBus, ValidateEventListener, ZuckerEventModule, createAsyncEvent, createBatchEntityEvent, createCreatedEntityEvent, createDeletedEntityEvent, createEntityEvent, createModifyEntityEvent, createSavedEntityEvent, createSingleEntityEvent, createTopicPayload, getEntityEventConfig, getEntityEventName, getSubscriptions, isDoFireEvent, isEntityEventEnabled, matchesTopic, tryFireEvent, withFireEvent, withoutFireEvent };
815
+ interface ReplayEventBusOptions<TContext, TPayload> {
816
+ channel: (context: TContext) => string;
817
+ isTerminal: (payload: TPayload) => boolean;
818
+ channelTtlMs: number;
819
+ cleanupIntervalMs: number;
820
+ beforePublish?: (context: TContext, payload: TPayload) => void;
821
+ }
822
+ /**
823
+ * Process-local progress notification with replay(1). Terminal events reach
824
+ * existing subscribers before their channel completes and is removed. Stale
825
+ * channels expire only when unobserved; slow work cannot evict an active stream.
826
+ * This is reconstructable delivery state, not durable task state or a queue.
827
+ */
828
+ declare class ReplayEventBus<TContext, TPayload> {
829
+ protected readonly options: ReplayEventBusOptions<TContext, TPayload>;
830
+ protected readonly channels: Map<string, {
831
+ subject: ReplaySubject<TPayload>;
832
+ updatedAt: number;
833
+ }>;
834
+ protected closed: boolean;
835
+ private readonly cleanupTimer;
836
+ constructor(options: ReplayEventBusOptions<TContext, TPayload>);
837
+ publish(context: TContext, payload: TPayload): void;
838
+ stream(context: TContext): Observable<TPayload>;
839
+ protected channelStream(channel: string): Observable<TPayload>;
840
+ protected emitLocal(channel: string, payload: TPayload): void;
841
+ protected removeChannel(channel: string): void;
842
+ protected stop(): void;
843
+ onModuleDestroy(): Promise<void>;
844
+ private getOrCreateChannel;
845
+ private cleanupStaleChannels;
846
+ }
847
+
848
+ interface ReplayEventRedisClient extends Pick<EventEmitter, 'on' | 'off'> {
849
+ duplicate(): ReplayEventRedisClient;
850
+ publish(channel: string, message: string): Promise<unknown>;
851
+ subscribe(channel: string): Promise<unknown>;
852
+ unsubscribe(channel: string): Promise<unknown>;
853
+ quit(): Promise<unknown>;
854
+ }
855
+ interface RedisReplayEventBusOptions<TContext, TPayload> extends ReplayEventBusOptions<TContext, TPayload> {
856
+ /** The bus owns this connection and its duplicate, and closes each exactly once. */
857
+ createClient: () => ReplayEventRedisClient;
858
+ /** Validate the deserialized wire payload at the application boundary. */
859
+ parsePayload: (value: unknown) => TPayload;
860
+ logger?: {
861
+ error(message: string): void;
862
+ warn(message: string): void;
863
+ };
864
+ }
865
+ /**
866
+ * Redis Pub/Sub transport with process-local replay(1). Notifications published
867
+ * before subscription are not persisted. Subscribe failures allow a later stream
868
+ * call to try again; the Redis client owns network reconnect/retry configuration.
869
+ */
870
+ declare class RedisReplayEventBus<TContext, TPayload> extends ReplayEventBus<TContext, TPayload> {
871
+ private readonly redisOptions;
872
+ private publisher?;
873
+ private subscriber?;
874
+ private readonly subscriptions;
875
+ private closePromise?;
876
+ private readonly onPublisherError;
877
+ private readonly onSubscriberError;
878
+ private readonly onMessage;
879
+ constructor(redisOptions: RedisReplayEventBusOptions<TContext, TPayload>);
880
+ onModuleInit(): void;
881
+ publish(context: TContext, payload: TPayload): void;
882
+ stream(context: TContext): Observable<TPayload>;
883
+ protected removeChannel(channel: string): void;
884
+ onModuleDestroy(): Promise<void>;
885
+ private ensureSubscribed;
886
+ }
887
+
888
+ export { ALL_EVENT_PHASES, type AsyncEvent, CLUSTER_TRANSPORT, CREATE_GROUP, type ClusterTransport, CompositeEventListener, CreatorEventListener, type CurrentUser, DefaultTopicEventBus, ENTITY_EVENT_CONFIG_KEY, ENTITY_EVENT_LISTENER_CUSTOMIZER, EVENT_STORE, EnableEntityEvent, type EntityEvent, type EntityEventConfig, type EntityEventListener, EntityEventListenerConfigure, type EntityEventListenerCustomizer, EntityEventPhase, EntityEventType, type EventHandler, type EventQueryOptions, type EventStore, type HookHandler, type HookPhase, type LegacyEventHandler, type OperationContext, OperationHookRegistry, RedisReplayEventBus, type RedisReplayEventBusOptions, ReplayEventBus, type ReplayEventBusOptions, type ReplayEventRedisClient, SAVE_GROUP, SUBSCRIBE_KEY, SUBSCRIBE_METHODS_KEY, type StoredEvent, Subscribe, SubscribeFeature, type SubscribeOptions, type Subscription, SubscriptionBuilder, type SubscriptionInfo, type SubscriptionInfoBuilder, TOPIC_EVENT_BUS, type TopicEventBus, type TopicPayload, UPDATE_GROUP, UnifiedEventBus, ValidateEventListener, ZuckerEventModule, createAsyncEvent, createBatchEntityEvent, createCreatedEntityEvent, createDeletedEntityEvent, createEntityEvent, createModifyEntityEvent, createSavedEntityEvent, createSingleEntityEvent, createTopicPayload, getEntityEventConfig, getEntityEventName, getSubscriptions, isDoFireEvent, isEntityEventEnabled, matchesTopic, tryFireEvent, withFireEvent, withoutFireEvent };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { ClassConstructor } from 'class-transformer';
2
2
  import { OnModuleInit, OnModuleDestroy, DynamicModule } from '@nestjs/common';
3
+ import { ReplaySubject, Observable } from 'rxjs';
4
+ import { EventEmitter } from 'node:events';
3
5
 
4
6
  declare enum EntityEventType {
5
7
  CREATE = "create",
@@ -810,4 +812,77 @@ declare class OperationHookRegistry {
810
812
  clear(): void;
811
813
  }
812
814
 
813
- export { ALL_EVENT_PHASES, type AsyncEvent, CLUSTER_TRANSPORT, CREATE_GROUP, type ClusterTransport, CompositeEventListener, CreatorEventListener, type CurrentUser, DefaultTopicEventBus, ENTITY_EVENT_CONFIG_KEY, ENTITY_EVENT_LISTENER_CUSTOMIZER, EVENT_STORE, EnableEntityEvent, type EntityEvent, type EntityEventConfig, type EntityEventListener, EntityEventListenerConfigure, type EntityEventListenerCustomizer, EntityEventPhase, EntityEventType, type EventHandler, type EventQueryOptions, type EventStore, type HookHandler, type HookPhase, type LegacyEventHandler, type OperationContext, OperationHookRegistry, SAVE_GROUP, SUBSCRIBE_KEY, SUBSCRIBE_METHODS_KEY, type StoredEvent, Subscribe, SubscribeFeature, type SubscribeOptions, type Subscription, SubscriptionBuilder, type SubscriptionInfo, type SubscriptionInfoBuilder, TOPIC_EVENT_BUS, type TopicEventBus, type TopicPayload, UPDATE_GROUP, UnifiedEventBus, ValidateEventListener, ZuckerEventModule, createAsyncEvent, createBatchEntityEvent, createCreatedEntityEvent, createDeletedEntityEvent, createEntityEvent, createModifyEntityEvent, createSavedEntityEvent, createSingleEntityEvent, createTopicPayload, getEntityEventConfig, getEntityEventName, getSubscriptions, isDoFireEvent, isEntityEventEnabled, matchesTopic, tryFireEvent, withFireEvent, withoutFireEvent };
815
+ interface ReplayEventBusOptions<TContext, TPayload> {
816
+ channel: (context: TContext) => string;
817
+ isTerminal: (payload: TPayload) => boolean;
818
+ channelTtlMs: number;
819
+ cleanupIntervalMs: number;
820
+ beforePublish?: (context: TContext, payload: TPayload) => void;
821
+ }
822
+ /**
823
+ * Process-local progress notification with replay(1). Terminal events reach
824
+ * existing subscribers before their channel completes and is removed. Stale
825
+ * channels expire only when unobserved; slow work cannot evict an active stream.
826
+ * This is reconstructable delivery state, not durable task state or a queue.
827
+ */
828
+ declare class ReplayEventBus<TContext, TPayload> {
829
+ protected readonly options: ReplayEventBusOptions<TContext, TPayload>;
830
+ protected readonly channels: Map<string, {
831
+ subject: ReplaySubject<TPayload>;
832
+ updatedAt: number;
833
+ }>;
834
+ protected closed: boolean;
835
+ private readonly cleanupTimer;
836
+ constructor(options: ReplayEventBusOptions<TContext, TPayload>);
837
+ publish(context: TContext, payload: TPayload): void;
838
+ stream(context: TContext): Observable<TPayload>;
839
+ protected channelStream(channel: string): Observable<TPayload>;
840
+ protected emitLocal(channel: string, payload: TPayload): void;
841
+ protected removeChannel(channel: string): void;
842
+ protected stop(): void;
843
+ onModuleDestroy(): Promise<void>;
844
+ private getOrCreateChannel;
845
+ private cleanupStaleChannels;
846
+ }
847
+
848
+ interface ReplayEventRedisClient extends Pick<EventEmitter, 'on' | 'off'> {
849
+ duplicate(): ReplayEventRedisClient;
850
+ publish(channel: string, message: string): Promise<unknown>;
851
+ subscribe(channel: string): Promise<unknown>;
852
+ unsubscribe(channel: string): Promise<unknown>;
853
+ quit(): Promise<unknown>;
854
+ }
855
+ interface RedisReplayEventBusOptions<TContext, TPayload> extends ReplayEventBusOptions<TContext, TPayload> {
856
+ /** The bus owns this connection and its duplicate, and closes each exactly once. */
857
+ createClient: () => ReplayEventRedisClient;
858
+ /** Validate the deserialized wire payload at the application boundary. */
859
+ parsePayload: (value: unknown) => TPayload;
860
+ logger?: {
861
+ error(message: string): void;
862
+ warn(message: string): void;
863
+ };
864
+ }
865
+ /**
866
+ * Redis Pub/Sub transport with process-local replay(1). Notifications published
867
+ * before subscription are not persisted. Subscribe failures allow a later stream
868
+ * call to try again; the Redis client owns network reconnect/retry configuration.
869
+ */
870
+ declare class RedisReplayEventBus<TContext, TPayload> extends ReplayEventBus<TContext, TPayload> {
871
+ private readonly redisOptions;
872
+ private publisher?;
873
+ private subscriber?;
874
+ private readonly subscriptions;
875
+ private closePromise?;
876
+ private readonly onPublisherError;
877
+ private readonly onSubscriberError;
878
+ private readonly onMessage;
879
+ constructor(redisOptions: RedisReplayEventBusOptions<TContext, TPayload>);
880
+ onModuleInit(): void;
881
+ publish(context: TContext, payload: TPayload): void;
882
+ stream(context: TContext): Observable<TPayload>;
883
+ protected removeChannel(channel: string): void;
884
+ onModuleDestroy(): Promise<void>;
885
+ private ensureSubscribed;
886
+ }
887
+
888
+ export { ALL_EVENT_PHASES, type AsyncEvent, CLUSTER_TRANSPORT, CREATE_GROUP, type ClusterTransport, CompositeEventListener, CreatorEventListener, type CurrentUser, DefaultTopicEventBus, ENTITY_EVENT_CONFIG_KEY, ENTITY_EVENT_LISTENER_CUSTOMIZER, EVENT_STORE, EnableEntityEvent, type EntityEvent, type EntityEventConfig, type EntityEventListener, EntityEventListenerConfigure, type EntityEventListenerCustomizer, EntityEventPhase, EntityEventType, type EventHandler, type EventQueryOptions, type EventStore, type HookHandler, type HookPhase, type LegacyEventHandler, type OperationContext, OperationHookRegistry, RedisReplayEventBus, type RedisReplayEventBusOptions, ReplayEventBus, type ReplayEventBusOptions, type ReplayEventRedisClient, SAVE_GROUP, SUBSCRIBE_KEY, SUBSCRIBE_METHODS_KEY, type StoredEvent, Subscribe, SubscribeFeature, type SubscribeOptions, type Subscription, SubscriptionBuilder, type SubscriptionInfo, type SubscriptionInfoBuilder, TOPIC_EVENT_BUS, type TopicEventBus, type TopicPayload, UPDATE_GROUP, UnifiedEventBus, ValidateEventListener, ZuckerEventModule, createAsyncEvent, createBatchEntityEvent, createCreatedEntityEvent, createDeletedEntityEvent, createEntityEvent, createModifyEntityEvent, createSavedEntityEvent, createSingleEntityEvent, createTopicPayload, getEntityEventConfig, getEntityEventName, getSubscriptions, isDoFireEvent, isEntityEventEnabled, matchesTopic, tryFireEvent, withFireEvent, withoutFireEvent };
package/dist/index.js CHANGED
@@ -35,6 +35,8 @@ __export(index_exports, {
35
35
  EntityEventPhase: () => EntityEventPhase,
36
36
  EntityEventType: () => EntityEventType,
37
37
  OperationHookRegistry: () => OperationHookRegistry,
38
+ RedisReplayEventBus: () => RedisReplayEventBus,
39
+ ReplayEventBus: () => ReplayEventBus,
38
40
  SAVE_GROUP: () => SAVE_GROUP,
39
41
  SUBSCRIBE_KEY: () => SUBSCRIBE_KEY,
40
42
  SUBSCRIBE_METHODS_KEY: () => SUBSCRIBE_METHODS_KEY,
@@ -1053,6 +1055,168 @@ var OperationHookRegistry = class _OperationHookRegistry {
1053
1055
  this.hooks.length = 0;
1054
1056
  }
1055
1057
  };
1058
+
1059
+ // src/replay-event-bus.ts
1060
+ var import_rxjs = require("rxjs");
1061
+ var ReplayEventBus = class {
1062
+ static {
1063
+ __name(this, "ReplayEventBus");
1064
+ }
1065
+ options;
1066
+ channels = /* @__PURE__ */ new Map();
1067
+ closed = false;
1068
+ cleanupTimer;
1069
+ constructor(options) {
1070
+ this.options = options;
1071
+ this.cleanupTimer = setInterval(() => this.cleanupStaleChannels(), options.cleanupIntervalMs);
1072
+ this.cleanupTimer.unref?.();
1073
+ }
1074
+ publish(context, payload) {
1075
+ if (this.closed) return;
1076
+ this.options.beforePublish?.(context, payload);
1077
+ this.emitLocal(this.options.channel(context), payload);
1078
+ }
1079
+ stream(context) {
1080
+ return this.channelStream(this.options.channel(context));
1081
+ }
1082
+ channelStream(channel) {
1083
+ if (this.closed) return new import_rxjs.Observable((subscriber) => subscriber.complete());
1084
+ return this.getOrCreateChannel(channel).subject.asObservable();
1085
+ }
1086
+ emitLocal(channel, payload) {
1087
+ if (this.closed) return;
1088
+ const entry = this.getOrCreateChannel(channel);
1089
+ entry.updatedAt = Date.now();
1090
+ entry.subject.next(payload);
1091
+ if (this.options.isTerminal(payload)) this.removeChannel(channel);
1092
+ }
1093
+ removeChannel(channel) {
1094
+ const entry = this.channels.get(channel);
1095
+ this.channels.delete(channel);
1096
+ entry?.subject.complete();
1097
+ }
1098
+ stop() {
1099
+ if (this.closed) return;
1100
+ this.closed = true;
1101
+ clearInterval(this.cleanupTimer);
1102
+ for (const entry of this.channels.values()) entry.subject.complete();
1103
+ this.channels.clear();
1104
+ }
1105
+ async onModuleDestroy() {
1106
+ this.stop();
1107
+ }
1108
+ getOrCreateChannel(channel) {
1109
+ let entry = this.channels.get(channel);
1110
+ if (!entry) {
1111
+ entry = {
1112
+ subject: new import_rxjs.ReplaySubject(1),
1113
+ updatedAt: Date.now()
1114
+ };
1115
+ this.channels.set(channel, entry);
1116
+ }
1117
+ return entry;
1118
+ }
1119
+ cleanupStaleChannels() {
1120
+ const now = Date.now();
1121
+ for (const [channel, entry] of this.channels) {
1122
+ if (now - entry.updatedAt > this.options.channelTtlMs && !entry.subject.observed) {
1123
+ this.removeChannel(channel);
1124
+ }
1125
+ }
1126
+ }
1127
+ };
1128
+
1129
+ // src/redis-replay-event-bus.ts
1130
+ var RedisReplayEventBus = class extends ReplayEventBus {
1131
+ static {
1132
+ __name(this, "RedisReplayEventBus");
1133
+ }
1134
+ redisOptions;
1135
+ publisher;
1136
+ subscriber;
1137
+ subscriptions = /* @__PURE__ */ new Map();
1138
+ closePromise;
1139
+ onPublisherError = /* @__PURE__ */ __name(() => this.redisOptions.logger?.error("Redis event publisher connection failed"), "onPublisherError");
1140
+ onSubscriberError = /* @__PURE__ */ __name(() => this.redisOptions.logger?.error("Redis event subscriber connection failed"), "onSubscriberError");
1141
+ onMessage = /* @__PURE__ */ __name((channel, message) => {
1142
+ if (this.closed || !this.subscriptions.has(channel) || !this.channels.has(channel)) return;
1143
+ try {
1144
+ const payload = this.redisOptions.parsePayload(JSON.parse(message));
1145
+ this.emitLocal(channel, payload);
1146
+ } catch {
1147
+ this.redisOptions.logger?.warn("Invalid Redis progress event");
1148
+ }
1149
+ }, "onMessage");
1150
+ constructor(redisOptions) {
1151
+ super(redisOptions), this.redisOptions = redisOptions;
1152
+ }
1153
+ onModuleInit() {
1154
+ if (this.closed || this.publisher) return;
1155
+ const publisher = this.redisOptions.createClient();
1156
+ publisher.on("error", this.onPublisherError);
1157
+ this.publisher = publisher;
1158
+ try {
1159
+ this.subscriber = publisher.duplicate();
1160
+ this.subscriber.on("error", this.onSubscriberError);
1161
+ this.subscriber.on("message", this.onMessage);
1162
+ for (const channel of this.channels.keys()) this.ensureSubscribed(channel);
1163
+ } catch (error) {
1164
+ void this.onModuleDestroy();
1165
+ throw error;
1166
+ }
1167
+ }
1168
+ publish(context, payload) {
1169
+ if (this.closed || !this.publisher) return;
1170
+ this.options.beforePublish?.(context, payload);
1171
+ const channel = this.options.channel(context);
1172
+ const message = JSON.stringify(payload);
1173
+ void this.publisher.publish(channel, message).catch(() => {
1174
+ this.redisOptions.logger?.error("Redis event publish failed");
1175
+ });
1176
+ }
1177
+ stream(context) {
1178
+ const channel = this.options.channel(context);
1179
+ const stream = this.channelStream(channel);
1180
+ this.ensureSubscribed(channel);
1181
+ return stream;
1182
+ }
1183
+ removeChannel(channel) {
1184
+ const subscribed = this.subscriptions.delete(channel);
1185
+ if (subscribed && this.subscriber) {
1186
+ void this.subscriber.unsubscribe(channel).catch(() => {
1187
+ this.redisOptions.logger?.warn("Redis event unsubscribe failed");
1188
+ });
1189
+ }
1190
+ super.removeChannel(channel);
1191
+ }
1192
+ onModuleDestroy() {
1193
+ if (this.closePromise) return this.closePromise;
1194
+ this.stop();
1195
+ this.subscriptions.clear();
1196
+ this.subscriber?.off("message", this.onMessage);
1197
+ const publisher = this.publisher;
1198
+ const subscriber = this.subscriber;
1199
+ this.closePromise = Promise.allSettled([
1200
+ Promise.resolve().then(() => subscriber?.quit()),
1201
+ Promise.resolve().then(() => publisher?.quit())
1202
+ ]).then(() => {
1203
+ subscriber?.off("error", this.onSubscriberError);
1204
+ publisher?.off("error", this.onPublisherError);
1205
+ this.subscriber = void 0;
1206
+ this.publisher = void 0;
1207
+ });
1208
+ return this.closePromise;
1209
+ }
1210
+ ensureSubscribed(channel) {
1211
+ if (this.closed || !this.subscriber || this.subscriptions.has(channel)) return;
1212
+ const token = {};
1213
+ this.subscriptions.set(channel, token);
1214
+ void this.subscriber.subscribe(channel).catch(() => {
1215
+ if (this.subscriptions.get(channel) === token) this.subscriptions.delete(channel);
1216
+ this.redisOptions.logger?.error("Redis event subscribe failed");
1217
+ });
1218
+ }
1219
+ };
1056
1220
  // Annotate the CommonJS export names for ESM import in node:
1057
1221
  0 && (module.exports = {
1058
1222
  ALL_EVENT_PHASES,
@@ -1069,6 +1233,8 @@ var OperationHookRegistry = class _OperationHookRegistry {
1069
1233
  EntityEventPhase,
1070
1234
  EntityEventType,
1071
1235
  OperationHookRegistry,
1236
+ RedisReplayEventBus,
1237
+ ReplayEventBus,
1072
1238
  SAVE_GROUP,
1073
1239
  SUBSCRIBE_KEY,
1074
1240
  SUBSCRIBE_METHODS_KEY,
package/dist/index.mjs CHANGED
@@ -987,6 +987,168 @@ var OperationHookRegistry = class _OperationHookRegistry {
987
987
  this.hooks.length = 0;
988
988
  }
989
989
  };
990
+
991
+ // src/replay-event-bus.ts
992
+ import { Observable, ReplaySubject } from "rxjs";
993
+ var ReplayEventBus = class {
994
+ static {
995
+ __name(this, "ReplayEventBus");
996
+ }
997
+ options;
998
+ channels = /* @__PURE__ */ new Map();
999
+ closed = false;
1000
+ cleanupTimer;
1001
+ constructor(options) {
1002
+ this.options = options;
1003
+ this.cleanupTimer = setInterval(() => this.cleanupStaleChannels(), options.cleanupIntervalMs);
1004
+ this.cleanupTimer.unref?.();
1005
+ }
1006
+ publish(context, payload) {
1007
+ if (this.closed) return;
1008
+ this.options.beforePublish?.(context, payload);
1009
+ this.emitLocal(this.options.channel(context), payload);
1010
+ }
1011
+ stream(context) {
1012
+ return this.channelStream(this.options.channel(context));
1013
+ }
1014
+ channelStream(channel) {
1015
+ if (this.closed) return new Observable((subscriber) => subscriber.complete());
1016
+ return this.getOrCreateChannel(channel).subject.asObservable();
1017
+ }
1018
+ emitLocal(channel, payload) {
1019
+ if (this.closed) return;
1020
+ const entry = this.getOrCreateChannel(channel);
1021
+ entry.updatedAt = Date.now();
1022
+ entry.subject.next(payload);
1023
+ if (this.options.isTerminal(payload)) this.removeChannel(channel);
1024
+ }
1025
+ removeChannel(channel) {
1026
+ const entry = this.channels.get(channel);
1027
+ this.channels.delete(channel);
1028
+ entry?.subject.complete();
1029
+ }
1030
+ stop() {
1031
+ if (this.closed) return;
1032
+ this.closed = true;
1033
+ clearInterval(this.cleanupTimer);
1034
+ for (const entry of this.channels.values()) entry.subject.complete();
1035
+ this.channels.clear();
1036
+ }
1037
+ async onModuleDestroy() {
1038
+ this.stop();
1039
+ }
1040
+ getOrCreateChannel(channel) {
1041
+ let entry = this.channels.get(channel);
1042
+ if (!entry) {
1043
+ entry = {
1044
+ subject: new ReplaySubject(1),
1045
+ updatedAt: Date.now()
1046
+ };
1047
+ this.channels.set(channel, entry);
1048
+ }
1049
+ return entry;
1050
+ }
1051
+ cleanupStaleChannels() {
1052
+ const now = Date.now();
1053
+ for (const [channel, entry] of this.channels) {
1054
+ if (now - entry.updatedAt > this.options.channelTtlMs && !entry.subject.observed) {
1055
+ this.removeChannel(channel);
1056
+ }
1057
+ }
1058
+ }
1059
+ };
1060
+
1061
+ // src/redis-replay-event-bus.ts
1062
+ var RedisReplayEventBus = class extends ReplayEventBus {
1063
+ static {
1064
+ __name(this, "RedisReplayEventBus");
1065
+ }
1066
+ redisOptions;
1067
+ publisher;
1068
+ subscriber;
1069
+ subscriptions = /* @__PURE__ */ new Map();
1070
+ closePromise;
1071
+ onPublisherError = /* @__PURE__ */ __name(() => this.redisOptions.logger?.error("Redis event publisher connection failed"), "onPublisherError");
1072
+ onSubscriberError = /* @__PURE__ */ __name(() => this.redisOptions.logger?.error("Redis event subscriber connection failed"), "onSubscriberError");
1073
+ onMessage = /* @__PURE__ */ __name((channel, message) => {
1074
+ if (this.closed || !this.subscriptions.has(channel) || !this.channels.has(channel)) return;
1075
+ try {
1076
+ const payload = this.redisOptions.parsePayload(JSON.parse(message));
1077
+ this.emitLocal(channel, payload);
1078
+ } catch {
1079
+ this.redisOptions.logger?.warn("Invalid Redis progress event");
1080
+ }
1081
+ }, "onMessage");
1082
+ constructor(redisOptions) {
1083
+ super(redisOptions), this.redisOptions = redisOptions;
1084
+ }
1085
+ onModuleInit() {
1086
+ if (this.closed || this.publisher) return;
1087
+ const publisher = this.redisOptions.createClient();
1088
+ publisher.on("error", this.onPublisherError);
1089
+ this.publisher = publisher;
1090
+ try {
1091
+ this.subscriber = publisher.duplicate();
1092
+ this.subscriber.on("error", this.onSubscriberError);
1093
+ this.subscriber.on("message", this.onMessage);
1094
+ for (const channel of this.channels.keys()) this.ensureSubscribed(channel);
1095
+ } catch (error) {
1096
+ void this.onModuleDestroy();
1097
+ throw error;
1098
+ }
1099
+ }
1100
+ publish(context, payload) {
1101
+ if (this.closed || !this.publisher) return;
1102
+ this.options.beforePublish?.(context, payload);
1103
+ const channel = this.options.channel(context);
1104
+ const message = JSON.stringify(payload);
1105
+ void this.publisher.publish(channel, message).catch(() => {
1106
+ this.redisOptions.logger?.error("Redis event publish failed");
1107
+ });
1108
+ }
1109
+ stream(context) {
1110
+ const channel = this.options.channel(context);
1111
+ const stream = this.channelStream(channel);
1112
+ this.ensureSubscribed(channel);
1113
+ return stream;
1114
+ }
1115
+ removeChannel(channel) {
1116
+ const subscribed = this.subscriptions.delete(channel);
1117
+ if (subscribed && this.subscriber) {
1118
+ void this.subscriber.unsubscribe(channel).catch(() => {
1119
+ this.redisOptions.logger?.warn("Redis event unsubscribe failed");
1120
+ });
1121
+ }
1122
+ super.removeChannel(channel);
1123
+ }
1124
+ onModuleDestroy() {
1125
+ if (this.closePromise) return this.closePromise;
1126
+ this.stop();
1127
+ this.subscriptions.clear();
1128
+ this.subscriber?.off("message", this.onMessage);
1129
+ const publisher = this.publisher;
1130
+ const subscriber = this.subscriber;
1131
+ this.closePromise = Promise.allSettled([
1132
+ Promise.resolve().then(() => subscriber?.quit()),
1133
+ Promise.resolve().then(() => publisher?.quit())
1134
+ ]).then(() => {
1135
+ subscriber?.off("error", this.onSubscriberError);
1136
+ publisher?.off("error", this.onPublisherError);
1137
+ this.subscriber = void 0;
1138
+ this.publisher = void 0;
1139
+ });
1140
+ return this.closePromise;
1141
+ }
1142
+ ensureSubscribed(channel) {
1143
+ if (this.closed || !this.subscriber || this.subscriptions.has(channel)) return;
1144
+ const token = {};
1145
+ this.subscriptions.set(channel, token);
1146
+ void this.subscriber.subscribe(channel).catch(() => {
1147
+ if (this.subscriptions.get(channel) === token) this.subscriptions.delete(channel);
1148
+ this.redisOptions.logger?.error("Redis event subscribe failed");
1149
+ });
1150
+ }
1151
+ };
990
1152
  export {
991
1153
  ALL_EVENT_PHASES,
992
1154
  CLUSTER_TRANSPORT,
@@ -1002,6 +1164,8 @@ export {
1002
1164
  EntityEventPhase,
1003
1165
  EntityEventType,
1004
1166
  OperationHookRegistry,
1167
+ RedisReplayEventBus,
1168
+ ReplayEventBus,
1005
1169
  SAVE_GROUP,
1006
1170
  SUBSCRIBE_KEY,
1007
1171
  SUBSCRIBE_METHODS_KEY,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zucker-framework/event",
3
- "version": "1.0.0",
3
+ "version": "1.0.5",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -23,7 +23,7 @@
23
23
  "THIRD_PARTY_NOTICES"
24
24
  ],
25
25
  "dependencies": {
26
- "@zucker-framework/core": "1.0.0",
26
+ "@zucker-framework/core": "1.0.5",
27
27
  "eventemitter2": "^6.4.9"
28
28
  },
29
29
  "peerDependencies": {
@@ -31,12 +31,14 @@
31
31
  "@nestjs/core": "^12.0.0",
32
32
  "@nestjs/event-emitter": "^12.0.0",
33
33
  "class-transformer": "^0.5.0",
34
- "class-validator": "^0.14.0 || ^0.15.0"
34
+ "class-validator": "^0.14.0 || ^0.15.0",
35
+ "rxjs": "^7.8.2"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@types/node": "^20.19.43",
38
39
  "class-transformer": "^0.5.1",
39
- "class-validator": "^0.15.1"
40
+ "class-validator": "^0.15.1",
41
+ "rxjs": "^7.8.2"
40
42
  },
41
43
  "license": "Apache-2.0",
42
44
  "author": "Lind",
@@ -50,7 +52,7 @@
50
52
  "registry": "https://registry.npmjs.org/"
51
53
  },
52
54
  "scripts": {
53
- "build": "tsup src/index.ts --format cjs,esm --dts --external @nestjs/common --external @nestjs/core --external @nestjs/event-emitter --external class-transformer --external class-validator --external @zucker-framework/core",
55
+ "build": "tsup src/index.ts --format cjs,esm --dts --external @nestjs/common --external @nestjs/core --external @nestjs/event-emitter --external class-transformer --external class-validator --external @zucker-framework/core --external rxjs",
54
56
  "clean": "rm -rf dist",
55
57
  "lint": "eslint src/"
56
58
  }