@spectrum-ts/imessage 6.0.0 → 6.1.1
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/index.js +68 -34
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -219,6 +219,7 @@ const log$3 = createLogger("spectrum.imessage.auth");
|
|
|
219
219
|
const RENEWAL_RATIO = .8;
|
|
220
220
|
const EXPIRY_BUFFER_MS = 3e4;
|
|
221
221
|
const RETRY_DELAY_MS = 3e4;
|
|
222
|
+
const FORCE_REFRESH_MIN_INTERVAL_MS = 5e3;
|
|
222
223
|
const cloudAuthState = /* @__PURE__ */ new WeakMap();
|
|
223
224
|
const requirePhone = (data, instanceId) => {
|
|
224
225
|
const phone = data.numbers?.[instanceId];
|
|
@@ -231,6 +232,8 @@ async function createCloudClients(projectId, projectSecret) {
|
|
|
231
232
|
let disposed = false;
|
|
232
233
|
let renewalTimer;
|
|
233
234
|
let refreshFailures = 0;
|
|
235
|
+
let refreshInFlight;
|
|
236
|
+
let lastRefreshAt = Date.now();
|
|
234
237
|
const records = [];
|
|
235
238
|
const syncPhones = (data) => {
|
|
236
239
|
for (const { entry, instanceId } of records) entry.phone = requirePhone(data, instanceId);
|
|
@@ -249,39 +252,66 @@ async function createCloudClients(projectId, projectSecret) {
|
|
|
249
252
|
...errorAttrs(error)
|
|
250
253
|
}, error);
|
|
251
254
|
};
|
|
255
|
+
const refreshNow = async () => {
|
|
256
|
+
tokenData = await cloud.issueImessageTokens(projectId, projectSecret);
|
|
257
|
+
tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
|
|
258
|
+
lastRefreshAt = Date.now();
|
|
259
|
+
if (tokenData.type === "dedicated") syncPhones(tokenData);
|
|
260
|
+
onRefreshSuccess();
|
|
261
|
+
scheduleRenewal();
|
|
262
|
+
};
|
|
263
|
+
const coalescedRefresh = () => {
|
|
264
|
+
if (!refreshInFlight) refreshInFlight = refreshNow().finally(() => {
|
|
265
|
+
refreshInFlight = void 0;
|
|
266
|
+
});
|
|
267
|
+
return refreshInFlight;
|
|
268
|
+
};
|
|
252
269
|
const scheduleRenewal = () => {
|
|
253
270
|
if (disposed) return;
|
|
271
|
+
if (renewalTimer !== void 0) {
|
|
272
|
+
clearTimeout(renewalTimer);
|
|
273
|
+
renewalTimer = void 0;
|
|
274
|
+
}
|
|
254
275
|
const ttlMs = tokenData.expiresIn * 1e3;
|
|
255
276
|
const renewInMs = Math.max(ttlMs * RENEWAL_RATIO, 5e3);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
tokenData = await cloud.issueImessageTokens(projectId, projectSecret);
|
|
259
|
-
tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
|
|
260
|
-
if (tokenData.type === "dedicated") syncPhones(tokenData);
|
|
261
|
-
onRefreshSuccess();
|
|
262
|
-
scheduleRenewal();
|
|
263
|
-
} catch (error) {
|
|
277
|
+
const runScheduledRefresh = () => {
|
|
278
|
+
coalescedRefresh().catch((error) => {
|
|
264
279
|
onRefreshFailure(error);
|
|
265
|
-
|
|
280
|
+
if (disposed) return;
|
|
281
|
+
renewalTimer = setTimeout(runScheduledRefresh, RETRY_DELAY_MS);
|
|
266
282
|
renewalTimer?.unref?.();
|
|
267
|
-
}
|
|
268
|
-
}
|
|
283
|
+
});
|
|
284
|
+
};
|
|
285
|
+
renewalTimer = setTimeout(runScheduledRefresh, renewInMs);
|
|
269
286
|
renewalTimer?.unref?.();
|
|
270
287
|
};
|
|
271
288
|
scheduleRenewal();
|
|
272
289
|
const refreshIfNeeded = async () => {
|
|
273
290
|
if (Date.now() < tokenExpiresAt - EXPIRY_BUFFER_MS) return;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
291
|
+
await coalescedRefresh();
|
|
292
|
+
};
|
|
293
|
+
const forceRefresh = async () => {
|
|
294
|
+
if (Date.now() - lastRefreshAt < FORCE_REFRESH_MIN_INTERVAL_MS) return;
|
|
295
|
+
await coalescedRefresh();
|
|
296
|
+
};
|
|
297
|
+
const dispose = () => {
|
|
298
|
+
disposed = true;
|
|
299
|
+
if (renewalTimer !== void 0) {
|
|
300
|
+
clearTimeout(renewalTimer);
|
|
301
|
+
renewalTimer = void 0;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const cloudAuth = {
|
|
305
|
+
dispose,
|
|
306
|
+
forceRefresh
|
|
279
307
|
};
|
|
280
308
|
if (tokenData.type === "shared") {
|
|
281
309
|
const entries = [{
|
|
282
310
|
phone: SHARED_PHONE,
|
|
283
311
|
client: createClient({
|
|
284
312
|
address: process.env.SPECTRUM_IMESSAGE_ADDRESS ?? "imessage.spectrum.photon.codes:443",
|
|
313
|
+
autoIdempotency: true,
|
|
314
|
+
retry: true,
|
|
285
315
|
tls: true,
|
|
286
316
|
token: async () => {
|
|
287
317
|
await refreshIfNeeded();
|
|
@@ -289,13 +319,7 @@ async function createCloudClients(projectId, projectSecret) {
|
|
|
289
319
|
}
|
|
290
320
|
})
|
|
291
321
|
}];
|
|
292
|
-
cloudAuthState.set(entries,
|
|
293
|
-
disposed = true;
|
|
294
|
-
if (renewalTimer !== void 0) {
|
|
295
|
-
clearTimeout(renewalTimer);
|
|
296
|
-
renewalTimer = void 0;
|
|
297
|
-
}
|
|
298
|
-
} });
|
|
322
|
+
cloudAuthState.set(entries, cloudAuth);
|
|
299
323
|
return entries;
|
|
300
324
|
}
|
|
301
325
|
const dedicated = tokenData;
|
|
@@ -304,6 +328,8 @@ async function createCloudClients(projectId, projectSecret) {
|
|
|
304
328
|
phone: requirePhone(dedicated, instanceId),
|
|
305
329
|
client: createClient({
|
|
306
330
|
address: `${instanceId}.imsg.photon.codes:443`,
|
|
331
|
+
autoIdempotency: true,
|
|
332
|
+
retry: true,
|
|
307
333
|
tls: true,
|
|
308
334
|
token: async () => {
|
|
309
335
|
await refreshIfNeeded();
|
|
@@ -317,13 +343,7 @@ async function createCloudClients(projectId, projectSecret) {
|
|
|
317
343
|
});
|
|
318
344
|
}
|
|
319
345
|
const entries = records.map((r) => r.entry);
|
|
320
|
-
cloudAuthState.set(entries,
|
|
321
|
-
disposed = true;
|
|
322
|
-
if (renewalTimer !== void 0) {
|
|
323
|
-
clearTimeout(renewalTimer);
|
|
324
|
-
renewalTimer = void 0;
|
|
325
|
-
}
|
|
326
|
-
} });
|
|
346
|
+
cloudAuthState.set(entries, cloudAuth);
|
|
327
347
|
return entries;
|
|
328
348
|
}
|
|
329
349
|
async function disposeCloudAuth(clients) {
|
|
@@ -333,6 +353,15 @@ async function disposeCloudAuth(clients) {
|
|
|
333
353
|
cloudAuthState.delete(clients);
|
|
334
354
|
}
|
|
335
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* The recover hook for a cloud-backed client array: forces a token re-mint so a
|
|
358
|
+
* persistently-failing stream (server rejecting an unexpired token after a
|
|
359
|
+
* restart) gets a fresh bearer on its next reconnect. Returns undefined for
|
|
360
|
+
* explicitly-configured (static-token) clients, which have nothing to re-mint.
|
|
361
|
+
*/
|
|
362
|
+
function getCloudRecover(clients) {
|
|
363
|
+
return cloudAuthState.get(clients)?.forceRefresh;
|
|
364
|
+
}
|
|
336
365
|
//#endregion
|
|
337
366
|
//#region src/cache.ts
|
|
338
367
|
const DEFAULT_MAX = 1e3;
|
|
@@ -1776,29 +1805,32 @@ async function* afterCursor(stream, cursor) {
|
|
|
1776
1805
|
const withClose = (source, cursor) => Object.assign(afterCursor(source, cursor), { close: async () => {
|
|
1777
1806
|
await source.close?.();
|
|
1778
1807
|
} });
|
|
1779
|
-
const messageStream = (client, phone, onInbound) => resumableOrderedStream({
|
|
1808
|
+
const messageStream = (client, phone, onInbound, recover) => resumableOrderedStream({
|
|
1780
1809
|
fetchMissed: (cursor) => catchUpEvents(client, cursor, isMessageEvent),
|
|
1781
1810
|
isCursorRejectedError: isCursorRejectedIMessageError,
|
|
1782
1811
|
label: streamLabel("messages", phone),
|
|
1812
|
+
recover,
|
|
1783
1813
|
processLive: (event) => toMessageItem(client, event, phone, String(event.sequence), onInbound),
|
|
1784
1814
|
processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toMessageItem(client, event, phone, String(event.sequence), onInbound),
|
|
1785
1815
|
subscribeLive: (cursor) => withClose(client.messages.subscribeEvents(), cursor)
|
|
1786
1816
|
});
|
|
1787
|
-
const pollStream = (client, pollCache, phone) => resumableOrderedStream({
|
|
1817
|
+
const pollStream = (client, pollCache, phone, recover) => resumableOrderedStream({
|
|
1788
1818
|
fetchMissed: (cursor) => catchUpEvents(client, cursor, isPollEvent),
|
|
1789
1819
|
isCursorRejectedError: isCursorRejectedIMessageError,
|
|
1790
1820
|
label: streamLabel("polls", phone),
|
|
1821
|
+
recover,
|
|
1791
1822
|
processLive: (event) => toPollItem(client, pollCache, event, phone, String(event.sequence)),
|
|
1792
1823
|
processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toPollItem(client, pollCache, event, phone, String(event.sequence)),
|
|
1793
1824
|
subscribeLive: (cursor) => withClose(client.polls.subscribeEvents(), cursor)
|
|
1794
1825
|
});
|
|
1795
|
-
const clientStream = (client, pollCache, phone, onInbound) => mergeStreams([messageStream(client, phone, onInbound), pollStream(client, pollCache, phone)]);
|
|
1826
|
+
const clientStream = (client, pollCache, phone, onInbound, recover) => mergeStreams([messageStream(client, phone, onInbound, recover), pollStream(client, pollCache, phone, recover)]);
|
|
1796
1827
|
const messages$1 = (clients, projectConfig) => {
|
|
1797
1828
|
const pollCache = getPollCache(clients);
|
|
1798
1829
|
const shareEnabled = projectConfig?.profile?.imessageSynced === true;
|
|
1830
|
+
const recover = getCloudRecover(clients);
|
|
1799
1831
|
return mergeStreams(clients.map((entry) => {
|
|
1800
1832
|
const tracker = shareEnabled ? getContactShareTracker(entry.client) : void 0;
|
|
1801
|
-
return clientStream(entry.client, pollCache, entry.phone, tracker ? (chatGuid) => tracker.maybeShare(chatGuid) : void 0);
|
|
1833
|
+
return clientStream(entry.client, pollCache, entry.phone, tracker ? (chatGuid) => tracker.maybeShare(chatGuid) : void 0, recover);
|
|
1802
1834
|
}));
|
|
1803
1835
|
};
|
|
1804
1836
|
//#endregion
|
|
@@ -2056,6 +2088,8 @@ const imessage = definePlatform("iMessage", {
|
|
|
2056
2088
|
phone: e.phone,
|
|
2057
2089
|
client: createClient({
|
|
2058
2090
|
address: e.address,
|
|
2091
|
+
autoIdempotency: true,
|
|
2092
|
+
retry: true,
|
|
2059
2093
|
tls: true,
|
|
2060
2094
|
token: e.token
|
|
2061
2095
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spectrum-ts/imessage",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.1.1",
|
|
4
4
|
"description": "iMessage provider for spectrum-ts — local and remote (advanced) modes.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"label": "iMessage"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@photon-ai/advanced-imessage": "^0.
|
|
34
|
+
"@photon-ai/advanced-imessage": "^0.12.0",
|
|
35
35
|
"@photon-ai/imessage-kit": "^3.0.0",
|
|
36
36
|
"@photon-ai/otel": "^1.0.0",
|
|
37
37
|
"lru-cache": "^11.0.0",
|