@spectrum-ts/whatsapp-business 8.2.0 → 8.2.2

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.
Files changed (2) hide show
  1. package/dist/index.js +79 -22
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ const RENEWAL_RATIO = .8;
10
10
  const EXPIRY_BUFFER_MS = 3e4;
11
11
  const RETRY_DELAY_MS = 3e4;
12
12
  const RESUBSCRIBE_BACKOFF_MS = 500;
13
+ const ignoreCleanupError = () => void 0;
13
14
  const cloudAuthState = /* @__PURE__ */ new WeakMap();
14
15
  async function createCloudClients(projectId, projectSecret) {
15
16
  let tokenData = await cloud.issueWhatsappBusinessTokens(projectId, projectSecret);
@@ -17,6 +18,7 @@ async function createCloudClients(projectId, projectSecret) {
17
18
  let disposed = false;
18
19
  let renewalTimer;
19
20
  let refreshFailures = 0;
21
+ let refreshInFlight;
20
22
  const lines = /* @__PURE__ */ new Map();
21
23
  const buildRawClient = (phoneNumberId) => {
22
24
  const accessToken = tokenData.auth[phoneNumberId];
@@ -58,30 +60,36 @@ async function createCloudClients(projectId, projectSecret) {
58
60
  renewalTimer = void 0;
59
61
  }
60
62
  };
63
+ const refreshNow = async () => {
64
+ await refreshTokens();
65
+ onRefreshSuccess();
66
+ scheduleRenewal();
67
+ };
68
+ const coalescedRefresh = () => {
69
+ if (!refreshInFlight) refreshInFlight = refreshNow().finally(() => {
70
+ refreshInFlight = void 0;
71
+ });
72
+ return refreshInFlight;
73
+ };
61
74
  const scheduleRenewal = () => {
62
75
  if (disposed) return;
63
76
  clearRenewalTimer();
64
77
  const ttlMs = tokenData.expiresIn * 1e3;
65
78
  const renewInMs = Math.max(ttlMs * RENEWAL_RATIO, 5e3);
66
- renewalTimer = setTimeout(async () => {
67
- try {
68
- await refreshTokens();
69
- onRefreshSuccess();
70
- scheduleRenewal();
71
- } catch (err) {
79
+ const runScheduledRefresh = () => {
80
+ coalescedRefresh().catch((err) => {
72
81
  onRefreshFailure(err);
73
- clearRenewalTimer();
74
- renewalTimer = setTimeout(() => scheduleRenewal(), RETRY_DELAY_MS);
82
+ if (disposed) return;
83
+ renewalTimer = setTimeout(runScheduledRefresh, RETRY_DELAY_MS);
75
84
  renewalTimer?.unref?.();
76
- }
77
- }, renewInMs);
85
+ });
86
+ };
87
+ renewalTimer = setTimeout(runScheduledRefresh, renewInMs);
78
88
  renewalTimer?.unref?.();
79
89
  };
80
90
  const refreshIfNeeded = async () => {
81
91
  if (Date.now() < tokenExpiresAt - EXPIRY_BUFFER_MS) return;
82
- await refreshTokens();
83
- onRefreshSuccess();
84
- scheduleRenewal();
92
+ await coalescedRefresh();
85
93
  };
86
94
  scheduleRenewal();
87
95
  const clients = Object.keys(tokenData.auth).map((phoneNumberId) => {
@@ -133,18 +141,45 @@ const buildClientProxy = (state, refresh) => {
133
141
  }
134
142
  };
135
143
  };
144
+ const settleNext = (next) => next.then((result) => ({
145
+ type: "next",
146
+ result
147
+ }), (error) => ({
148
+ type: "error",
149
+ error
150
+ }));
151
+ const closeStream = (stream) => {
152
+ stream.close().catch(ignoreCleanupError);
153
+ };
154
+ const returnIterator = (iterator) => {
155
+ iterator.return?.(void 0).catch(ignoreCleanupError);
156
+ };
136
157
  const pumpOnce = async (ctx) => {
137
158
  const sub = ctx.getCurrent().events.subscribe(ctx.options);
159
+ const iterator = sub[Symbol.asyncIterator]();
160
+ const swapVersion = ctx.swapVersion();
138
161
  ctx.setActive(sub);
139
162
  try {
140
- for await (const event of sub) await ctx.emit(event);
141
- return true;
163
+ while (!ctx.isClosed()) {
164
+ const result = await Promise.race([settleNext(iterator.next()), ctx.waitForSwap(swapVersion).then(() => ({ type: "swap" }))]);
165
+ if (result.type === "swap") {
166
+ closeStream(sub);
167
+ returnIterator(iterator);
168
+ return ctx.isClosed() ? "closed" : "swap";
169
+ }
170
+ if (result.type === "error") throw result.error;
171
+ if (result.result.done) return ctx.isClosed() ? "closed" : "ended";
172
+ await ctx.emit(result.result.value);
173
+ }
174
+ return "closed";
142
175
  } catch (error) {
176
+ closeStream(sub);
177
+ returnIterator(iterator);
143
178
  streamLog$1.warn("whatsapp event stream interrupted; resubscribing", {
144
179
  "spectrum.whatsapp.resubscribe_in_ms": RESUBSCRIBE_BACKOFF_MS,
145
180
  ...errorAttrs(error)
146
181
  }, error);
147
- return false;
182
+ return ctx.isClosed() ? "closed" : "error";
148
183
  } finally {
149
184
  ctx.setActive(void 0);
150
185
  }
@@ -152,25 +187,45 @@ const pumpOnce = async (ctx) => {
152
187
  const resubscribableStream = (state, options) => {
153
188
  let closed = false;
154
189
  let active;
190
+ let swapVersion = 0;
191
+ let wakeSwap;
192
+ const wake = () => {
193
+ wakeSwap?.();
194
+ wakeSwap = void 0;
195
+ };
196
+ const requestResubscribe = () => {
197
+ swapVersion += 1;
198
+ wake();
199
+ active?.close().catch(ignoreCleanupError);
200
+ };
155
201
  const source = stream((emit, end) => {
156
202
  const ctx = {
157
203
  emit,
158
204
  getCurrent: () => state.current,
205
+ isClosed: () => closed,
159
206
  options,
160
207
  setActive: (s) => {
161
208
  active = s;
209
+ },
210
+ swapVersion: () => swapVersion,
211
+ waitForSwap: (version) => {
212
+ if (closed || swapVersion !== version) return Promise.resolve();
213
+ return new Promise((resolve) => {
214
+ wakeSwap = resolve;
215
+ });
162
216
  }
163
217
  };
164
218
  const pump = (async () => {
165
219
  while (!closed) {
166
- await pumpOnce(ctx);
167
- if (!closed) await new Promise((r) => setTimeout(r, RESUBSCRIBE_BACKOFF_MS));
220
+ const result = await pumpOnce(ctx);
221
+ if (!closed && result !== "swap") await new Promise((r) => setTimeout(r, RESUBSCRIBE_BACKOFF_MS));
168
222
  }
169
223
  end();
170
224
  })();
171
225
  return async () => {
172
226
  closed = true;
173
- active?.close().catch(() => void 0);
227
+ wake();
228
+ active?.close().catch(ignoreCleanupError);
174
229
  active = void 0;
175
230
  state.subscriptions.delete(subscription);
176
231
  await pump;
@@ -179,16 +234,18 @@ const resubscribableStream = (state, options) => {
179
234
  const subscription = {
180
235
  close: () => {
181
236
  closed = true;
182
- active?.close().catch(() => void 0);
237
+ wake();
238
+ active?.close().catch(ignoreCleanupError);
183
239
  },
184
240
  swap: () => {
185
- active?.close().catch(() => void 0);
241
+ requestResubscribe();
186
242
  }
187
243
  };
188
244
  state.subscriptions.add(subscription);
189
245
  return new TypedEventStream(source, async () => {
190
246
  closed = true;
191
- active?.close().catch(() => void 0);
247
+ wake();
248
+ active?.close().catch(ignoreCleanupError);
192
249
  state.subscriptions.delete(subscription);
193
250
  await source.close();
194
251
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectrum-ts/whatsapp-business",
3
- "version": "8.2.0",
3
+ "version": "8.2.2",
4
4
  "description": "WhatsApp Business provider for spectrum-ts.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,7 +31,7 @@
31
31
  "label": "WhatsApp Business"
32
32
  },
33
33
  "dependencies": {
34
- "@photon-ai/whatsapp-business": "^0.1.1",
34
+ "@photon-ai/whatsapp-business": "^0.2.0",
35
35
  "mime-types": "^3.0.1",
36
36
  "zod": "^4.2.1"
37
37
  },