@hamak/event-channel 0.5.3

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.
Files changed (52) hide show
  1. package/dist/api/actions/event-channel-actions.d.ts +39 -0
  2. package/dist/api/actions/event-channel-actions.d.ts.map +1 -0
  3. package/dist/api/actions/event-channel-actions.js +29 -0
  4. package/dist/api/index.d.ts +10 -0
  5. package/dist/api/index.d.ts.map +1 -0
  6. package/dist/api/index.js +9 -0
  7. package/dist/api/tokens/service-tokens.d.ts +6 -0
  8. package/dist/api/tokens/service-tokens.d.ts.map +1 -0
  9. package/dist/api/tokens/service-tokens.js +5 -0
  10. package/dist/api/types/connection-types.d.ts +17 -0
  11. package/dist/api/types/connection-types.d.ts.map +1 -0
  12. package/dist/api/types/connection-types.js +4 -0
  13. package/dist/api/types/event-types.d.ts +64 -0
  14. package/dist/api/types/event-types.d.ts.map +1 -0
  15. package/dist/api/types/event-types.js +4 -0
  16. package/dist/impl/core/event-channel.d.ts +35 -0
  17. package/dist/impl/core/event-channel.d.ts.map +1 -0
  18. package/dist/impl/core/event-channel.js +177 -0
  19. package/dist/impl/index.d.ts +16 -0
  20. package/dist/impl/index.d.ts.map +1 -0
  21. package/dist/impl/index.js +19 -0
  22. package/dist/impl/middleware/event-channel-middleware.d.ts +14 -0
  23. package/dist/impl/middleware/event-channel-middleware.d.ts.map +1 -0
  24. package/dist/impl/middleware/event-channel-middleware.js +33 -0
  25. package/dist/impl/plugin/event-channel-plugin-factory.d.ts +27 -0
  26. package/dist/impl/plugin/event-channel-plugin-factory.d.ts.map +1 -0
  27. package/dist/impl/plugin/event-channel-plugin-factory.js +136 -0
  28. package/dist/impl/security/action-whitelist-filter.d.ts +15 -0
  29. package/dist/impl/security/action-whitelist-filter.d.ts.map +1 -0
  30. package/dist/impl/security/action-whitelist-filter.js +34 -0
  31. package/dist/impl/store/event-channel-reducer.d.ts +17 -0
  32. package/dist/impl/store/event-channel-reducer.d.ts.map +1 -0
  33. package/dist/impl/store/event-channel-reducer.js +38 -0
  34. package/dist/impl/transport/sse-transport.d.ts +25 -0
  35. package/dist/impl/transport/sse-transport.d.ts.map +1 -0
  36. package/dist/impl/transport/sse-transport.js +119 -0
  37. package/dist/index.d.ts +6 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +5 -0
  40. package/dist/spi/config/event-channel-plugin-config.d.ts +59 -0
  41. package/dist/spi/config/event-channel-plugin-config.d.ts.map +1 -0
  42. package/dist/spi/config/event-channel-plugin-config.js +4 -0
  43. package/dist/spi/index.d.ts +10 -0
  44. package/dist/spi/index.d.ts.map +1 -0
  45. package/dist/spi/index.js +7 -0
  46. package/dist/spi/security/i-action-filter.d.ts +12 -0
  47. package/dist/spi/security/i-action-filter.d.ts.map +1 -0
  48. package/dist/spi/security/i-action-filter.js +4 -0
  49. package/dist/spi/transport/i-event-transport.d.ts +20 -0
  50. package/dist/spi/transport/i-event-transport.d.ts.map +1 -0
  51. package/dist/spi/transport/i-event-transport.js +5 -0
  52. package/package.json +63 -0
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Redux action types and creators for the event channel.
3
+ */
4
+ import type { ConnectionInfo } from '../types/connection-types';
5
+ import type { EventChannelEvent, RemoteAction } from '../types/event-types';
6
+ export declare const EventChannelActionTypes: {
7
+ readonly CONNECTION_STATUS_CHANGED: "eventChannel/connectionStatusChanged";
8
+ readonly EVENT_RECEIVED: "eventChannel/eventReceived";
9
+ readonly REMOTE_ACTION_RECEIVED: "eventChannel/remoteActionReceived";
10
+ readonly CONNECT: "eventChannel/connect";
11
+ readonly DISCONNECT: "eventChannel/disconnect";
12
+ };
13
+ export interface ConnectionStatusChangedAction {
14
+ type: typeof EventChannelActionTypes.CONNECTION_STATUS_CHANGED;
15
+ payload: ConnectionInfo;
16
+ }
17
+ export interface EventReceivedAction {
18
+ type: typeof EventChannelActionTypes.EVENT_RECEIVED;
19
+ payload: EventChannelEvent;
20
+ }
21
+ export interface RemoteActionReceivedAction {
22
+ type: typeof EventChannelActionTypes.REMOTE_ACTION_RECEIVED;
23
+ payload: RemoteAction;
24
+ }
25
+ export interface ConnectAction {
26
+ type: typeof EventChannelActionTypes.CONNECT;
27
+ }
28
+ export interface DisconnectAction {
29
+ type: typeof EventChannelActionTypes.DISCONNECT;
30
+ }
31
+ export type EventChannelAction = ConnectionStatusChangedAction | EventReceivedAction | RemoteActionReceivedAction | ConnectAction | DisconnectAction;
32
+ export declare const eventChannelActions: {
33
+ connectionStatusChanged(info: ConnectionInfo): ConnectionStatusChangedAction;
34
+ eventReceived(event: EventChannelEvent): EventReceivedAction;
35
+ remoteActionReceived(action: RemoteAction): RemoteActionReceivedAction;
36
+ connect(): ConnectAction;
37
+ disconnect(): DisconnectAction;
38
+ };
39
+ //# sourceMappingURL=event-channel-actions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-channel-actions.d.ts","sourceRoot":"","sources":["../../../src/api/actions/event-channel-actions.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAG5E,eAAO,MAAM,uBAAuB;;;;;;CAM1B,CAAC;AAGX,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,OAAO,uBAAuB,CAAC,yBAAyB,CAAC;IAC/D,OAAO,EAAE,cAAc,CAAC;CACzB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,OAAO,uBAAuB,CAAC,cAAc,CAAC;IACpD,OAAO,EAAE,iBAAiB,CAAC;CAC5B;AAED,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,OAAO,uBAAuB,CAAC,sBAAsB,CAAC;IAC5D,OAAO,EAAE,YAAY,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,uBAAuB,CAAC,OAAO,CAAC;CAC9C;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,OAAO,uBAAuB,CAAC,UAAU,CAAC;CACjD;AAED,MAAM,MAAM,kBAAkB,GAC1B,6BAA6B,GAC7B,mBAAmB,GACnB,0BAA0B,GAC1B,aAAa,GACb,gBAAgB,CAAC;AAGrB,eAAO,MAAM,mBAAmB;kCACA,cAAc,GAAG,6BAA6B;yBAIvD,iBAAiB,GAAG,mBAAmB;iCAI/B,YAAY,GAAG,0BAA0B;eAI3D,aAAa;kBAIV,gBAAgB;CAG/B,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Redux action types and creators for the event channel.
3
+ */
4
+ // Action type constants
5
+ export const EventChannelActionTypes = {
6
+ CONNECTION_STATUS_CHANGED: 'eventChannel/connectionStatusChanged',
7
+ EVENT_RECEIVED: 'eventChannel/eventReceived',
8
+ REMOTE_ACTION_RECEIVED: 'eventChannel/remoteActionReceived',
9
+ CONNECT: 'eventChannel/connect',
10
+ DISCONNECT: 'eventChannel/disconnect',
11
+ };
12
+ // Action creators
13
+ export const eventChannelActions = {
14
+ connectionStatusChanged(info) {
15
+ return { type: EventChannelActionTypes.CONNECTION_STATUS_CHANGED, payload: info };
16
+ },
17
+ eventReceived(event) {
18
+ return { type: EventChannelActionTypes.EVENT_RECEIVED, payload: event };
19
+ },
20
+ remoteActionReceived(action) {
21
+ return { type: EventChannelActionTypes.REMOTE_ACTION_RECEIVED, payload: action };
22
+ },
23
+ connect() {
24
+ return { type: EventChannelActionTypes.CONNECT };
25
+ },
26
+ disconnect() {
27
+ return { type: EventChannelActionTypes.DISCONNECT };
28
+ },
29
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @hamak/event-channel API
3
+ *
4
+ * Public types, interfaces, tokens, and action creators.
5
+ */
6
+ export type { ConnectionStatus, ConnectionInfo, } from './types/connection-types';
7
+ export type { EventChannelEvent, RemoteAction, IEventChannel, } from './types/event-types';
8
+ export { EVENT_CHANNEL_TOKEN, EVENT_CHANNEL_CONFIG_TOKEN, } from './tokens/service-tokens';
9
+ export { EventChannelActionTypes, eventChannelActions, type EventChannelAction, type ConnectionStatusChangedAction, type EventReceivedAction, type RemoteActionReceivedAction, type ConnectAction, type DisconnectAction, } from './actions/event-channel-actions';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,YAAY,EACV,gBAAgB,EAChB,cAAc,GACf,MAAM,0BAA0B,CAAC;AAElC,YAAY,EACV,iBAAiB,EACjB,YAAY,EACZ,aAAa,GACd,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,mBAAmB,EACnB,0BAA0B,GAC3B,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,KAAK,kBAAkB,EACvB,KAAK,6BAA6B,EAClC,KAAK,mBAAmB,EACxB,KAAK,0BAA0B,EAC/B,KAAK,aAAa,EAClB,KAAK,gBAAgB,GACtB,MAAM,iCAAiC,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @hamak/event-channel API
3
+ *
4
+ * Public types, interfaces, tokens, and action creators.
5
+ */
6
+ // Tokens
7
+ export { EVENT_CHANNEL_TOKEN, EVENT_CHANNEL_CONFIG_TOKEN, } from './tokens/service-tokens';
8
+ // Actions
9
+ export { EventChannelActionTypes, eventChannelActions, } from './actions/event-channel-actions';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Dependency injection tokens for the event channel.
3
+ */
4
+ export declare const EVENT_CHANNEL_TOKEN: unique symbol;
5
+ export declare const EVENT_CHANNEL_CONFIG_TOKEN: unique symbol;
6
+ //# sourceMappingURL=service-tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-tokens.d.ts","sourceRoot":"","sources":["../../../src/api/tokens/service-tokens.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,eAAO,MAAM,mBAAmB,eAAkD,CAAC;AACnF,eAAO,MAAM,0BAA0B,eAA4C,CAAC"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Dependency injection tokens for the event channel.
3
+ */
4
+ export const EVENT_CHANNEL_TOKEN = Symbol.for('@hamak/event-channel:EventChannel');
5
+ export const EVENT_CHANNEL_CONFIG_TOKEN = Symbol.for('@hamak/event-channel:Config');
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Connection status and info types for the event channel.
3
+ */
4
+ export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error';
5
+ export interface ConnectionInfo {
6
+ /** Current connection status */
7
+ status: ConnectionStatus;
8
+ /** Number of reconnect attempts so far */
9
+ reconnectAttempts: number;
10
+ /** ISO timestamp of last successful connection */
11
+ lastConnectedAt?: string;
12
+ /** Error message if status is 'error' */
13
+ error?: string;
14
+ /** URL of the event stream */
15
+ url: string;
16
+ }
17
+ //# sourceMappingURL=connection-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-types.d.ts","sourceRoot":"","sources":["../../../src/api/types/connection-types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,MAAM,gBAAgB,GAAG,cAAc,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,OAAO,CAAC;AAEtG,MAAM,WAAW,cAAc;IAC7B,gCAAgC;IAChC,MAAM,EAAE,gBAAgB,CAAC;IACzB,0CAA0C;IAC1C,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kDAAkD;IAClD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8BAA8B;IAC9B,GAAG,EAAE,MAAM,CAAC;CACb"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Connection status and info types for the event channel.
3
+ */
4
+ export {};
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Core event channel types.
3
+ */
4
+ import type { ConnectionStatus } from './connection-types';
5
+ /**
6
+ * A typed event from the event channel.
7
+ */
8
+ export interface EventChannelEvent<T = unknown> {
9
+ /** Unique event ID (server-generated) */
10
+ id: string;
11
+ /** Event type discriminator */
12
+ type: string;
13
+ /** Event payload */
14
+ data: T;
15
+ /** ISO timestamp from server */
16
+ timestamp: string;
17
+ /** Source identifier (which backend service emitted this) */
18
+ source?: string;
19
+ }
20
+ /**
21
+ * A remote Redux action dispatched from the backend.
22
+ * Delivered as an EventChannelEvent with type='remote-action'.
23
+ */
24
+ export interface RemoteAction {
25
+ /** The Redux action type to dispatch */
26
+ actionType: string;
27
+ /** The action payload */
28
+ payload?: unknown;
29
+ /** Optional metadata */
30
+ meta?: {
31
+ source: string;
32
+ correlationId?: string;
33
+ timestamp: string;
34
+ };
35
+ }
36
+ /**
37
+ * Event channel service interface.
38
+ */
39
+ export interface IEventChannel {
40
+ /** Connect to the event stream */
41
+ connect(): void;
42
+ /** Disconnect from the event stream */
43
+ disconnect(): void;
44
+ /** Current connection status */
45
+ getStatus(): ConnectionStatus;
46
+ /**
47
+ * Subscribe to events of a specific type.
48
+ * @returns unsubscribe function
49
+ */
50
+ on<T = unknown>(eventType: string, handler: (event: EventChannelEvent<T>) => void): () => void;
51
+ /**
52
+ * Subscribe to all events.
53
+ * @returns unsubscribe function
54
+ */
55
+ onAny(handler: (event: EventChannelEvent) => void): () => void;
56
+ /**
57
+ * Subscribe to connection status changes.
58
+ * @returns unsubscribe function
59
+ */
60
+ onStatusChange(handler: (status: ConnectionStatus) => void): () => void;
61
+ /** Destroy the channel and clean up all subscriptions */
62
+ destroy(): void;
63
+ }
64
+ //# sourceMappingURL=event-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-types.d.ts","sourceRoot":"","sources":["../../../src/api/types/event-types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D;;GAEG;AACH,MAAM,WAAW,iBAAiB,CAAC,CAAC,GAAG,OAAO;IAC5C,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,IAAI,EAAE,CAAC,CAAC;IACR,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,yBAAyB;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wBAAwB;IACxB,IAAI,CAAC,EAAE;QACL,MAAM,EAAE,MAAM,CAAC;QACf,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,kCAAkC;IAClC,OAAO,IAAI,IAAI,CAAC;IAEhB,uCAAuC;IACvC,UAAU,IAAI,IAAI,CAAC;IAEnB,gCAAgC;IAChC,SAAS,IAAI,gBAAgB,CAAC;IAE9B;;;OAGG;IACH,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAE/F;;;OAGG;IACH,KAAK,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAE/D;;;OAGG;IACH,cAAc,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAExE,yDAAyD;IACzD,OAAO,IAAI,IAAI,CAAC;CACjB"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Core event channel types.
3
+ */
4
+ export {};
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Event Channel Service
3
+ *
4
+ * Manages the event stream connection with auto-reconnect
5
+ * and typed event subscriptions.
6
+ */
7
+ import type { EventChannelEvent, ConnectionStatus, IEventChannel } from '../../api';
8
+ import type { IEventTransport } from '../../spi/transport/i-event-transport';
9
+ import type { EventChannelPluginConfig } from '../../spi/config/event-channel-plugin-config';
10
+ export declare class EventChannel implements IEventChannel {
11
+ private transport;
12
+ private config;
13
+ private typedHandlers;
14
+ private anyHandlers;
15
+ private statusHandlers;
16
+ private reconnectTimer;
17
+ private reconnectAttempts;
18
+ private destroyed;
19
+ private unsubscribers;
20
+ constructor(pluginConfig: EventChannelPluginConfig, transport: IEventTransport);
21
+ connect(): void;
22
+ disconnect(): void;
23
+ getStatus(): ConnectionStatus;
24
+ on<T = unknown>(eventType: string, handler: (event: EventChannelEvent<T>) => void): () => void;
25
+ onAny(handler: (event: EventChannelEvent) => void): () => void;
26
+ onStatusChange(handler: (status: ConnectionStatus) => void): () => void;
27
+ destroy(): void;
28
+ private handleEvent;
29
+ private handleStatusChange;
30
+ private handleError;
31
+ private scheduleReconnect;
32
+ private cancelReconnect;
33
+ private log;
34
+ }
35
+ //# sourceMappingURL=event-channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-channel.d.ts","sourceRoot":"","sources":["../../../src/impl/core/event-channel.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACpF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAC;AAC7E,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,8CAA8C,CAAC;AAE7F,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,MAAM,CAAgJ;IAC9J,OAAO,CAAC,aAAa,CAA8D;IACnF,OAAO,CAAC,WAAW,CAAiD;IACpE,OAAO,CAAC,cAAc,CAAiD;IACvE,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,aAAa,CAAyB;gBAElC,YAAY,EAAE,wBAAwB,EAAE,SAAS,EAAE,eAAe;IAmB9E,OAAO,IAAI,IAAI;IAQf,UAAU,IAAI,IAAI;IAMlB,SAAS,IAAI,gBAAgB;IAI7B,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI;IAS9F,KAAK,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,MAAM,IAAI;IAK9D,cAAc,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,GAAG,MAAM,IAAI;IAKvE,OAAO,IAAI,IAAI;IAcf,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,kBAAkB;IAe1B,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,iBAAiB;IAyBzB,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,GAAG;CAKZ"}
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Event Channel Service
3
+ *
4
+ * Manages the event stream connection with auto-reconnect
5
+ * and typed event subscriptions.
6
+ */
7
+ export class EventChannel {
8
+ constructor(pluginConfig, transport) {
9
+ Object.defineProperty(this, "transport", {
10
+ enumerable: true,
11
+ configurable: true,
12
+ writable: true,
13
+ value: void 0
14
+ });
15
+ Object.defineProperty(this, "config", {
16
+ enumerable: true,
17
+ configurable: true,
18
+ writable: true,
19
+ value: void 0
20
+ });
21
+ Object.defineProperty(this, "typedHandlers", {
22
+ enumerable: true,
23
+ configurable: true,
24
+ writable: true,
25
+ value: new Map()
26
+ });
27
+ Object.defineProperty(this, "anyHandlers", {
28
+ enumerable: true,
29
+ configurable: true,
30
+ writable: true,
31
+ value: new Set()
32
+ });
33
+ Object.defineProperty(this, "statusHandlers", {
34
+ enumerable: true,
35
+ configurable: true,
36
+ writable: true,
37
+ value: new Set()
38
+ });
39
+ Object.defineProperty(this, "reconnectTimer", {
40
+ enumerable: true,
41
+ configurable: true,
42
+ writable: true,
43
+ value: null
44
+ });
45
+ Object.defineProperty(this, "reconnectAttempts", {
46
+ enumerable: true,
47
+ configurable: true,
48
+ writable: true,
49
+ value: 0
50
+ });
51
+ Object.defineProperty(this, "destroyed", {
52
+ enumerable: true,
53
+ configurable: true,
54
+ writable: true,
55
+ value: false
56
+ });
57
+ Object.defineProperty(this, "unsubscribers", {
58
+ enumerable: true,
59
+ configurable: true,
60
+ writable: true,
61
+ value: []
62
+ });
63
+ this.transport = transport;
64
+ this.config = {
65
+ url: pluginConfig.url ?? '/api/events',
66
+ autoReconnect: pluginConfig.autoReconnect ?? true,
67
+ maxReconnectAttempts: pluginConfig.maxReconnectAttempts ?? 0,
68
+ reconnectDelay: pluginConfig.reconnectDelay ?? 1000,
69
+ maxReconnectDelay: pluginConfig.maxReconnectDelay ?? 30000,
70
+ debug: pluginConfig.debug ?? false,
71
+ };
72
+ // Wire transport events
73
+ this.unsubscribers.push(this.transport.onEvent((event) => this.handleEvent(event)), this.transport.onStatusChange((status) => this.handleStatusChange(status)), this.transport.onError((error) => this.handleError(error)));
74
+ }
75
+ connect() {
76
+ if (this.destroyed)
77
+ return;
78
+ this.reconnectAttempts = 0;
79
+ this.cancelReconnect();
80
+ this.transport.connect(this.config.url);
81
+ this.log('Connecting to', this.config.url);
82
+ }
83
+ disconnect() {
84
+ this.cancelReconnect();
85
+ this.transport.disconnect();
86
+ this.log('Disconnected');
87
+ }
88
+ getStatus() {
89
+ return this.transport.getStatus();
90
+ }
91
+ on(eventType, handler) {
92
+ if (!this.typedHandlers.has(eventType)) {
93
+ this.typedHandlers.set(eventType, new Set());
94
+ }
95
+ const handlers = this.typedHandlers.get(eventType);
96
+ handlers.add(handler);
97
+ return () => { handlers.delete(handler); };
98
+ }
99
+ onAny(handler) {
100
+ this.anyHandlers.add(handler);
101
+ return () => { this.anyHandlers.delete(handler); };
102
+ }
103
+ onStatusChange(handler) {
104
+ this.statusHandlers.add(handler);
105
+ return () => { this.statusHandlers.delete(handler); };
106
+ }
107
+ destroy() {
108
+ this.destroyed = true;
109
+ this.cancelReconnect();
110
+ this.transport.disconnect();
111
+ for (const unsub of this.unsubscribers) {
112
+ unsub();
113
+ }
114
+ this.unsubscribers = [];
115
+ this.typedHandlers.clear();
116
+ this.anyHandlers.clear();
117
+ this.statusHandlers.clear();
118
+ this.log('Destroyed');
119
+ }
120
+ handleEvent(event) {
121
+ // Notify typed handlers
122
+ const handlers = this.typedHandlers.get(event.type);
123
+ if (handlers) {
124
+ for (const handler of handlers) {
125
+ handler(event);
126
+ }
127
+ }
128
+ // Notify wildcard handlers
129
+ for (const handler of this.anyHandlers) {
130
+ handler(event);
131
+ }
132
+ }
133
+ handleStatusChange(status) {
134
+ if (status === 'connected') {
135
+ this.reconnectAttempts = 0;
136
+ this.log('Connected');
137
+ }
138
+ if ((status === 'error' || status === 'reconnecting') && this.config.autoReconnect) {
139
+ this.scheduleReconnect();
140
+ }
141
+ for (const handler of this.statusHandlers) {
142
+ handler(status);
143
+ }
144
+ }
145
+ handleError(error) {
146
+ this.log('Error:', error.message);
147
+ }
148
+ scheduleReconnect() {
149
+ if (this.destroyed || this.reconnectTimer)
150
+ return;
151
+ if (this.config.maxReconnectAttempts > 0 && this.reconnectAttempts >= this.config.maxReconnectAttempts) {
152
+ this.log('Max reconnect attempts reached');
153
+ return;
154
+ }
155
+ const jitter = Math.random() * 500;
156
+ const delay = Math.min(this.config.reconnectDelay * Math.pow(2, this.reconnectAttempts) + jitter, this.config.maxReconnectDelay);
157
+ this.reconnectAttempts++;
158
+ this.log(`Reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts})`);
159
+ this.reconnectTimer = setTimeout(() => {
160
+ this.reconnectTimer = null;
161
+ if (!this.destroyed) {
162
+ this.transport.connect(this.config.url);
163
+ }
164
+ }, delay);
165
+ }
166
+ cancelReconnect() {
167
+ if (this.reconnectTimer) {
168
+ clearTimeout(this.reconnectTimer);
169
+ this.reconnectTimer = null;
170
+ }
171
+ }
172
+ log(...args) {
173
+ if (this.config.debug) {
174
+ console.log('[event-channel]', ...args);
175
+ }
176
+ }
177
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @hamak/event-channel Implementation
3
+ *
4
+ * Re-exports API + SPI + concrete implementations.
5
+ */
6
+ export * from '../api';
7
+ export type { IEventTransport } from '../spi/transport/i-event-transport';
8
+ export type { IRemoteActionFilter } from '../spi/security/i-action-filter';
9
+ export type { EventChannelPluginConfig } from '../spi/config/event-channel-plugin-config';
10
+ export { SseTransport } from './transport/sse-transport';
11
+ export { EventChannel } from './core/event-channel';
12
+ export { ActionWhitelistFilter } from './security/action-whitelist-filter';
13
+ export { eventChannelReducer, type EventChannelState } from './store/event-channel-reducer';
14
+ export { createEventChannelMiddleware, type EventChannelMiddlewareConfig } from './middleware/event-channel-middleware';
15
+ export { createEventChannelPlugin } from './plugin/event-channel-plugin-factory';
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/impl/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,cAAc,QAAQ,CAAC;AACvB,YAAY,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAC1E,YAAY,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,YAAY,EAAE,wBAAwB,EAAE,MAAM,2CAA2C,CAAC;AAG1F,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAGzD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAG3E,OAAO,EAAE,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAG5F,OAAO,EAAE,4BAA4B,EAAE,KAAK,4BAA4B,EAAE,MAAM,uCAAuC,CAAC;AAGxH,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @hamak/event-channel Implementation
3
+ *
4
+ * Re-exports API + SPI + concrete implementations.
5
+ */
6
+ // Re-export API and SPI
7
+ export * from '../api';
8
+ // Transport
9
+ export { SseTransport } from './transport/sse-transport';
10
+ // Core
11
+ export { EventChannel } from './core/event-channel';
12
+ // Security
13
+ export { ActionWhitelistFilter } from './security/action-whitelist-filter';
14
+ // Store
15
+ export { eventChannelReducer } from './store/event-channel-reducer';
16
+ // Middleware
17
+ export { createEventChannelMiddleware } from './middleware/event-channel-middleware';
18
+ // Plugin
19
+ export { createEventChannelPlugin } from './plugin/event-channel-plugin-factory';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Event Channel Middleware
3
+ *
4
+ * Intercepts REMOTE_ACTION_RECEIVED events and dispatches
5
+ * the actual Redux action after checking the security filter.
6
+ */
7
+ import type { Middleware } from '@reduxjs/toolkit';
8
+ import type { IRemoteActionFilter } from '../../spi/security/i-action-filter';
9
+ export interface EventChannelMiddlewareConfig {
10
+ actionFilter: IRemoteActionFilter;
11
+ debug?: boolean;
12
+ }
13
+ export declare function createEventChannelMiddleware(config: EventChannelMiddlewareConfig): Middleware;
14
+ //# sourceMappingURL=event-channel-middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-channel-middleware.d.ts","sourceRoot":"","sources":["../../../src/impl/middleware/event-channel-middleware.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAE9E,MAAM,WAAW,4BAA4B;IAC3C,YAAY,EAAE,mBAAmB,CAAC;IAClC,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,4BAA4B,GACnC,UAAU,CA4BZ"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Event Channel Middleware
3
+ *
4
+ * Intercepts REMOTE_ACTION_RECEIVED events and dispatches
5
+ * the actual Redux action after checking the security filter.
6
+ */
7
+ import { EventChannelActionTypes } from '../../api/actions/event-channel-actions';
8
+ export function createEventChannelMiddleware(config) {
9
+ return (store) => (next) => (action) => {
10
+ const result = next(action);
11
+ const typedAction = action;
12
+ if (typedAction.type === EventChannelActionTypes.REMOTE_ACTION_RECEIVED) {
13
+ const remoteAction = typedAction.payload;
14
+ if (config.actionFilter.isAllowed(remoteAction)) {
15
+ if (config.debug) {
16
+ console.log('[event-channel] Dispatching remote action:', remoteAction.actionType);
17
+ }
18
+ store.dispatch({
19
+ type: remoteAction.actionType,
20
+ payload: remoteAction.payload,
21
+ meta: {
22
+ ...remoteAction.meta,
23
+ remote: true,
24
+ },
25
+ });
26
+ }
27
+ else if (config.debug) {
28
+ console.warn('[event-channel] Blocked remote action:', remoteAction.actionType);
29
+ }
30
+ }
31
+ return result;
32
+ };
33
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Event Channel Plugin Factory
3
+ *
4
+ * Creates a microkernel plugin for the event channel.
5
+ */
6
+ import type { PluginModule } from '@hamak/microkernel-spi';
7
+ import type { EventChannelPluginConfig } from '../../spi/config/event-channel-plugin-config';
8
+ /**
9
+ * Create an event channel plugin for the microkernel.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { createEventChannelPlugin } from '@hamak/event-channel';
14
+ *
15
+ * host.registerPlugin(
16
+ * 'event-channel',
17
+ * { name: 'event-channel', version: '1.0.0', entry: '', dependsOn: ['store'] },
18
+ * createEventChannelPlugin({
19
+ * url: '/api/events',
20
+ * allowedActionTypes: ['todos/*', 'notification/*'],
21
+ * debug: true,
22
+ * })
23
+ * );
24
+ * ```
25
+ */
26
+ export declare function createEventChannelPlugin(config?: EventChannelPluginConfig): PluginModule;
27
+ //# sourceMappingURL=event-channel-plugin-factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-channel-plugin-factory.d.ts","sourceRoot":"","sources":["../../../src/impl/plugin/event-channel-plugin-factory.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAyB,MAAM,wBAAwB,CAAC;AAIlF,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,8CAA8C,CAAC;AAQ7F;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,GAAE,wBAA6B,GACpC,YAAY,CA6Hd"}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Event Channel Plugin Factory
3
+ *
4
+ * Creates a microkernel plugin for the event channel.
5
+ */
6
+ import { EVENT_CHANNEL_TOKEN } from '../../api/tokens/service-tokens';
7
+ import { eventChannelActions } from '../../api/actions/event-channel-actions';
8
+ import { SseTransport } from '../transport/sse-transport';
9
+ import { EventChannel } from '../core/event-channel';
10
+ import { ActionWhitelistFilter } from '../security/action-whitelist-filter';
11
+ import { eventChannelReducer } from '../store/event-channel-reducer';
12
+ import { createEventChannelMiddleware } from '../middleware/event-channel-middleware';
13
+ /**
14
+ * Create an event channel plugin for the microkernel.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * import { createEventChannelPlugin } from '@hamak/event-channel';
19
+ *
20
+ * host.registerPlugin(
21
+ * 'event-channel',
22
+ * { name: 'event-channel', version: '1.0.0', entry: '', dependsOn: ['store'] },
23
+ * createEventChannelPlugin({
24
+ * url: '/api/events',
25
+ * allowedActionTypes: ['todos/*', 'notification/*'],
26
+ * debug: true,
27
+ * })
28
+ * );
29
+ * ```
30
+ */
31
+ export function createEventChannelPlugin(config = {}) {
32
+ let channel;
33
+ const debug = config.debug ?? false;
34
+ return {
35
+ async initialize(ctx) {
36
+ // Build action filter
37
+ let actionFilter;
38
+ if (!config.allowedActionTypes) {
39
+ actionFilter = { isAllowed: () => true };
40
+ }
41
+ else if (Array.isArray(config.allowedActionTypes)) {
42
+ actionFilter = new ActionWhitelistFilter(config.allowedActionTypes);
43
+ }
44
+ else {
45
+ actionFilter = config.allowedActionTypes;
46
+ }
47
+ // Build transport
48
+ const transport = config.transport ?? new SseTransport();
49
+ // Build channel
50
+ channel = new EventChannel(config, transport);
51
+ // Register in DI
52
+ ctx.provide({ provide: EVENT_CHANNEL_TOKEN, useValue: channel });
53
+ // Register reducer and middleware with store (optional dependency)
54
+ try {
55
+ const STORE_EXTENSIONS_TOKEN = Symbol.for('@hamak/ui-store:StoreExtensionsRegistry');
56
+ const storeExtensions = ctx.resolve(STORE_EXTENSIONS_TOKEN);
57
+ if (storeExtensions && typeof storeExtensions.register === 'function') {
58
+ storeExtensions.register('event-channel', {
59
+ reducers: {
60
+ eventChannel: eventChannelReducer,
61
+ },
62
+ middleware: [{
63
+ id: 'event-channel',
64
+ middleware: createEventChannelMiddleware({ actionFilter, debug }),
65
+ priority: config.middlewarePriority ?? 500,
66
+ plugin: 'event-channel',
67
+ description: 'Dispatches remote actions from backend event stream',
68
+ }],
69
+ });
70
+ }
71
+ }
72
+ catch {
73
+ if (debug) {
74
+ console.log('[event-channel] Store extensions not available, skipping reducer/middleware registration');
75
+ }
76
+ }
77
+ // Register commands
78
+ ctx.commands.register('eventChannel.connect', () => channel.connect());
79
+ ctx.commands.register('eventChannel.disconnect', () => channel.disconnect());
80
+ ctx.commands.register('eventChannel.status', () => channel.getStatus());
81
+ },
82
+ async activate(ctx) {
83
+ const url = config.url ?? '/api/events';
84
+ // Wire channel events to hooks and store
85
+ channel.onStatusChange((status) => {
86
+ ctx.hooks.emit('event-channel:status', status);
87
+ // Dispatch connection status to store
88
+ try {
89
+ const STORE_MANAGER_TOKEN = Symbol.for('StoreManager');
90
+ const storeManager = ctx.resolve(STORE_MANAGER_TOKEN);
91
+ if (storeManager) {
92
+ storeManager.dispatch(eventChannelActions.connectionStatusChanged({
93
+ status,
94
+ reconnectAttempts: 0,
95
+ url,
96
+ lastConnectedAt: status === 'connected' ? new Date().toISOString() : undefined,
97
+ }));
98
+ }
99
+ }
100
+ catch {
101
+ // Store not available
102
+ }
103
+ });
104
+ channel.onAny((event) => {
105
+ ctx.hooks.emit('event-channel:event', event);
106
+ // Dispatch to store
107
+ try {
108
+ const STORE_MANAGER_TOKEN = Symbol.for('StoreManager');
109
+ const storeManager = ctx.resolve(STORE_MANAGER_TOKEN);
110
+ if (storeManager) {
111
+ // Store the event
112
+ storeManager.dispatch(eventChannelActions.eventReceived(event));
113
+ // If it's a remote action, dispatch so middleware can process it
114
+ if (event.type === 'remote-action') {
115
+ storeManager.dispatch(eventChannelActions.remoteActionReceived(event.data));
116
+ }
117
+ }
118
+ }
119
+ catch {
120
+ // Store not available
121
+ }
122
+ });
123
+ // Auto-connect if configured
124
+ if (config.autoConnect !== false) {
125
+ channel.connect();
126
+ }
127
+ ctx.hooks.emit('event-channel:ready', { channel });
128
+ if (debug) {
129
+ console.log('[event-channel] Plugin activated', config.autoConnect !== false ? '(auto-connecting)' : '');
130
+ }
131
+ },
132
+ async deactivate() {
133
+ channel?.destroy();
134
+ },
135
+ };
136
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Action Whitelist Filter
3
+ *
4
+ * Filters remote actions based on a whitelist of allowed action type patterns.
5
+ * Supports exact match and simple glob patterns (e.g., 'todos/*').
6
+ */
7
+ import type { RemoteAction } from '../../api';
8
+ import type { IRemoteActionFilter } from '../../spi/security/i-action-filter';
9
+ export declare class ActionWhitelistFilter implements IRemoteActionFilter {
10
+ private readonly patterns;
11
+ constructor(allowedPatterns: string[]);
12
+ isAllowed(action: RemoteAction): boolean;
13
+ private matches;
14
+ }
15
+ //# sourceMappingURL=action-whitelist-filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-whitelist-filter.d.ts","sourceRoot":"","sources":["../../../src/impl/security/action-whitelist-filter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAE9E,qBAAa,qBAAsB,YAAW,mBAAmB;IAC/D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;gBAExB,eAAe,EAAE,MAAM,EAAE;IAIrC,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO;IAIxC,OAAO,CAAC,OAAO;CAehB"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Action Whitelist Filter
3
+ *
4
+ * Filters remote actions based on a whitelist of allowed action type patterns.
5
+ * Supports exact match and simple glob patterns (e.g., 'todos/*').
6
+ */
7
+ export class ActionWhitelistFilter {
8
+ constructor(allowedPatterns) {
9
+ Object.defineProperty(this, "patterns", {
10
+ enumerable: true,
11
+ configurable: true,
12
+ writable: true,
13
+ value: void 0
14
+ });
15
+ this.patterns = allowedPatterns;
16
+ }
17
+ isAllowed(action) {
18
+ return this.patterns.some((pattern) => this.matches(pattern, action.actionType));
19
+ }
20
+ matches(pattern, actionType) {
21
+ // Exact match
22
+ if (pattern === actionType)
23
+ return true;
24
+ // Simple glob: 'prefix/*' matches 'prefix/anything'
25
+ if (pattern.endsWith('/*')) {
26
+ const prefix = pattern.slice(0, -2);
27
+ return actionType.startsWith(prefix + '/') || actionType === prefix;
28
+ }
29
+ // Wildcard-only: '*' matches everything
30
+ if (pattern === '*')
31
+ return true;
32
+ return false;
33
+ }
34
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Event Channel Reducer
3
+ *
4
+ * Manages connection state and recent events in the Redux store.
5
+ */
6
+ import type { ConnectionInfo } from '../../api/types/connection-types';
7
+ import type { EventChannelEvent } from '../../api/types/event-types';
8
+ import { type EventChannelAction } from '../../api/actions/event-channel-actions';
9
+ export interface EventChannelState {
10
+ connection: ConnectionInfo;
11
+ recentEvents: EventChannelEvent[];
12
+ totalEventsReceived: number;
13
+ }
14
+ export declare function eventChannelReducer(state: EventChannelState | undefined, action: EventChannelAction | {
15
+ type: string;
16
+ }): EventChannelState;
17
+ //# sourceMappingURL=event-channel-reducer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-channel-reducer.d.ts","sourceRoot":"","sources":["../../../src/impl/store/event-channel-reducer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAA2B,KAAK,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAI3G,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,cAAc,CAAC;IAC3B,YAAY,EAAE,iBAAiB,EAAE,CAAC;IAClC,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAYD,wBAAgB,mBAAmB,CACjC,KAAK,+BAAkC,EACvC,MAAM,EAAE,kBAAkB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5C,iBAAiB,CAuBnB"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Event Channel Reducer
3
+ *
4
+ * Manages connection state and recent events in the Redux store.
5
+ */
6
+ import { EventChannelActionTypes } from '../../api/actions/event-channel-actions';
7
+ const MAX_RECENT_EVENTS = 50;
8
+ const initialState = {
9
+ connection: {
10
+ status: 'disconnected',
11
+ reconnectAttempts: 0,
12
+ url: '',
13
+ },
14
+ recentEvents: [],
15
+ totalEventsReceived: 0,
16
+ };
17
+ export function eventChannelReducer(state = initialState, action) {
18
+ switch (action.type) {
19
+ case EventChannelActionTypes.CONNECTION_STATUS_CHANGED: {
20
+ const { payload } = action;
21
+ return {
22
+ ...state,
23
+ connection: payload,
24
+ };
25
+ }
26
+ case EventChannelActionTypes.EVENT_RECEIVED: {
27
+ const { payload } = action;
28
+ const recentEvents = [payload, ...state.recentEvents].slice(0, MAX_RECENT_EVENTS);
29
+ return {
30
+ ...state,
31
+ recentEvents,
32
+ totalEventsReceived: state.totalEventsReceived + 1,
33
+ };
34
+ }
35
+ default:
36
+ return state;
37
+ }
38
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * SSE Transport
3
+ *
4
+ * Browser EventSource-based transport for the event channel.
5
+ */
6
+ import type { EventChannelEvent, ConnectionStatus } from '../../api';
7
+ import type { IEventTransport } from '../../spi';
8
+ export declare class SseTransport implements IEventTransport {
9
+ private eventSource;
10
+ private status;
11
+ private eventHandlers;
12
+ private statusHandlers;
13
+ private errorHandlers;
14
+ connect(url: string): void;
15
+ /** Add a listener for a specific SSE event type */
16
+ addEventType(type: string): void;
17
+ disconnect(): void;
18
+ getStatus(): ConnectionStatus;
19
+ onEvent(handler: (event: EventChannelEvent) => void): () => void;
20
+ onStatusChange(handler: (status: ConnectionStatus) => void): () => void;
21
+ onError(handler: (error: Error) => void): () => void;
22
+ private handleMessage;
23
+ private setStatus;
24
+ }
25
+ //# sourceMappingURL=sse-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse-transport.d.ts","sourceRoot":"","sources":["../../../src/impl/transport/sse-transport.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD,qBAAa,YAAa,YAAW,eAAe;IAClD,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,MAAM,CAAoC;IAClD,OAAO,CAAC,aAAa,CAAiD;IACtE,OAAO,CAAC,cAAc,CAAiD;IACvE,OAAO,CAAC,aAAa,CAAqC;IAE1D,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAqC1B,mDAAmD;IACnD,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAOhC,UAAU,IAAI,IAAI;IAQlB,SAAS,IAAI,gBAAgB;IAI7B,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,MAAM,IAAI;IAKhE,cAAc,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,GAAG,MAAM,IAAI;IAKvE,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI;IAKpD,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,SAAS;CAOlB"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * SSE Transport
3
+ *
4
+ * Browser EventSource-based transport for the event channel.
5
+ */
6
+ export class SseTransport {
7
+ constructor() {
8
+ Object.defineProperty(this, "eventSource", {
9
+ enumerable: true,
10
+ configurable: true,
11
+ writable: true,
12
+ value: null
13
+ });
14
+ Object.defineProperty(this, "status", {
15
+ enumerable: true,
16
+ configurable: true,
17
+ writable: true,
18
+ value: 'disconnected'
19
+ });
20
+ Object.defineProperty(this, "eventHandlers", {
21
+ enumerable: true,
22
+ configurable: true,
23
+ writable: true,
24
+ value: new Set()
25
+ });
26
+ Object.defineProperty(this, "statusHandlers", {
27
+ enumerable: true,
28
+ configurable: true,
29
+ writable: true,
30
+ value: new Set()
31
+ });
32
+ Object.defineProperty(this, "errorHandlers", {
33
+ enumerable: true,
34
+ configurable: true,
35
+ writable: true,
36
+ value: new Set()
37
+ });
38
+ }
39
+ connect(url) {
40
+ if (this.eventSource) {
41
+ this.disconnect();
42
+ }
43
+ this.setStatus('connecting');
44
+ this.eventSource = new EventSource(url);
45
+ this.eventSource.onopen = () => {
46
+ this.setStatus('connected');
47
+ };
48
+ this.eventSource.onerror = () => {
49
+ // EventSource auto-reconnects on error; we report the status change
50
+ if (this.eventSource?.readyState === EventSource.CLOSED) {
51
+ this.setStatus('error');
52
+ for (const handler of this.errorHandlers) {
53
+ handler(new Error('EventSource connection closed'));
54
+ }
55
+ }
56
+ else if (this.eventSource?.readyState === EventSource.CONNECTING) {
57
+ this.setStatus('reconnecting');
58
+ }
59
+ };
60
+ // Listen for all named events via a generic message listener
61
+ this.eventSource.onmessage = (messageEvent) => {
62
+ this.handleMessage(messageEvent);
63
+ };
64
+ // Also listen for named events by intercepting addEventListener
65
+ // EventSource fires named events that onmessage doesn't catch
66
+ // We use a proxy pattern: listen on the most common event types
67
+ // The server sends events with `event: <type>`, which EventSource routes to named listeners
68
+ // We'll use the 'message' event as fallback and add specific listeners for known types
69
+ }
70
+ /** Add a listener for a specific SSE event type */
71
+ addEventType(type) {
72
+ if (!this.eventSource)
73
+ return;
74
+ this.eventSource.addEventListener(type, ((event) => {
75
+ this.handleMessage(event);
76
+ }));
77
+ }
78
+ disconnect() {
79
+ if (this.eventSource) {
80
+ this.eventSource.close();
81
+ this.eventSource = null;
82
+ }
83
+ this.setStatus('disconnected');
84
+ }
85
+ getStatus() {
86
+ return this.status;
87
+ }
88
+ onEvent(handler) {
89
+ this.eventHandlers.add(handler);
90
+ return () => { this.eventHandlers.delete(handler); };
91
+ }
92
+ onStatusChange(handler) {
93
+ this.statusHandlers.add(handler);
94
+ return () => { this.statusHandlers.delete(handler); };
95
+ }
96
+ onError(handler) {
97
+ this.errorHandlers.add(handler);
98
+ return () => { this.errorHandlers.delete(handler); };
99
+ }
100
+ handleMessage(messageEvent) {
101
+ try {
102
+ const parsed = JSON.parse(messageEvent.data);
103
+ for (const handler of this.eventHandlers) {
104
+ handler(parsed);
105
+ }
106
+ }
107
+ catch {
108
+ // Non-JSON message (e.g., initial connection message), ignore
109
+ }
110
+ }
111
+ setStatus(newStatus) {
112
+ if (this.status === newStatus)
113
+ return;
114
+ this.status = newStatus;
115
+ for (const handler of this.statusHandlers) {
116
+ handler(newStatus);
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @hamak/event-channel
3
+ * Reactive event channel with SSE transport and remote action dispatch
4
+ */
5
+ export * from './impl';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,QAAQ,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @hamak/event-channel
3
+ * Reactive event channel with SSE transport and remote action dispatch
4
+ */
5
+ export * from './impl';
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Configuration for the event channel plugin.
3
+ */
4
+ import type { IEventTransport } from '../transport/i-event-transport';
5
+ import type { IRemoteActionFilter } from '../security/i-action-filter';
6
+ export interface EventChannelPluginConfig {
7
+ /**
8
+ * URL of the SSE endpoint.
9
+ * @default '/api/events'
10
+ */
11
+ url?: string;
12
+ /**
13
+ * Whether to connect automatically on plugin activation.
14
+ * @default true
15
+ */
16
+ autoConnect?: boolean;
17
+ /**
18
+ * Enable auto-reconnect on connection loss.
19
+ * @default true
20
+ */
21
+ autoReconnect?: boolean;
22
+ /**
23
+ * Max reconnect attempts (0 = infinite).
24
+ * @default 0
25
+ */
26
+ maxReconnectAttempts?: number;
27
+ /**
28
+ * Base reconnect delay in ms (exponential backoff applied).
29
+ * @default 1000
30
+ */
31
+ reconnectDelay?: number;
32
+ /**
33
+ * Maximum reconnect delay in ms.
34
+ * @default 30000
35
+ */
36
+ maxReconnectDelay?: number;
37
+ /**
38
+ * Allowed action types for remote dispatch.
39
+ * String array supports glob patterns (e.g., 'todos/*').
40
+ * Or provide a custom IRemoteActionFilter.
41
+ * If not provided, ALL remote actions are allowed.
42
+ */
43
+ allowedActionTypes?: string[] | IRemoteActionFilter;
44
+ /**
45
+ * Custom transport (defaults to SseTransport).
46
+ */
47
+ transport?: IEventTransport;
48
+ /**
49
+ * Priority for the event-channel middleware.
50
+ * @default 500
51
+ */
52
+ middlewarePriority?: number;
53
+ /**
54
+ * Enable debug logging.
55
+ * @default false
56
+ */
57
+ debug?: boolean;
58
+ }
59
+ //# sourceMappingURL=event-channel-plugin-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-channel-plugin-config.d.ts","sourceRoot":"","sources":["../../../src/spi/config/event-channel-plugin-config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAEvE,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAE9B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,GAAG,mBAAmB,CAAC;IAEpD;;OAEG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;IAE5B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Configuration for the event channel plugin.
3
+ */
4
+ export {};
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @hamak/event-channel SPI
3
+ *
4
+ * Extension points and provider interfaces.
5
+ */
6
+ export * from '../api';
7
+ export type { IEventTransport } from './transport/i-event-transport';
8
+ export type { IRemoteActionFilter } from './security/i-action-filter';
9
+ export type { EventChannelPluginConfig } from './config/event-channel-plugin-config';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/spi/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,cAAc,QAAQ,CAAC;AAGvB,YAAY,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAGrE,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAGtE,YAAY,EAAE,wBAAwB,EAAE,MAAM,sCAAsC,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @hamak/event-channel SPI
3
+ *
4
+ * Extension points and provider interfaces.
5
+ */
6
+ // Re-export API
7
+ export * from '../api';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Security filter for remote actions.
3
+ */
4
+ import type { RemoteAction } from '../../api';
5
+ /**
6
+ * Determines which action types the backend is allowed to dispatch.
7
+ */
8
+ export interface IRemoteActionFilter {
9
+ /** Return true if this remote action should be dispatched */
10
+ isAllowed(action: RemoteAction): boolean;
11
+ }
12
+ //# sourceMappingURL=i-action-filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i-action-filter.d.ts","sourceRoot":"","sources":["../../../src/spi/security/i-action-filter.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,6DAA6D;IAC7D,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC;CAC1C"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Security filter for remote actions.
3
+ */
4
+ export {};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Transport abstraction for the event channel.
3
+ * SSE for Phase 1, WebSocket for Phase 2.
4
+ */
5
+ import type { EventChannelEvent, ConnectionStatus } from '../../api';
6
+ export interface IEventTransport {
7
+ /** Open the transport connection */
8
+ connect(url: string): void;
9
+ /** Close the transport connection */
10
+ disconnect(): void;
11
+ /** Current connection status */
12
+ getStatus(): ConnectionStatus;
13
+ /** Register event handler (called for each incoming event) */
14
+ onEvent(handler: (event: EventChannelEvent) => void): () => void;
15
+ /** Register status change handler */
16
+ onStatusChange(handler: (status: ConnectionStatus) => void): () => void;
17
+ /** Register error handler */
18
+ onError(handler: (error: Error) => void): () => void;
19
+ }
20
+ //# sourceMappingURL=i-event-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i-event-transport.d.ts","sourceRoot":"","sources":["../../../src/spi/transport/i-event-transport.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAErE,MAAM,WAAW,eAAe;IAC9B,oCAAoC;IACpC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAE3B,qCAAqC;IACrC,UAAU,IAAI,IAAI,CAAC;IAEnB,gCAAgC;IAChC,SAAS,IAAI,gBAAgB,CAAC;IAE9B,8DAA8D;IAC9D,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAEjE,qCAAqC;IACrC,cAAc,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAExE,6BAA6B;IAC7B,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACtD"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Transport abstraction for the event channel.
3
+ * SSE for Phase 1, WebSocket for Phase 2.
4
+ */
5
+ export {};
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@hamak/event-channel",
3
+ "version": "0.5.3",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Reactive event channel with SSE transport and remote action dispatch for microkernel-based applications",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "sideEffects": false,
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/amah/app-framework.git",
16
+ "directory": "packages/event-channel"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "build": "tsc -p tsconfig.lib.json",
23
+ "clean": "rm -rf dist",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest"
26
+ },
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "./api": {
34
+ "types": "./dist/api/index.d.ts",
35
+ "import": "./dist/api/index.js",
36
+ "default": "./dist/api/index.js"
37
+ },
38
+ "./spi": {
39
+ "types": "./dist/spi/index.d.ts",
40
+ "import": "./dist/spi/index.js",
41
+ "default": "./dist/spi/index.js"
42
+ }
43
+ },
44
+ "typesVersions": {
45
+ "*": {
46
+ "api": [
47
+ "./dist/api/index.d.ts"
48
+ ],
49
+ "spi": [
50
+ "./dist/spi/index.d.ts"
51
+ ]
52
+ }
53
+ },
54
+ "dependencies": {
55
+ "@hamak/microkernel-spi": "*",
56
+ "@reduxjs/toolkit": "^2.0.0"
57
+ },
58
+ "devDependencies": {
59
+ "typescript": "~5.4.0",
60
+ "vitest": "^2.0.0",
61
+ "@types/node": "^20.0.0"
62
+ }
63
+ }