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.
@@ -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.85-realtime"
6
+ "wirejs-resources": "^0.1.87-realtime"
7
7
  }
8
8
  }
@@ -1,5 +1,5 @@
1
1
  import type { MessageStreamSubscriber } from "wirejs-resources";
2
- export type ChannelEvent<T = any> = {
2
+ export type ChannelEvent = {
3
3
  type: "subscribe_success" | "data" | "broadcast_error" | "ka";
4
4
  id: string;
5
5
  event: any;
@@ -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 channelSubs = new Map();
14
+ const channelSubIdLUT = new Map();
13
15
  /**
14
16
  * subscription ID -> subscriber
15
17
  */
16
- const subscribers = new Map();
18
+ const subIdSubscribers = new Map();
17
19
  /**
18
20
  * subcription ID -> connection state
19
21
  *
20
- * For when new subscribers are added, we'll want to broadcast the current
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 subscribers.get(sid) || []) {
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 subscribers.get(sid) || []) {
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 notifyClosed = () => {
99
- const subscriptionIds = Array.from(channelSubs.entries())
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
- subscriptionState.set(subscriptionId, 'closed');
104
- const subs = subscribers.get(subscriptionId);
105
- if (subs) {
106
- for (const subscriber of subs) {
107
- if (subscriber.onclose) {
108
- try {
109
- subscriber.onclose();
110
- }
111
- catch (error) {
112
- console.error('Error in subscriber onclose:', error);
113
- }
114
- }
115
- }
116
- subscribers.delete(subscriptionId);
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.log('closed', event);
140
+ console.debug('closed', ws);
120
141
  };
121
- ws.onclose = () => notifyClosed();
122
- ws.onerror = () => notifyClosed();
142
+ ws.onclose = () => cleanup('closed');
143
+ ws.onerror = () => cleanup('error');
123
144
  }
124
- if (!channelSubs.has(fullChannelName)) {
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 = channelSubs.get(fullChannelName);
147
- subscribers.get(subscriptionId).push(subscriber);
148
- if (subscriptionState.get(subscriptionId) === 'open') {
149
- subscriber.onopen?.();
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
- else if (subscriptionState.get(subscriptionId) === 'closed') {
152
- subscriber.onclose?.();
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 = channelSubs.get(fullChannelName);
161
- const subs = subId && subscribers.get(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
- subs.splice(subs.indexOf(sub), 1);
165
- subscribers.set(subId, subs);
166
- sub.onclose?.();
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 subscribers left for this channel. We can unsubscribe from channel.
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
- const socketSubs = Array.from(channelSubs.keys())
179
- .filter(k => k.startsWith(urlKey));
180
- if (socketSubs.length === 0) {
181
- // No channels left for this URL. We can close the WebSocket connection.
182
- ws?.close();
183
- connections.delete(urlKey);
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.117-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.85-realtime"
44
+ "wirejs-resources": "^0.1.87-realtime"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@aws-amplify/backend": "^1.14.0",