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/.eslintrc.js +1 -0
- package/.github/workflows/ci.yml +49 -45
- package/README.md +93 -17
- package/dist/Channel.d.ts +89 -0
- package/dist/Connector.d.ts +92 -0
- package/dist/EventFormatter.d.ts +10 -0
- package/dist/Websocket.d.ts +78 -0
- package/dist/laravel-echo-api-gateway.common.js +403 -762
- package/dist/laravel-echo-api-gateway.iife.js +487 -846
- package/dist/laravel-echo-api-gateway.js +401 -760
- package/jest.config.js +3 -7
- package/js-src/Channel.ts +74 -62
- package/js-src/Connector.ts +100 -40
- package/js-src/EventFormatter.ts +24 -0
- package/js-src/Websocket.ts +217 -168
- package/js-tests/Connector.test.ts +295 -60
- package/js-tests/EchoCompatibility.test.ts +96 -0
- package/js-tests/helpers.ts +37 -0
- package/package.json +23 -30
- package/phpstan.neon.dist +3 -12
- package/phpunit.xml +2 -2
- package/rollup.config.mjs +14 -0
- package/tsconfig.json +6 -6
- package/dist/js-src/Channel.d.ts +0 -68
- package/dist/js-src/Connector.d.ts +0 -49
- package/dist/js-src/Websocket.d.ts +0 -38
- package/rollup.config.js +0 -31
package/jest.config.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
module.exports = {
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
13
|
+
export class Channel {
|
|
12
14
|
/**
|
|
13
|
-
* The
|
|
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:
|
|
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:
|
|
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
|
|
41
|
+
this.eventFormatter = new EventFormatter(this.options.namespace);
|
|
42
42
|
|
|
43
43
|
this.subscribe();
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* Subscribe to
|
|
47
|
+
* Subscribe to the channel.
|
|
48
48
|
*/
|
|
49
|
-
subscribe():
|
|
50
|
-
this.options
|
|
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
|
|
56
|
+
* Unsubscribe from the channel.
|
|
57
57
|
*/
|
|
58
58
|
unsubscribe(): void {
|
|
59
|
-
this.options
|
|
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
|
|
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
|
|
77
|
+
this.options.debug && console.log(`${LOG_PREFIX} stop listening to ${event} for channel ${this.name} ...`);
|
|
80
78
|
|
|
81
|
-
this.socket.
|
|
79
|
+
this.socket.unbind(this, this.eventFormatter.format(event), callback);
|
|
82
80
|
|
|
83
81
|
return this;
|
|
84
82
|
}
|
|
85
83
|
|
|
86
84
|
/**
|
|
87
|
-
*
|
|
85
|
+
* Listen for a whisper event on the channel instance.
|
|
88
86
|
*/
|
|
89
|
-
|
|
90
|
-
this.
|
|
87
|
+
listenForWhisper(event: string, callback: Function): this {
|
|
88
|
+
return this.listen('.client-' + event, callback);
|
|
89
|
+
}
|
|
91
90
|
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
106
|
+
* Stop listening for notification events on the channel instance.
|
|
101
107
|
*/
|
|
102
|
-
|
|
103
|
-
this.
|
|
108
|
+
stopListeningForNotification(callback?: Function): this {
|
|
109
|
+
return this.stopListening(NOTIFICATION_EVENT, callback);
|
|
110
|
+
}
|
|
104
111
|
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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):
|
|
116
|
-
this.options
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
144
|
+
event: 'client-' + event,
|
|
145
|
+
channel: this.name,
|
|
128
146
|
data,
|
|
129
|
-
|
|
130
|
-
})
|
|
147
|
+
});
|
|
131
148
|
|
|
132
149
|
return this;
|
|
133
150
|
}
|
|
134
151
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
*
|
|
160
|
+
* Presence channels are not supported yet: member tracking is not implemented on the server.
|
|
143
161
|
*/
|
|
144
|
-
joining(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return this
|
|
162
|
+
joining(_callback: Function): this {
|
|
163
|
+
return this;
|
|
148
164
|
}
|
|
149
165
|
|
|
150
166
|
/**
|
|
151
|
-
*
|
|
167
|
+
* Presence channels are not supported yet: member tracking is not implemented on the server.
|
|
152
168
|
*/
|
|
153
|
-
leaving(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return this
|
|
169
|
+
leaving(_callback: Function): this {
|
|
170
|
+
return this;
|
|
157
171
|
}
|
|
158
172
|
}
|
|
159
|
-
|
|
160
|
-
export { PresenceChannel };
|
package/js-src/Connector.ts
CHANGED
|
@@ -1,12 +1,38 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
|
|
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:
|
|
22
|
-
|
|
48
|
+
constructor(options: Options = {}) {
|
|
49
|
+
this.setOptions(options);
|
|
50
|
+
this.connect();
|
|
23
51
|
}
|
|
24
52
|
|
|
25
53
|
/**
|
|
26
|
-
*
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|