wirejs-deploy-amplify-basic 0.0.117-realtime → 0.0.119-realtime
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/client/realtime.js
CHANGED
|
@@ -7,18 +7,23 @@
|
|
|
7
7
|
*/
|
|
8
8
|
const connections = new Map();
|
|
9
9
|
/**
|
|
10
|
+
* Channel subscription ID look-up table.
|
|
11
|
+
*
|
|
10
12
|
* `${URL}#${channel}` -> subscription ID
|
|
11
13
|
*/
|
|
12
|
-
const
|
|
14
|
+
const channelSubIdLUT = new Map();
|
|
13
15
|
/**
|
|
14
16
|
* subscription ID -> subscriber
|
|
15
17
|
*/
|
|
16
|
-
const
|
|
18
|
+
const subIdSubscribers = new Map();
|
|
17
19
|
/**
|
|
18
20
|
* subcription ID -> connection state
|
|
19
21
|
*
|
|
20
|
-
* For when new
|
|
22
|
+
* For when new subIdSubscribers are added, we'll want to broadcast the current
|
|
21
23
|
* state of the connection.
|
|
24
|
+
*
|
|
25
|
+
* "closed"-typed states are not represented here, as they are removed from the map
|
|
26
|
+
* when the WebSocket connection is closed.
|
|
22
27
|
*/
|
|
23
28
|
const subscriptionState = new Map();
|
|
24
29
|
/**
|
|
@@ -74,7 +79,7 @@ export function subscribe(url, channel, token, authHost, subscriber) {
|
|
|
74
79
|
const sid = subscriptionIdString(data.id);
|
|
75
80
|
if (data.type === 'data') {
|
|
76
81
|
const eventData = JSON.parse(data.event);
|
|
77
|
-
for (const subscriber of
|
|
82
|
+
for (const subscriber of subIdSubscribers.get(sid) || []) {
|
|
78
83
|
try {
|
|
79
84
|
subscriber.onmessage(eventData);
|
|
80
85
|
}
|
|
@@ -85,7 +90,7 @@ export function subscribe(url, channel, token, authHost, subscriber) {
|
|
|
85
90
|
}
|
|
86
91
|
else if (data.type === 'subscribe_success') {
|
|
87
92
|
subscriptionState.set(sid, 'open');
|
|
88
|
-
for (const subscriber of
|
|
93
|
+
for (const subscriber of subIdSubscribers.get(sid) || []) {
|
|
89
94
|
try {
|
|
90
95
|
subscriber.onopen?.();
|
|
91
96
|
}
|
|
@@ -95,35 +100,53 @@ export function subscribe(url, channel, token, authHost, subscriber) {
|
|
|
95
100
|
}
|
|
96
101
|
}
|
|
97
102
|
};
|
|
98
|
-
const
|
|
99
|
-
|
|
103
|
+
const cleanup = (reason) => {
|
|
104
|
+
// remove the top-level connection entry *first* so that subsequent
|
|
105
|
+
// subscribers don't try to access a closed WebSocket. they will instead
|
|
106
|
+
// see no connection and a new one will be created.
|
|
107
|
+
connections.delete(urlKey);
|
|
108
|
+
// next. we need to identify all subscriptions to notify -- but wait to notify
|
|
109
|
+
// until after we have removed them from the look-up table so that the callbacks
|
|
110
|
+
// don't interfere with the bookkeeping.
|
|
111
|
+
const subscriptionIds = Array.from(channelSubIdLUT.entries())
|
|
100
112
|
.filter(([urlChannel, _subId]) => urlChannel.startsWith(`${url}#`))
|
|
101
113
|
.map(([_urlChannel, subId]) => subscriptionIdString(subId));
|
|
114
|
+
const subs = [];
|
|
102
115
|
for (const subscriptionId of subscriptionIds) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
subs.concat(subIdSubscribers.get(subscriptionId));
|
|
117
|
+
}
|
|
118
|
+
// same story with channel names.
|
|
119
|
+
const fullChannelNames = Array.from(channelSubIdLUT.keys())
|
|
120
|
+
.filter(k => k.startsWith(`${url}#`));
|
|
121
|
+
// now that we've *identified* everything that requires bookkeeping and
|
|
122
|
+
// notification, we do the bookkeeping ONLY.
|
|
123
|
+
for (const subscriptionId of subscriptionIds) {
|
|
124
|
+
subIdSubscribers.delete(subscriptionId);
|
|
125
|
+
subscriptionState.delete(subscriptionId);
|
|
126
|
+
}
|
|
127
|
+
for (const fullChannelName of fullChannelNames) {
|
|
128
|
+
channelSubIdLUT.delete(fullChannelName);
|
|
129
|
+
}
|
|
130
|
+
// all bookkeeping is done, now we can notify subscribers.
|
|
131
|
+
// they can no longer interfere with the bookkeeping.
|
|
132
|
+
for (const subscriber of subs || []) {
|
|
133
|
+
try {
|
|
134
|
+
subscriber.onclose?.(reason);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
console.error('Error in subscriber onclose:', error);
|
|
117
138
|
}
|
|
118
139
|
}
|
|
119
|
-
console.
|
|
140
|
+
console.debug('closed', ws);
|
|
120
141
|
};
|
|
121
|
-
ws.onclose = () =>
|
|
122
|
-
ws.onerror = () =>
|
|
142
|
+
ws.onclose = () => cleanup('closed');
|
|
143
|
+
ws.onerror = () => cleanup('error');
|
|
123
144
|
}
|
|
124
|
-
if (!
|
|
145
|
+
if (!channelSubIdLUT.has(fullChannelName)) {
|
|
125
146
|
const subscriptionId = subscriptionIdString(crypto.randomUUID());
|
|
126
147
|
subscriptionState.set(subscriptionId, 'connecting');
|
|
148
|
+
channelSubIdLUT.set(fullChannelName, subscriptionId);
|
|
149
|
+
subIdSubscribers.set(subscriptionId, [subscriber]);
|
|
127
150
|
const ws = connections.get(urlKey);
|
|
128
151
|
const subscribe = () => {
|
|
129
152
|
ws.send(JSON.stringify({
|
|
@@ -132,8 +155,6 @@ export function subscribe(url, channel, token, authHost, subscriber) {
|
|
|
132
155
|
channel,
|
|
133
156
|
authorization
|
|
134
157
|
}));
|
|
135
|
-
channelSubs.set(fullChannelName, subscriptionId);
|
|
136
|
-
subscribers.set(subscriptionId, [subscriber]);
|
|
137
158
|
};
|
|
138
159
|
if (ws.readyState === WebSocket.OPEN) {
|
|
139
160
|
subscribe();
|
|
@@ -143,13 +164,22 @@ export function subscribe(url, channel, token, authHost, subscriber) {
|
|
|
143
164
|
}
|
|
144
165
|
}
|
|
145
166
|
else {
|
|
146
|
-
const subscriptionId =
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
167
|
+
const subscriptionId = channelSubIdLUT.get(fullChannelName);
|
|
168
|
+
if (!subscriptionId) {
|
|
169
|
+
throw new Error(`No subscription ID found for channel: ${fullChannelName}`);
|
|
170
|
+
}
|
|
171
|
+
const subs = subIdSubscribers.get(subscriptionId);
|
|
172
|
+
if (!subs) {
|
|
173
|
+
throw new Error(`No subscriber list found for subscription ID: ${subscriptionId}`);
|
|
150
174
|
}
|
|
151
|
-
|
|
152
|
-
|
|
175
|
+
subs?.push(subscriber);
|
|
176
|
+
if (subscriptionState.get(subscriptionId) === 'open') {
|
|
177
|
+
try {
|
|
178
|
+
subscriber.onopen?.();
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
console.error('Error in subscriber onopen:', error);
|
|
182
|
+
}
|
|
153
183
|
}
|
|
154
184
|
}
|
|
155
185
|
}
|
|
@@ -157,29 +187,52 @@ export function unsubscribe(url, channel, subscriber) {
|
|
|
157
187
|
const urlKey = urlString(url);
|
|
158
188
|
const ws = connections.get(urlKey);
|
|
159
189
|
const fullChannelName = fullChannelNameString(urlKey, channel);
|
|
160
|
-
const subId =
|
|
161
|
-
const subs = subId &&
|
|
190
|
+
const subId = channelSubIdLUT.get(fullChannelName);
|
|
191
|
+
const subs = subId && subIdSubscribers.get(subId);
|
|
162
192
|
const sub = subs ? subs.find(s => s === subscriber) : undefined;
|
|
163
193
|
if (sub && subs) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
194
|
+
try {
|
|
195
|
+
sub.onclose?.('unsubscribed');
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
console.error('Error in subscriber onclose:', error);
|
|
199
|
+
}
|
|
200
|
+
const i = subs.indexOf(sub);
|
|
201
|
+
if (i > -1)
|
|
202
|
+
subs.splice(i, 1);
|
|
167
203
|
}
|
|
168
204
|
if (subs && subs.length === 0) {
|
|
169
|
-
// No
|
|
205
|
+
// No subIdSubscribers left for this channel. We can unsubscribe from channel.
|
|
206
|
+
subscriptionState.delete(subId);
|
|
207
|
+
subIdSubscribers.delete(subId);
|
|
208
|
+
channelSubIdLUT.delete(fullChannelName);
|
|
170
209
|
ws?.send(JSON.stringify({
|
|
171
210
|
id: subId,
|
|
172
211
|
type: 'unsubscribe',
|
|
173
212
|
}));
|
|
174
|
-
subscriptionState.delete(subId);
|
|
175
|
-
subscribers.delete(subId);
|
|
176
|
-
channelSubs.delete(fullChannelName);
|
|
177
213
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
214
|
+
// debounce closing the WebSocket connection for use-cases where a client
|
|
215
|
+
// might just be changing channels. ws should only be closed if there are
|
|
216
|
+
// no more subscriptions running over the socket.
|
|
217
|
+
setTimeout(() => {
|
|
218
|
+
const socketSubs = Array.from(channelSubIdLUT.keys())
|
|
219
|
+
.filter(k => k.startsWith(`${url}#`));
|
|
220
|
+
if (socketSubs.length === 0) {
|
|
221
|
+
// No channels left for this URL. We can close the WebSocket connection.
|
|
222
|
+
connections.delete(urlKey);
|
|
223
|
+
ws?.close();
|
|
224
|
+
console.debug('closed', ws);
|
|
225
|
+
}
|
|
226
|
+
}, 5000);
|
|
227
|
+
// tell the subscriber that they have been unsubscribed as the last step so
|
|
228
|
+
// they can clean up any resources they have allocated without interfering
|
|
229
|
+
// with the preceding bookkeeping.
|
|
230
|
+
if (sub) {
|
|
231
|
+
try {
|
|
232
|
+
sub.onclose?.('unsubscribed');
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
console.error('Error in subscriber onclose:', error);
|
|
236
|
+
}
|
|
184
237
|
}
|
|
185
238
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wirejs-deploy-amplify-basic",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.119-realtime",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"recursive-copy": "^2.0.14",
|
|
42
42
|
"rimraf": "^6.0.1",
|
|
43
43
|
"wirejs-dom": "^1.0.41",
|
|
44
|
-
"wirejs-resources": "^0.1.
|
|
44
|
+
"wirejs-resources": "^0.1.87-realtime"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@aws-amplify/backend": "^1.14.0",
|