@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 +76 -1
- package/dist/index.d.ts +76 -1
- package/dist/index.js +166 -0
- package/dist/index.mjs +164 -0
- package/package.json +7 -5
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|