laravel-echo-api-gateway 0.5.8 → 0.5.9

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/jest.config.js CHANGED
@@ -1,9 +1,5 @@
1
1
  module.exports = {
2
- transform: {
3
- '^.+\\.tsx?$': 'ts-jest',
4
- },
5
- moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node', 'd.ts'],
6
- transformIgnorePatterns: [
7
- 'node_modules/(?!(laravel-echo)/)',
8
- ],
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ testMatch: ['<rootDir>/js-tests/**/*.test.ts'],
9
5
  };
package/js-src/Channel.ts CHANGED
@@ -1,16 +1,18 @@
1
- import { EventFormatter } from 'laravel-echo/src/util';
2
- import { Channel as BaseChannel } from 'laravel-echo/src/channel/channel';
3
- import { PresenceChannel } from "laravel-echo/src/channel";
1
+ import { EventFormatter } from "./EventFormatter";
4
2
  import { Websocket } from "./Websocket";
3
+ import type { Options } from "./Websocket";
5
4
 
6
5
  const LOG_PREFIX = '[LE-AG-Channel]';
7
6
 
7
+ const NOTIFICATION_EVENT = '.Illuminate\\Notifications\\Events\\BroadcastNotificationCreated';
8
+
8
9
  /**
9
- * This class represents a Pusher channel.
10
+ * A channel on the API Gateway websocket. It implements the public, private and presence channel
11
+ * interfaces of laravel-echo (v1 and v2) without extending its internal classes.
10
12
  */
11
- export class Channel extends BaseChannel implements PresenceChannel {
13
+ export class Channel {
12
14
  /**
13
- * The Pusher client instance.
15
+ * The websocket connection.
14
16
  */
15
17
  socket: Websocket;
16
18
 
@@ -22,7 +24,7 @@ export class Channel extends BaseChannel implements PresenceChannel {
22
24
  /**
23
25
  * Channel options.
24
26
  */
25
- options: object;
27
+ options: Options;
26
28
 
27
29
  /**
28
30
  * The event formatter.
@@ -32,31 +34,29 @@ export class Channel extends BaseChannel implements PresenceChannel {
32
34
  /**
33
35
  * Create a new class instance.
34
36
  */
35
- constructor(socket: Websocket, name: string, options: object) {
36
- super();
37
-
37
+ constructor(socket: Websocket, name: string, options: Options) {
38
38
  this.name = name;
39
39
  this.socket = socket;
40
40
  this.options = options;
41
- this.eventFormatter = new EventFormatter(this.options["namespace"]);
41
+ this.eventFormatter = new EventFormatter(this.options.namespace);
42
42
 
43
43
  this.subscribe();
44
44
  }
45
45
 
46
46
  /**
47
- * Subscribe to a Pusher channel.
47
+ * Subscribe to the channel.
48
48
  */
49
- subscribe(): any {
50
- this.options["debug"] && console.log(`${LOG_PREFIX} subscribe for channel ${this.name} ...`);
49
+ subscribe(): void {
50
+ this.options.debug && console.log(`${LOG_PREFIX} subscribe for channel ${this.name} ...`);
51
51
 
52
- this.socket.subscribe(this)
52
+ this.socket.subscribe(this);
53
53
  }
54
54
 
55
55
  /**
56
- * Unsubscribe from a Pusher channel.
56
+ * Unsubscribe from the channel.
57
57
  */
58
58
  unsubscribe(): void {
59
- this.options["debug"] && console.log(`${LOG_PREFIX} unsubscribe for channel ${this.name} ...`);
59
+ this.options.debug && console.log(`${LOG_PREFIX} unsubscribe for channel ${this.name} ...`);
60
60
 
61
61
  this.socket.unsubscribe(this);
62
62
  }
@@ -65,96 +65,108 @@ export class Channel extends BaseChannel implements PresenceChannel {
65
65
  * Listen for an event on the channel instance.
66
66
  */
67
67
  listen(event: string, callback: Function): this {
68
- this.options["debug"] && console.log(`${LOG_PREFIX} listen to ${event} for channel ${this.name} ...`);
69
-
70
- this.on(this.eventFormatter.format(event), callback);
68
+ this.options.debug && console.log(`${LOG_PREFIX} listen to ${event} for channel ${this.name} ...`);
71
69
 
72
- return this;
70
+ return this.on(this.eventFormatter.format(event), callback);
73
71
  }
74
72
 
75
73
  /**
76
- * Stop listening for an event on the channel instance.
74
+ * Stop listening for an event on the channel instance. Without a callback, every listener of the event is removed.
77
75
  */
78
76
  stopListening(event: string, callback?: Function): this {
79
- this.options["debug"] && console.log(`${LOG_PREFIX} stop listening to ${event} for channel ${this.name} ...`);
77
+ this.options.debug && console.log(`${LOG_PREFIX} stop listening to ${event} for channel ${this.name} ...`);
80
78
 
81
- this.socket.unbindEvent(this, event, callback)
79
+ this.socket.unbind(this, this.eventFormatter.format(event), callback);
82
80
 
83
81
  return this;
84
82
  }
85
83
 
86
84
  /**
87
- * Register a callback to be called anytime a subscription succeeds.
85
+ * Listen for a whisper event on the channel instance.
88
86
  */
89
- subscribed(callback: Function): this {
90
- this.options["debug"] && console.log(`${LOG_PREFIX} subscribed for channel ${this.name} ...`);
87
+ listenForWhisper(event: string, callback: Function): this {
88
+ return this.listen('.client-' + event, callback);
89
+ }
91
90
 
92
- this.on('subscription_succeeded', () => {
93
- callback();
94
- });
91
+ /**
92
+ * Stop listening for a whisper event on the channel instance.
93
+ */
94
+ stopListeningForWhisper(event: string, callback?: Function): this {
95
+ return this.stopListening('.client-' + event, callback);
96
+ }
95
97
 
96
- return this;
98
+ /**
99
+ * Listen for an event on the channel instance.
100
+ */
101
+ notification(callback: Function): this {
102
+ return this.listen(NOTIFICATION_EVENT, callback);
97
103
  }
98
104
 
99
105
  /**
100
- * Register a callback to be called anytime a subscription error occurs.
106
+ * Stop listening for notification events on the channel instance.
101
107
  */
102
- error(callback: Function): this {
103
- this.options["debug"] && console.log(`${LOG_PREFIX} error for channel ${this.name} ...`);
108
+ stopListeningForNotification(callback?: Function): this {
109
+ return this.stopListening(NOTIFICATION_EVENT, callback);
110
+ }
104
111
 
105
- this.on('error', (status) => {
106
- callback(status);
112
+ /**
113
+ * Register a callback to be called anytime a subscription succeeds.
114
+ */
115
+ subscribed(callback: Function): this {
116
+ return this.on('subscription_succeeded', () => {
117
+ callback();
107
118
  });
119
+ }
108
120
 
109
- return this;
121
+ /**
122
+ * Register a callback to be called anytime a subscription or authorization error occurs.
123
+ */
124
+ error(callback: Function): this {
125
+ return this.on('error', callback);
110
126
  }
111
127
 
112
128
  /**
113
129
  * Bind a channel to an event.
114
130
  */
115
- on(event: string, callback: Function): Channel {
116
- this.options["debug"] && console.log(`${LOG_PREFIX} on ${event} for channel ${this.name} ...`);
131
+ on(event: string, callback: Function): this {
132
+ this.options.debug && console.log(`${LOG_PREFIX} on ${event} for channel ${this.name} ...`);
117
133
 
118
- this.socket.bind(this, event, callback)
134
+ this.socket.bind(this, event, callback);
119
135
 
120
136
  return this;
121
137
  }
122
138
 
123
- whisper(event: string, data: object): this {
124
- let channel = this.name;
125
- let formattedEvent = "client-" + event;
139
+ /**
140
+ * Send a client event to the other members of the channel.
141
+ */
142
+ whisper(event: string, data: unknown): this {
126
143
  this.socket.send({
127
- "event": formattedEvent,
144
+ event: 'client-' + event,
145
+ channel: this.name,
128
146
  data,
129
- channel,
130
- })
147
+ });
131
148
 
132
149
  return this;
133
150
  }
134
151
 
135
- here(callback: Function): this {
136
- // TODO: implement
137
-
138
- return this
152
+ /**
153
+ * Presence channels are not supported yet: member tracking is not implemented on the server.
154
+ */
155
+ here(_callback: Function): this {
156
+ return this;
139
157
  }
140
158
 
141
159
  /**
142
- * Listen for someone joining the channel.
160
+ * Presence channels are not supported yet: member tracking is not implemented on the server.
143
161
  */
144
- joining(callback: Function): this {
145
- // TODO: implement
146
-
147
- return this
162
+ joining(_callback: Function): this {
163
+ return this;
148
164
  }
149
165
 
150
166
  /**
151
- * Listen for someone leaving the channel.
167
+ * Presence channels are not supported yet: member tracking is not implemented on the server.
152
168
  */
153
- leaving(callback: Function): this {
154
- // TODO: implement
155
-
156
- return this
169
+ leaving(_callback: Function): this {
170
+ return this;
157
171
  }
158
172
  }
159
-
160
- export { PresenceChannel };
@@ -1,12 +1,38 @@
1
- import {Connector as BaseConnector} from "laravel-echo/src/connector/connector";
2
- import {Websocket} from "./Websocket";
3
- import {Channel} from "./Channel";
1
+ import { Websocket } from "./Websocket";
2
+ import type { Options } from "./Websocket";
3
+ import { Channel } from "./Channel";
4
4
 
5
- export const broadcaster = (options: object): Connector => new Connector(options);
5
+ export { Channel };
6
+ export { EventFormatter } from "./EventFormatter";
7
+ export type { AuthorizationData, AuthorizationError, ChannelAuthorization, Options } from "./Websocket";
6
8
 
7
9
  const LOG_PREFIX = '[LE-AG-Connector]';
8
10
 
9
- export class Connector extends BaseConnector {
11
+ /**
12
+ * Laravel Echo connector for API Gateway websockets.
13
+ *
14
+ * It does not extend laravel-echo's internal classes, so it works with laravel-echo v1 and v2:
15
+ * both create custom connectors with `new options.broadcaster(options)`.
16
+ */
17
+ export class Connector {
18
+ /**
19
+ * Default connector options, matching laravel-echo's defaults.
20
+ */
21
+ static readonly _defaultOptions = {
22
+ auth: {
23
+ headers: {},
24
+ },
25
+ authEndpoint: '/broadcasting/auth',
26
+ csrfToken: null,
27
+ bearerToken: null,
28
+ host: null,
29
+ namespace: 'App.Events',
30
+ };
31
+
32
+ /**
33
+ * Connector options.
34
+ */
35
+ options: Options;
10
36
 
11
37
  socket: Websocket;
12
38
 
@@ -16,30 +42,70 @@ export class Connector extends BaseConnector {
16
42
  channels: { [name: string]: Channel } = {};
17
43
 
18
44
  /**
19
- * Create a new class instance.
45
+ * Create a new class instance. Without a host no connection is opened, which keeps
46
+ * laravel-echo v2's `new broadcaster()` capability check free of side effects.
20
47
  */
21
- constructor(options: any) {
22
- super(options);
48
+ constructor(options: Options = {}) {
49
+ this.setOptions(options);
50
+ this.connect();
23
51
  }
24
52
 
25
53
  /**
26
- * Create a fresh Socket.io connection.
54
+ * Merge the custom options with the defaults and add the CSRF and bearer token headers.
55
+ */
56
+ protected setOptions(options: Options): void {
57
+ this.options = {
58
+ ...Connector._defaultOptions,
59
+ ...options,
60
+ auth: {
61
+ headers: { ...(options.auth?.headers ?? {}) },
62
+ },
63
+ };
64
+
65
+ const csrfToken = this.csrfToken();
66
+
67
+ if (csrfToken) {
68
+ this.options.auth.headers['X-CSRF-TOKEN'] = csrfToken;
69
+ }
70
+
71
+ if (this.options.bearerToken) {
72
+ this.options.auth.headers['Authorization'] = 'Bearer ' + this.options.bearerToken;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Extract the CSRF token from the page.
78
+ */
79
+ protected csrfToken(): string | null {
80
+ if (typeof window !== 'undefined' && (window as any).Laravel?.csrfToken) {
81
+ return (window as any).Laravel.csrfToken;
82
+ }
83
+
84
+ if (this.options.csrfToken) {
85
+ return this.options.csrfToken;
86
+ }
87
+
88
+ if (typeof document !== 'undefined' && typeof document.querySelector === 'function') {
89
+ return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') ?? null;
90
+ }
91
+
92
+ return null;
93
+ }
94
+
95
+ /**
96
+ * Create a fresh websocket connection.
27
97
  */
28
98
  connect(): void {
29
- this.options.debug && console.log(LOG_PREFIX + 'Connect ...' );
99
+ this.options.debug && console.log(LOG_PREFIX + ' Connect ...');
30
100
 
31
101
  this.socket = new Websocket(this.options);
102
+ }
32
103
 
33
- return;
34
-
35
- //
36
- // this.socket.on('reconnect', () => {
37
- // Object.values(this.channels).forEach((channel) => {
38
- // channel.subscribe();
39
- // });
40
- // });
41
- //
42
- // return this.socket;
104
+ /**
105
+ * Listen for an event on a channel instance.
106
+ */
107
+ listen(name: string, event: string, callback: Function): Channel {
108
+ return this.channel(name).listen(event, callback);
43
109
  }
44
110
 
45
111
  /**
@@ -57,36 +123,22 @@ export class Connector extends BaseConnector {
57
123
  * Get a private channel instance by name.
58
124
  */
59
125
  privateChannel(name: string): Channel {
60
- if (!this.channels['private-' + name]) {
61
- this.channels['private-' + name] = new Channel(this.socket, 'private-' + name, this.options);
62
- }
63
-
64
- return this.channels['private-' + name] as Channel;
126
+ return this.channel('private-' + name);
65
127
  }
66
128
 
67
129
  /**
68
130
  * Get a presence channel instance by name.
69
131
  */
70
132
  presenceChannel(name: string): Channel {
71
- if (!this.channels['presence-' + name]) {
72
- this.channels['presence-' + name] = new Channel(
73
- this.socket,
74
- 'presence-' + name,
75
- this.options
76
- );
77
- }
78
-
79
- return this.channels['presence-' + name] as Channel;
133
+ return this.channel('presence-' + name);
80
134
  }
81
135
 
82
136
  /**
83
137
  * Leave the given channel, as well as its private and presence variants.
84
138
  */
85
139
  leave(name: string): void {
86
- let channels = [name, 'private-' + name, 'presence-' + name];
87
-
88
- channels.forEach((name) => {
89
- this.leaveChannel(name);
140
+ [name, 'private-' + name, 'presence-' + name].forEach((channelName) => {
141
+ this.leaveChannel(channelName);
90
142
  });
91
143
  }
92
144
 
@@ -104,7 +156,7 @@ export class Connector extends BaseConnector {
104
156
  /**
105
157
  * Get the socket ID for the connection.
106
158
  */
107
- socketId(): string {
159
+ socketId(): string | undefined {
108
160
  return this.socket.getSocketId();
109
161
  }
110
162
 
@@ -112,8 +164,16 @@ export class Connector extends BaseConnector {
112
164
  * Disconnect socket connection.
113
165
  */
114
166
  disconnect(): void {
115
- this.options.debug && console.log(LOG_PREFIX + 'Disconnect ...' );
167
+ this.options.debug && console.log(LOG_PREFIX + ' Disconnect ...');
116
168
 
117
169
  this.socket.close();
118
170
  }
119
171
  }
172
+
173
+ /**
174
+ * Kept for backwards compatibility: `new Echo({ broadcaster })`. A function declaration (not an arrow
175
+ * function) so laravel-echo can call it with `new`.
176
+ */
177
+ export function broadcaster(options?: Options): Connector {
178
+ return new Connector(options);
179
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Formats event names the same way laravel-echo does, so `listen('OrderCreated')` matches
3
+ * `App\Events\OrderCreated` and `listen('.custom-name')` matches `custom-name`.
4
+ */
5
+ export class EventFormatter {
6
+ constructor(private namespace: string | false) {
7
+ }
8
+
9
+ format(event: string): string {
10
+ if (['.', '\\'].includes(event.charAt(0))) {
11
+ return event.substring(1);
12
+ }
13
+
14
+ if (this.namespace) {
15
+ event = this.namespace + '.' + event;
16
+ }
17
+
18
+ return event.replace(/\./g, '\\');
19
+ }
20
+
21
+ setNamespace(namespace: string | false): void {
22
+ this.namespace = namespace;
23
+ }
24
+ }