wirejs-deploy-amplify-basic 0.0.118-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.
@@ -3,6 +3,6 @@
3
3
  "dependencies": {
4
4
  "jsdom": "^25.0.1",
5
5
  "wirejs-dom": "^1.0.41",
6
- "wirejs-resources": "^0.1.86-realtime"
6
+ "wirejs-resources": "^0.1.87-realtime"
7
7
  }
8
8
  }
@@ -100,37 +100,53 @@ export function subscribe(url, channel, token, authHost, subscriber) {
100
100
  }
101
101
  }
102
102
  };
103
- const notifyClosed = (reason) => {
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.
104
111
  const subscriptionIds = Array.from(channelSubIdLUT.entries())
105
112
  .filter(([urlChannel, _subId]) => urlChannel.startsWith(`${url}#`))
106
113
  .map(([_urlChannel, subId]) => subscriptionIdString(subId));
114
+ const subs = [];
115
+ for (const subscriptionId of subscriptionIds) {
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.
107
123
  for (const subscriptionId of subscriptionIds) {
108
- const subs = subIdSubscribers.get(subscriptionId);
109
- if (subs) {
110
- for (const subscriber of subs) {
111
- if (subscriber.onclose) {
112
- try {
113
- subscriber.onclose(reason);
114
- }
115
- catch (error) {
116
- console.error('Error in subscriber onclose:', error);
117
- }
118
- }
119
- }
120
- }
121
124
  subIdSubscribers.delete(subscriptionId);
122
125
  subscriptionState.delete(subscriptionId);
123
126
  }
124
- channelSubIdLUT.delete(fullChannelName);
125
- connections.delete(urlKey);
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);
138
+ }
139
+ }
126
140
  console.debug('closed', ws);
127
141
  };
128
- ws.onclose = () => notifyClosed('closed');
129
- ws.onerror = () => notifyClosed('error');
142
+ ws.onclose = () => cleanup('closed');
143
+ ws.onerror = () => cleanup('error');
130
144
  }
131
145
  if (!channelSubIdLUT.has(fullChannelName)) {
132
146
  const subscriptionId = subscriptionIdString(crypto.randomUUID());
133
147
  subscriptionState.set(subscriptionId, 'connecting');
148
+ channelSubIdLUT.set(fullChannelName, subscriptionId);
149
+ subIdSubscribers.set(subscriptionId, [subscriber]);
134
150
  const ws = connections.get(urlKey);
135
151
  const subscribe = () => {
136
152
  ws.send(JSON.stringify({
@@ -139,8 +155,6 @@ export function subscribe(url, channel, token, authHost, subscriber) {
139
155
  channel,
140
156
  authorization
141
157
  }));
142
- channelSubIdLUT.set(fullChannelName, subscriptionId);
143
- subIdSubscribers.set(subscriptionId, [subscriber]);
144
158
  };
145
159
  if (ws.readyState === WebSocket.OPEN) {
146
160
  subscribe();
@@ -151,9 +165,21 @@ export function subscribe(url, channel, token, authHost, subscriber) {
151
165
  }
152
166
  else {
153
167
  const subscriptionId = channelSubIdLUT.get(fullChannelName);
154
- subIdSubscribers.get(subscriptionId).push(subscriber);
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}`);
174
+ }
175
+ subs?.push(subscriber);
155
176
  if (subscriptionState.get(subscriptionId) === 'open') {
156
- subscriber.onopen?.();
177
+ try {
178
+ subscriber.onopen?.();
179
+ }
180
+ catch (error) {
181
+ console.error('Error in subscriber onopen:', error);
182
+ }
157
183
  }
158
184
  }
159
185
  }
@@ -165,27 +191,48 @@ export function unsubscribe(url, channel, subscriber) {
165
191
  const subs = subId && subIdSubscribers.get(subId);
166
192
  const sub = subs ? subs.find(s => s === subscriber) : undefined;
167
193
  if (sub && subs) {
168
- sub.onclose?.('unsubscribed');
194
+ try {
195
+ sub.onclose?.('unsubscribed');
196
+ }
197
+ catch (error) {
198
+ console.error('Error in subscriber onclose:', error);
199
+ }
169
200
  const i = subs.indexOf(sub);
170
201
  if (i > -1)
171
202
  subs.splice(i, 1);
172
203
  }
173
204
  if (subs && subs.length === 0) {
174
205
  // No subIdSubscribers left for this channel. We can unsubscribe from channel.
206
+ subscriptionState.delete(subId);
207
+ subIdSubscribers.delete(subId);
208
+ channelSubIdLUT.delete(fullChannelName);
175
209
  ws?.send(JSON.stringify({
176
210
  id: subId,
177
211
  type: 'unsubscribe',
178
212
  }));
179
- subscriptionState.delete(subId);
180
- subIdSubscribers.delete(subId);
181
- channelSubIdLUT.delete(fullChannelName);
182
213
  }
183
- const socketSubs = Array.from(channelSubIdLUT.keys())
184
- .filter(k => k.startsWith(`${url}#`));
185
- if (socketSubs.length === 0) {
186
- // No channels left for this URL. We can close the WebSocket connection.
187
- ws?.close();
188
- connections.delete(urlKey);
189
- console.debug('closed', ws);
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
+ }
190
237
  }
191
238
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wirejs-deploy-amplify-basic",
3
- "version": "0.0.118-realtime",
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.86-realtime"
44
+ "wirejs-resources": "^0.1.87-realtime"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@aws-amplify/backend": "^1.14.0",