UNPKG

5.46 kBPlain TextView Raw
1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4import { ConsoleLogger as Logger } from './Logger';
5
6const logger = new Logger('Hub');
7
8const AMPLIFY_SYMBOL = (
9 typeof Symbol !== 'undefined' && typeof Symbol.for === 'function'
10 ? Symbol.for('amplify_default')
11 : '@@amplify_default'
12) as Symbol;
13interface IPattern {
14 pattern: RegExp;
15 callback: HubCallback;
16}
17
18interface IListener {
19 name: string;
20 callback: HubCallback;
21}
22
23export type HubCapsule = {
24 channel: string;
25 payload: HubPayload;
26 source: string;
27 patternInfo?: string[];
28};
29
30export type HubPayload = {
31 event: string;
32 data?: any;
33 message?: string;
34};
35
36export type HubCallback = (capsule: HubCapsule) => void;
37
38export type LegacyCallback = { onHubCapsule: HubCallback };
39
40function isLegacyCallback(callback: any): callback is LegacyCallback {
41 return (<LegacyCallback>callback).onHubCapsule !== undefined;
42}
43
44export class HubClass {
45 name: string;
46 private listeners: IListener[] = [];
47 private patterns: IPattern[] = [];
48
49 protectedChannels = [
50 'core',
51 'auth',
52 'api',
53 'analytics',
54 'interactions',
55 'pubsub',
56 'storage',
57 'ui',
58 'xr',
59 ];
60
61 constructor(name: string) {
62 this.name = name;
63 }
64
65 /**
66 * Used internally to remove a Hub listener.
67 *
68 * @remarks
69 * This private method is for internal use only. Instead of calling Hub.remove, call the result of Hub.listen.
70 */
71 private _remove(channel: string | RegExp, listener: HubCallback) {
72 if (channel instanceof RegExp) {
73 const pattern = this.patterns.find(
74 ({ pattern }) => pattern.source === channel.source
75 );
76 if (!pattern) {
77 logger.warn(`No listeners for ${channel}`);
78 return;
79 }
80 this.patterns = [...this.patterns.filter(x => x !== pattern)];
81 } else {
82 const holder = this.listeners[channel];
83 if (!holder) {
84 logger.warn(`No listeners for ${channel}`);
85 return;
86 }
87 this.listeners[channel] = [
88 ...holder.filter(({ callback }) => callback !== listener),
89 ];
90 }
91 }
92
93 /**
94 * @deprecated Instead of calling Hub.remove, call the result of Hub.listen.
95 */
96 remove(channel: string | RegExp, listener: HubCallback) {
97 this._remove(channel, listener);
98 }
99
100 /**
101 * Used to send a Hub event.
102 *
103 * @param channel - The channel on which the event will be broadcast
104 * @param payload - The HubPayload
105 * @param source - The source of the event; defaults to ''
106 * @param ampSymbol - Symbol used to determine if the event is dispatched internally on a protected channel
107 *
108 */
109 dispatch(
110 channel: string,
111 payload: HubPayload,
112 source: string = '',
113 ampSymbol?: Symbol
114 ) {
115 if (this.protectedChannels.indexOf(channel) > -1) {
116 const hasAccess = ampSymbol === AMPLIFY_SYMBOL;
117
118 if (!hasAccess) {
119 logger.warn(
120 `WARNING: ${channel} is protected and dispatching on it can have unintended consequences`
121 );
122 }
123 }
124
125 const capsule: HubCapsule = {
126 channel,
127 payload: { ...payload },
128 source,
129 patternInfo: [],
130 };
131
132 try {
133 this._toListeners(capsule);
134 } catch (e) {
135 logger.error(e);
136 }
137 }
138
139 /**
140 * Used to listen for Hub events.
141 *
142 * @param channel - The channel on which to listen
143 * @param callback - The callback to execute when an event is received on the specified channel
144 * @param listenerName - The name of the listener; defaults to 'noname'
145 * @returns A function which can be called to cancel the listener.
146 *
147 */
148 listen(
149 channel: string | RegExp,
150 callback?: HubCallback | LegacyCallback,
151 listenerName = 'noname'
152 ) {
153 let cb: HubCallback;
154 // Check for legacy onHubCapsule callback for backwards compatability
155 if (isLegacyCallback(callback)) {
156 logger.warn(
157 `WARNING onHubCapsule is Deprecated. Please pass in a callback.`
158 );
159 cb = callback.onHubCapsule.bind(callback);
160 } else if (typeof callback !== 'function') {
161 throw new Error('No callback supplied to Hub');
162 } else {
163 cb = callback;
164 }
165
166 if (channel instanceof RegExp) {
167 this.patterns.push({
168 pattern: channel,
169 callback: cb,
170 });
171 } else {
172 let holder = this.listeners[channel];
173
174 if (!holder) {
175 holder = [];
176 this.listeners[channel] = holder;
177 }
178
179 holder.push({
180 name: listenerName,
181 callback: cb,
182 });
183 }
184
185 return () => {
186 this._remove(channel, cb);
187 };
188 }
189
190 private _toListeners(capsule: HubCapsule) {
191 const { channel, payload } = capsule;
192 const holder = this.listeners[channel];
193
194 if (holder) {
195 holder.forEach(listener => {
196 logger.debug(`Dispatching to ${channel} with `, payload);
197 try {
198 listener.callback(capsule);
199 } catch (e) {
200 logger.error(e);
201 }
202 });
203 }
204
205 if (this.patterns.length > 0) {
206 if (!payload.message) {
207 logger.warn(`Cannot perform pattern matching without a message key`);
208 return;
209 }
210
211 const payloadStr = payload.message;
212
213 this.patterns.forEach(pattern => {
214 const match = payloadStr.match(pattern.pattern);
215 if (match) {
216 const [, ...groups] = match;
217 const dispatchingCapsule: HubCapsule = {
218 ...capsule,
219 patternInfo: groups,
220 };
221 try {
222 pattern.callback(dispatchingCapsule);
223 } catch (e) {
224 logger.error(e);
225 }
226 }
227 });
228 }
229 }
230}
231
232/*We export a __default__ instance of HubClass to use it as a
233pseudo Singleton for the main messaging bus, however you can still create
234your own instance of HubClass() for a separate "private bus" of events.*/
235export const Hub = new HubClass('__default__');