zapo-js 1.6.0 → 1.6.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.
- package/dist/client/WaClientFactory.js +2 -1
- package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +2 -2
- package/dist/client/coordinators/WaMessageDispatchCoordinator.js +6 -36
- package/dist/client/coordinators/WaPrivacyCoordinator.d.ts +18 -5
- package/dist/client/coordinators/WaPrivacyCoordinator.js +29 -4
- package/dist/esm/client/WaClientFactory.js +2 -1
- package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +6 -36
- package/dist/esm/client/coordinators/WaPrivacyCoordinator.js +30 -5
- package/dist/esm/signal/api/SignalDeviceSyncApi.js +62 -1
- package/dist/esm/store/createStore.js +119 -57
- package/dist/esm/transport/WaComms.js +7 -0
- package/dist/esm/transport/keepalive/WaKeepAlive.js +24 -7
- package/dist/esm/transport/node/builders/privacy.js +32 -2
- package/dist/signal/api/SignalDeviceSyncApi.d.ts +27 -0
- package/dist/signal/api/SignalDeviceSyncApi.js +61 -0
- package/dist/store/createStore.js +119 -57
- package/dist/store/types.d.ts +31 -0
- package/dist/transport/WaComms.d.ts +3 -0
- package/dist/transport/WaComms.js +7 -0
- package/dist/transport/keepalive/WaKeepAlive.d.ts +8 -2
- package/dist/transport/keepalive/WaKeepAlive.js +24 -7
- package/dist/transport/node/builders/privacy.d.ts +26 -1
- package/dist/transport/node/builders/privacy.js +34 -3
- package/package.json +1 -1
|
@@ -78,6 +78,7 @@ export class WaComms {
|
|
|
78
78
|
this.usedResumeHandshake = false;
|
|
79
79
|
this.noiseSession = null;
|
|
80
80
|
this.lastServerStaticKey = null;
|
|
81
|
+
this.lastInboundAtMs = 0;
|
|
81
82
|
this.frameProcessingQueue = Promise.resolve();
|
|
82
83
|
this.frameHandlerQueue = new BoundedTaskQueue(WA_FRAME_HANDLER_QUEUE_MAX_SIZE, WA_FRAME_HANDLER_MAX_CONCURRENCY);
|
|
83
84
|
}
|
|
@@ -205,6 +206,10 @@ export class WaComms {
|
|
|
205
206
|
const wire = await this.noiseSession.encryptFrame(payload);
|
|
206
207
|
await this.socket.send(wire);
|
|
207
208
|
}
|
|
209
|
+
/** Timestamp (`Date.now()` ms) of the last raw payload received from the socket; `0` before any inbound. */
|
|
210
|
+
getLastInboundAtMs() {
|
|
211
|
+
return this.lastInboundAtMs;
|
|
212
|
+
}
|
|
208
213
|
getCommsState() {
|
|
209
214
|
return {
|
|
210
215
|
started: this.started,
|
|
@@ -285,6 +290,7 @@ export class WaComms {
|
|
|
285
290
|
});
|
|
286
291
|
}
|
|
287
292
|
onSocketMessage(payload) {
|
|
293
|
+
this.lastInboundAtMs = Date.now();
|
|
288
294
|
this.frameProcessingQueue = this.frameProcessingQueue.then(() => this.processSocketPayload(payload), () => this.processSocketPayload(payload));
|
|
289
295
|
}
|
|
290
296
|
async processSocketPayload(payload) {
|
|
@@ -522,6 +528,7 @@ export class WaComms {
|
|
|
522
528
|
this.resumeHandshakeFailures = 0;
|
|
523
529
|
}
|
|
524
530
|
this.noiseSession = null;
|
|
531
|
+
this.lastInboundAtMs = 0;
|
|
525
532
|
this.clearPendingFrames();
|
|
526
533
|
this.pendingFramesOverflowClosing = false;
|
|
527
534
|
if (options.clearHandlers) {
|
|
@@ -5,8 +5,13 @@ const KEEPALIVE_DEFAULT_MIN_JITTER_MS = 250;
|
|
|
5
5
|
const KEEPALIVE_MAX_JITTER_RATIO = 0.5;
|
|
6
6
|
/**
|
|
7
7
|
* Periodically pings the server with a ping IQ to detect dead sockets and
|
|
8
|
-
* measure clock skew.
|
|
9
|
-
*
|
|
8
|
+
* measure clock skew. A ping is skipped while another ping is in flight, or
|
|
9
|
+
* while other queries are pending and the socket shows recent inbound
|
|
10
|
+
* activity. A socket silent past the dead-socket timeout is pinged even with
|
|
11
|
+
* queries pending: pending queries on a silent socket are evidence of a dead
|
|
12
|
+
* connection, so steady outbound traffic must not starve detection. On ping
|
|
13
|
+
* failure it rejects pending queries and resumes the socket via the
|
|
14
|
+
* configured `getComms()`.
|
|
10
15
|
*/
|
|
11
16
|
export class WaKeepAlive {
|
|
12
17
|
constructor(options) {
|
|
@@ -68,14 +73,25 @@ export class WaKeepAlive {
|
|
|
68
73
|
this.schedule(generation);
|
|
69
74
|
return;
|
|
70
75
|
}
|
|
71
|
-
if (this.inFlight
|
|
72
|
-
this.logger.trace('keepalive skipped:
|
|
73
|
-
inFlight: this.inFlight,
|
|
74
|
-
pendingQueries: this.nodeOrchestrator.hasPending()
|
|
75
|
-
});
|
|
76
|
+
if (this.inFlight) {
|
|
77
|
+
this.logger.trace('keepalive skipped: ping already in flight');
|
|
76
78
|
this.schedule(generation);
|
|
77
79
|
return;
|
|
78
80
|
}
|
|
81
|
+
if (this.nodeOrchestrator.hasPending()) {
|
|
82
|
+
const inboundIdleMs = Date.now() - comms.getLastInboundAtMs();
|
|
83
|
+
if (inboundIdleMs < this.timeoutMs) {
|
|
84
|
+
this.logger.trace('keepalive skipped: pending queries with recent inbound', {
|
|
85
|
+
inboundIdleMs
|
|
86
|
+
});
|
|
87
|
+
this.schedule(generation);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.logger.debug('keepalive pinging despite pending queries: inbound is silent', {
|
|
91
|
+
inboundIdleMs,
|
|
92
|
+
timeoutMs: this.timeoutMs
|
|
93
|
+
});
|
|
94
|
+
}
|
|
79
95
|
this.inFlight = true;
|
|
80
96
|
const startedAt = Date.now();
|
|
81
97
|
try {
|
|
@@ -108,6 +124,7 @@ export class WaKeepAlive {
|
|
|
108
124
|
this.logger.warn('keepalive ping failed, reconnecting socket', {
|
|
109
125
|
message: toError(error).message
|
|
110
126
|
});
|
|
127
|
+
this.nodeOrchestrator.clearPending(new Error('socket resume requested by keepalive ping failure'));
|
|
111
128
|
try {
|
|
112
129
|
await comms.closeSocketAndResume();
|
|
113
130
|
}
|
|
@@ -38,11 +38,41 @@ export function buildGetPrivacyDisallowedListIq(category) {
|
|
|
38
38
|
export function buildGetBlocklistIq() {
|
|
39
39
|
return buildIqNode(WA_IQ_TYPES.GET, WA_DEFAULTS.HOST_DOMAIN, WA_XMLNS.BLOCKLIST);
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Builds the blocklist `set` IQ for a block action. LID-migrated targets are
|
|
43
|
+
* addressed by the LID jid plus an identifier attribute: `pn_jid` when the
|
|
44
|
+
* phone jid is known, else `unknown_identifier="true"`. Non-migrated targets
|
|
45
|
+
* are addressed by the phone jid alone. The server rejects a block that
|
|
46
|
+
* addresses a migrated account by phone jid or omits the identifier
|
|
47
|
+
* (`400: bad-request`).
|
|
48
|
+
*/
|
|
49
|
+
export function buildBlocklistBlockIq(target) {
|
|
50
|
+
let attrs;
|
|
51
|
+
if (target.lidJid !== null) {
|
|
52
|
+
attrs =
|
|
53
|
+
target.pnJid !== null
|
|
54
|
+
? { action: 'block', jid: target.lidJid, pn_jid: target.pnJid }
|
|
55
|
+
: { action: 'block', jid: target.lidJid, unknown_identifier: 'true' };
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
attrs = { action: 'block', jid: target.pnJid };
|
|
59
|
+
}
|
|
42
60
|
return buildIqNode(WA_IQ_TYPES.SET, WA_DEFAULTS.HOST_DOMAIN, WA_XMLNS.BLOCKLIST, [
|
|
43
61
|
{
|
|
44
62
|
tag: 'item',
|
|
45
|
-
attrs
|
|
63
|
+
attrs
|
|
64
|
+
}
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Builds the blocklist `set` IQ for an unblock action. The server keys
|
|
69
|
+
* migrated entries by LID, so `jid` must be the LID jid when one exists.
|
|
70
|
+
*/
|
|
71
|
+
export function buildBlocklistUnblockIq(jid) {
|
|
72
|
+
return buildIqNode(WA_IQ_TYPES.SET, WA_DEFAULTS.HOST_DOMAIN, WA_XMLNS.BLOCKLIST, [
|
|
73
|
+
{
|
|
74
|
+
tag: 'item',
|
|
75
|
+
attrs: { jid, action: 'unblock' }
|
|
46
76
|
}
|
|
47
77
|
]);
|
|
48
78
|
}
|
|
@@ -29,6 +29,17 @@ export interface SignalLidSyncResult {
|
|
|
29
29
|
*/
|
|
30
30
|
readonly invalid: boolean;
|
|
31
31
|
}
|
|
32
|
+
/** Both addressing forms of a 1:1 user, as far as they could be resolved. */
|
|
33
|
+
export interface SignalUserJidPair {
|
|
34
|
+
/** LID user jid, or `null` when no LID mapping is known. */
|
|
35
|
+
readonly lidJid: string | null;
|
|
36
|
+
/**
|
|
37
|
+
* Phone-number user jid, or `null` when unknown. When resolved through
|
|
38
|
+
* the usync fallback this is the server-canonical form, which may differ
|
|
39
|
+
* from the queried number (e.g. BR 9th digit added).
|
|
40
|
+
*/
|
|
41
|
+
readonly pnJid: string | null;
|
|
42
|
+
}
|
|
32
43
|
/**
|
|
33
44
|
* Resolves the device list and LID mapping for a set of users via the `usync`
|
|
34
45
|
* protocol. Concurrent calls for the same JIDs are deduplicated.
|
|
@@ -56,6 +67,22 @@ export declare class SignalDeviceSyncApi {
|
|
|
56
67
|
* one entry per input JID with `exists` indicating server-side presence.
|
|
57
68
|
*/
|
|
58
69
|
queryLidsByPhoneJids(phoneJids: readonly string[], timeoutMs?: number): Promise<readonly SignalLidSyncResult[]>;
|
|
70
|
+
/**
|
|
71
|
+
* Resolves both addressing forms for a 1:1 user jid (PN or LID input),
|
|
72
|
+
* cache-first via the device-list store (`userJid`/`altUserJid`) with a
|
|
73
|
+
* one-shot {@link queryLidsByPhoneJids} fallback for PN inputs. LID
|
|
74
|
+
* inputs have no reverse lookup - their PN side stays `null` on a cache
|
|
75
|
+
* miss. Store/usync failures are logged at debug and degrade to the
|
|
76
|
+
* forms already known. Inputs that are neither PN nor LID user jids
|
|
77
|
+
* resolve to `{ lidJid: null, pnJid: null }`.
|
|
78
|
+
*/
|
|
79
|
+
resolveUserJidPair(userJid: string, timeoutMs?: number): Promise<SignalUserJidPair>;
|
|
80
|
+
/**
|
|
81
|
+
* Returns the device-list snapshot form of `userJid` that satisfies
|
|
82
|
+
* `matches` (checked against `userJid` then `altUserJid`), or `null` when
|
|
83
|
+
* the store is absent, misses, or fails.
|
|
84
|
+
*/
|
|
85
|
+
private findCachedAltForm;
|
|
59
86
|
/**
|
|
60
87
|
* Enriches existing device-list snapshots with the resolved LID equivalents
|
|
61
88
|
* so {@link WaDeviceListStore.findByAnyUserJid} can later match a retry
|
|
@@ -8,6 +8,7 @@ const usync_1 = require("../../transport/node/builders/usync");
|
|
|
8
8
|
const helpers_1 = require("../../transport/node/helpers");
|
|
9
9
|
const query_1 = require("../../transport/node/query");
|
|
10
10
|
const usync_2 = require("../../transport/node/usync");
|
|
11
|
+
const primitives_1 = require("../../util/primitives");
|
|
11
12
|
/**
|
|
12
13
|
* Resolves the device list and LID mapping for a set of users via the `usync`
|
|
13
14
|
* protocol. Concurrent calls for the same JIDs are deduplicated.
|
|
@@ -142,6 +143,66 @@ class SignalDeviceSyncApi {
|
|
|
142
143
|
await this.propagateAltUserJids(result);
|
|
143
144
|
return result;
|
|
144
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Resolves both addressing forms for a 1:1 user jid (PN or LID input),
|
|
148
|
+
* cache-first via the device-list store (`userJid`/`altUserJid`) with a
|
|
149
|
+
* one-shot {@link queryLidsByPhoneJids} fallback for PN inputs. LID
|
|
150
|
+
* inputs have no reverse lookup - their PN side stays `null` on a cache
|
|
151
|
+
* miss. Store/usync failures are logged at debug and degrade to the
|
|
152
|
+
* forms already known. Inputs that are neither PN nor LID user jids
|
|
153
|
+
* resolve to `{ lidJid: null, pnJid: null }`.
|
|
154
|
+
*/
|
|
155
|
+
async resolveUserJidPair(userJid, timeoutMs = this.defaultTimeoutMs) {
|
|
156
|
+
if ((0, jid_1.isLidJid)(userJid)) {
|
|
157
|
+
return { lidJid: userJid, pnJid: await this.findCachedAltForm(userJid, jid_1.isUserJid) };
|
|
158
|
+
}
|
|
159
|
+
if (!(0, jid_1.isUserJid)(userJid)) {
|
|
160
|
+
return { lidJid: null, pnJid: null };
|
|
161
|
+
}
|
|
162
|
+
const cachedLid = await this.findCachedAltForm(userJid, jid_1.isLidJid);
|
|
163
|
+
if (cachedLid) {
|
|
164
|
+
return { lidJid: cachedLid, pnJid: userJid };
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const results = await this.queryLidsByPhoneJids([userJid], timeoutMs);
|
|
168
|
+
const match = results.find((entry) => entry.queriedJid === userJid);
|
|
169
|
+
if (match?.lidJid) {
|
|
170
|
+
return { lidJid: match.lidJid, pnJid: match.phoneJid };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
this.logger.debug('lid usync resolution failed for jid pair', {
|
|
175
|
+
jid: userJid,
|
|
176
|
+
message: (0, primitives_1.toError)(error).message
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return { lidJid: null, pnJid: userJid };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Returns the device-list snapshot form of `userJid` that satisfies
|
|
183
|
+
* `matches` (checked against `userJid` then `altUserJid`), or `null` when
|
|
184
|
+
* the store is absent, misses, or fails.
|
|
185
|
+
*/
|
|
186
|
+
async findCachedAltForm(userJid, matches) {
|
|
187
|
+
if (!this.deviceListStore)
|
|
188
|
+
return null;
|
|
189
|
+
try {
|
|
190
|
+
const snapshot = await this.deviceListStore.findByAnyUserJid(userJid);
|
|
191
|
+
if (snapshot) {
|
|
192
|
+
if (matches(snapshot.userJid))
|
|
193
|
+
return snapshot.userJid;
|
|
194
|
+
if (snapshot.altUserJid && matches(snapshot.altUserJid))
|
|
195
|
+
return snapshot.altUserJid;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
this.logger.debug('device-list lookup failed for jid pair', {
|
|
200
|
+
jid: userJid,
|
|
201
|
+
message: (0, primitives_1.toError)(error).message
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
145
206
|
/**
|
|
146
207
|
* Enriches existing device-list snapshots with the resolved LID equivalents
|
|
147
208
|
* so {@link WaDeviceListStore.findByAnyUserJid} can later match a retry
|
|
@@ -37,6 +37,7 @@ const signal_store_1 = require("./memory/signal.store");
|
|
|
37
37
|
const thread_store_1 = require("./memory/thread.store");
|
|
38
38
|
const noop_store_1 = require("./noop.store");
|
|
39
39
|
const coercion_1 = require("../util/coercion");
|
|
40
|
+
const primitives_1 = require("../util/primitives");
|
|
40
41
|
const DEFAULT_CACHE_TTLS_MS = Object.freeze({
|
|
41
42
|
retryMs: 60 * 1000,
|
|
42
43
|
groupMetadataMs: 5 * 60 * 1000,
|
|
@@ -112,6 +113,7 @@ function createStore(options) {
|
|
|
112
113
|
messageSecret: (0, coercion_1.resolvePositive)(options.memory?.cacheTtlMs?.messageSecretMs, DEFAULT_CACHE_TTLS_MS.messageSecretMs, 'memory.cacheTtlMs.messageSecretMs')
|
|
113
114
|
});
|
|
114
115
|
const sessions = new Map();
|
|
116
|
+
const pendingSessionDestroys = new Set();
|
|
115
117
|
let storeDestroyed = false;
|
|
116
118
|
return {
|
|
117
119
|
session(sessionId) {
|
|
@@ -151,37 +153,48 @@ function createStore(options) {
|
|
|
151
153
|
? new contact_store_1.WaContactMemoryStore({ maxContacts: ml.contacts })
|
|
152
154
|
: noop_store_1.NOOP_CONTACT_STORE);
|
|
153
155
|
const rawPrivacyToken = resolveStore(id, backends, providers.privacyToken ?? 'memory', 'privacyToken', 'stores', () => new privacy_token_store_1.WaPrivacyTokenMemoryStore(ml.privacyTokens));
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
maxGroups: ml.groupMetadataGroups,
|
|
164
|
-
logger: memoryLogger?.child({
|
|
165
|
-
domain: 'groupMetadata',
|
|
166
|
-
sessionId: id
|
|
156
|
+
const buildCaches = () => ({
|
|
157
|
+
retry: (0, retry_lock_1.withRetryLock)(resolveStore(id, backends, cacheProviders.retry ?? 'memory', 'retry', 'caches', () => cacheProviders.retry === 'memory' || !cacheProviders.retry
|
|
158
|
+
? new retry_store_1.WaRetryMemoryStore(cacheTtlsMs.retry, {
|
|
159
|
+
maxOutboundMessages: ml.retryOutboundMessages,
|
|
160
|
+
maxInboundCounters: ml.retryInboundCounters,
|
|
161
|
+
logger: memoryLogger?.child({
|
|
162
|
+
domain: 'retry',
|
|
163
|
+
sessionId: id
|
|
164
|
+
})
|
|
167
165
|
})
|
|
168
|
-
|
|
169
|
-
:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
? new message_secret_store_1.WaMessageSecretMemoryStore(cacheTtlsMs.messageSecret, {
|
|
178
|
-
maxSecrets: ml.messageSecrets,
|
|
179
|
-
logger: memoryLogger?.child({
|
|
180
|
-
domain: 'messageSecret',
|
|
181
|
-
sessionId: id
|
|
166
|
+
: noop_store_1.NOOP_RETRY_STORE)),
|
|
167
|
+
groupMetadata: (0, group_metadata_lock_1.withGroupMetadataLock)(resolveStore(id, backends, cacheProviders.groupMetadata ?? 'memory', 'groupMetadata', 'caches', () => cacheProviders.groupMetadata === 'memory' ||
|
|
168
|
+
!cacheProviders.groupMetadata
|
|
169
|
+
? new group_metadata_store_1.WaGroupMetadataMemoryStore(cacheTtlsMs.groupMetadata, {
|
|
170
|
+
maxGroups: ml.groupMetadataGroups,
|
|
171
|
+
logger: memoryLogger?.child({
|
|
172
|
+
domain: 'groupMetadata',
|
|
173
|
+
sessionId: id
|
|
174
|
+
})
|
|
182
175
|
})
|
|
183
|
-
|
|
184
|
-
:
|
|
176
|
+
: noop_store_1.NOOP_GROUP_METADATA_STORE)),
|
|
177
|
+
deviceList: (0, device_list_lock_1.withDeviceListLock)(resolveStore(id, backends, cacheProviders.deviceList ?? 'memory', 'deviceList', 'caches', () => cacheProviders.deviceList === 'memory' || !cacheProviders.deviceList
|
|
178
|
+
? new device_list_store_1.WaDeviceListMemoryStore(cacheTtlsMs.deviceList, {
|
|
179
|
+
maxUsers: ml.deviceListUsers,
|
|
180
|
+
logger: memoryLogger?.child({
|
|
181
|
+
domain: 'deviceList',
|
|
182
|
+
sessionId: id
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
: noop_store_1.NOOP_DEVICE_LIST_STORE)),
|
|
186
|
+
messageSecret: (0, message_secret_lock_1.withMessageSecretLock)(resolveStore(id, backends, cacheProviders.messageSecret ?? 'memory', 'messageSecret', 'caches', () => cacheProviders.messageSecret === 'memory' ||
|
|
187
|
+
!cacheProviders.messageSecret
|
|
188
|
+
? new message_secret_store_1.WaMessageSecretMemoryStore(cacheTtlsMs.messageSecret, {
|
|
189
|
+
maxSecrets: ml.messageSecrets,
|
|
190
|
+
logger: memoryLogger?.child({
|
|
191
|
+
domain: 'messageSecret',
|
|
192
|
+
sessionId: id
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
: noop_store_1.NOOP_MESSAGE_SECRET_STORE))
|
|
196
|
+
});
|
|
197
|
+
let caches = buildCaches();
|
|
185
198
|
const authStore = (0, auth_lock_1.withAuthLock)(rawAuth);
|
|
186
199
|
const signalStore = (0, signal_lock_1.withSignalLock)(rawSignal);
|
|
187
200
|
const preKeyStore = (0, pre_key_lock_1.withPreKeyLock)(rawPreKey);
|
|
@@ -195,41 +208,72 @@ function createStore(options) {
|
|
|
195
208
|
? (0, sender_key_cache_1.withSenderKeyCache)(rawSenderKey, cacheLayer.limits?.senderKey)
|
|
196
209
|
: rawSenderKey);
|
|
197
210
|
const appStateStore = (0, appstate_lock_1.withAppStateLock)(rawAppState);
|
|
198
|
-
const retryStore = (0, retry_lock_1.withRetryLock)(rawRetry);
|
|
199
|
-
const groupMetadataStore = (0, group_metadata_lock_1.withGroupMetadataLock)(rawGroupMetadata);
|
|
200
|
-
const deviceListStore = (0, device_list_lock_1.withDeviceListLock)(rawDeviceList);
|
|
201
211
|
const messageStore = (0, message_lock_1.withMessageLock)(rawMessages);
|
|
202
|
-
const messageSecretStore = (0, message_secret_lock_1.withMessageSecretLock)(rawMessageSecret);
|
|
203
212
|
const threadStore = (0, thread_lock_1.withThreadLock)(rawThreads);
|
|
204
213
|
const contactStore = (0, contact_lock_1.withContactLock)(rawContacts);
|
|
205
214
|
const privacyTokenStore = (0, privacy_token_lock_1.withPrivacyTokenLock)(cacheLayer.privacyToken && usesBackend(providers.privacyToken)
|
|
206
215
|
? (0, privacy_token_cache_1.withPrivacyTokenCache)(rawPrivacyToken, cacheLayer.limits?.privacyToken)
|
|
207
216
|
: rawPrivacyToken);
|
|
208
|
-
let cachesDestroyed = false;
|
|
209
217
|
let sessionDestroyed = false;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
messageSecretStore.clear()
|
|
218
|
+
let destroyPromise = null;
|
|
219
|
+
let cacheLifecycle = Promise.resolve();
|
|
220
|
+
const teardownCaches = async (target) => {
|
|
221
|
+
const cleared = await Promise.allSettled([
|
|
222
|
+
target.retry.clear(),
|
|
223
|
+
target.groupMetadata.clear(),
|
|
224
|
+
target.deviceList.clear(),
|
|
225
|
+
target.messageSecret.clear()
|
|
219
226
|
]);
|
|
220
|
-
await Promise.
|
|
221
|
-
destroyIfSupported(
|
|
222
|
-
destroyIfSupported(
|
|
223
|
-
destroyIfSupported(
|
|
224
|
-
destroyIfSupported(
|
|
227
|
+
const destroyed = await Promise.allSettled([
|
|
228
|
+
destroyIfSupported(target.retry),
|
|
229
|
+
destroyIfSupported(target.groupMetadata),
|
|
230
|
+
destroyIfSupported(target.deviceList),
|
|
231
|
+
destroyIfSupported(target.messageSecret)
|
|
225
232
|
]);
|
|
233
|
+
const failures = [...cleared, ...destroyed].filter((result) => result.status === 'rejected');
|
|
234
|
+
if (failures.length > 0) {
|
|
235
|
+
storeLogger?.warn('cache teardown had failures', {
|
|
236
|
+
sessionId: id,
|
|
237
|
+
droppedCount: failures.length,
|
|
238
|
+
totalExpected: cleared.length + destroyed.length,
|
|
239
|
+
sample: (0, primitives_1.toError)(failures[0].reason).message
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return failures.length;
|
|
243
|
+
};
|
|
244
|
+
const destroyCaches = () => {
|
|
245
|
+
const run = cacheLifecycle.then(async () => {
|
|
246
|
+
if (sessionDestroyed)
|
|
247
|
+
return;
|
|
248
|
+
const failureCount = await teardownCaches(caches);
|
|
249
|
+
caches = buildCaches();
|
|
250
|
+
if (failureCount > 0) {
|
|
251
|
+
throw new Error(`cache reset finished with ${failureCount} teardown failure(s); ` +
|
|
252
|
+
'fresh caches are in place but old persistent entries may remain');
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
cacheLifecycle = run.then(() => undefined, () => undefined);
|
|
256
|
+
return run;
|
|
226
257
|
};
|
|
227
|
-
const destroy =
|
|
228
|
-
if (
|
|
229
|
-
|
|
258
|
+
const destroy = () => {
|
|
259
|
+
if (!destroyPromise) {
|
|
260
|
+
const pending = destroyInternal().finally(() => pendingSessionDestroys.delete(pending));
|
|
261
|
+
destroyPromise = pending;
|
|
262
|
+
pendingSessionDestroys.add(pending);
|
|
263
|
+
}
|
|
264
|
+
return destroyPromise;
|
|
265
|
+
};
|
|
266
|
+
const destroyInternal = async () => {
|
|
230
267
|
sessionDestroyed = true;
|
|
231
|
-
|
|
232
|
-
|
|
268
|
+
if (sessions.get(id) === storeSession) {
|
|
269
|
+
sessions.delete(id);
|
|
270
|
+
}
|
|
271
|
+
await cacheLifecycle;
|
|
272
|
+
await teardownCaches(caches);
|
|
273
|
+
await destroyPersistentStores();
|
|
274
|
+
};
|
|
275
|
+
const destroyPersistentStores = async () => {
|
|
276
|
+
const results = await Promise.allSettled([
|
|
233
277
|
destroyIfSupported(authStore),
|
|
234
278
|
destroyIfSupported(signalStore),
|
|
235
279
|
destroyIfSupported(preKeyStore),
|
|
@@ -242,6 +286,15 @@ function createStore(options) {
|
|
|
242
286
|
destroyIfSupported(contactStore),
|
|
243
287
|
destroyIfSupported(privacyTokenStore)
|
|
244
288
|
]);
|
|
289
|
+
const failures = results.filter((result) => result.status === 'rejected');
|
|
290
|
+
if (failures.length > 0) {
|
|
291
|
+
storeLogger?.warn('persistent store teardown had failures', {
|
|
292
|
+
sessionId: id,
|
|
293
|
+
droppedCount: failures.length,
|
|
294
|
+
totalExpected: results.length,
|
|
295
|
+
sample: (0, primitives_1.toError)(failures[0].reason).message
|
|
296
|
+
});
|
|
297
|
+
}
|
|
245
298
|
};
|
|
246
299
|
const storeSession = {
|
|
247
300
|
auth: authStore,
|
|
@@ -251,11 +304,19 @@ function createStore(options) {
|
|
|
251
304
|
identity: identityStore,
|
|
252
305
|
senderKey: senderKeyStore,
|
|
253
306
|
appState: appStateStore,
|
|
254
|
-
retry
|
|
255
|
-
|
|
256
|
-
|
|
307
|
+
get retry() {
|
|
308
|
+
return caches.retry;
|
|
309
|
+
},
|
|
310
|
+
get groupMetadata() {
|
|
311
|
+
return caches.groupMetadata;
|
|
312
|
+
},
|
|
313
|
+
get deviceList() {
|
|
314
|
+
return caches.deviceList;
|
|
315
|
+
},
|
|
257
316
|
messages: messageStore,
|
|
258
|
-
messageSecret
|
|
317
|
+
get messageSecret() {
|
|
318
|
+
return caches.messageSecret;
|
|
319
|
+
},
|
|
259
320
|
threads: threadStore,
|
|
260
321
|
contacts: contactStore,
|
|
261
322
|
privacyToken: privacyTokenStore,
|
|
@@ -276,6 +337,7 @@ function createStore(options) {
|
|
|
276
337
|
const list = Array.from(sessions.values());
|
|
277
338
|
sessions.clear();
|
|
278
339
|
await Promise.all(list.map((s) => s.destroy()));
|
|
340
|
+
await Promise.all(Array.from(pendingSessionDestroys));
|
|
279
341
|
const uniqueBackends = new Set(Object.values(backends));
|
|
280
342
|
await Promise.all(Array.from(uniqueBackends, (backend) => destroyIfSupported(backend)));
|
|
281
343
|
}
|
package/dist/store/types.d.ts
CHANGED
|
@@ -33,12 +33,43 @@ export interface WaStoreSession {
|
|
|
33
33
|
readonly threads: WaThreadStore;
|
|
34
34
|
readonly contacts: WaContactStore;
|
|
35
35
|
readonly privacyToken: WaPrivacyTokenStore;
|
|
36
|
+
/**
|
|
37
|
+
* Tears down the cache domains (retry/groupMetadata/deviceList/
|
|
38
|
+
* messageSecret) and swaps fresh, empty ones into this bundle. The
|
|
39
|
+
* session stays usable; the caches rebuild on demand. The swap happens
|
|
40
|
+
* after the old stores finish tearing down (so a persistent cache
|
|
41
|
+
* backend is never cleared under the fresh stores), which means cache
|
|
42
|
+
* operations issued while the reset is in flight may reject. References
|
|
43
|
+
* to the old cache stores captured before the call (e.g. by a live
|
|
44
|
+
* client) reject afterwards - recreate the client to pick up the fresh
|
|
45
|
+
* stores. Rejects when the old generation's teardown had failures - the
|
|
46
|
+
* fresh caches are still in place, but stale entries may remain in a
|
|
47
|
+
* persistent cache backend.
|
|
48
|
+
*/
|
|
36
49
|
destroyCaches(): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Final teardown of every domain store in this bundle. The bundle is
|
|
52
|
+
* single-shot: every operation on it rejects afterwards. The sessionId
|
|
53
|
+
* is released as soon as destruction starts, so a concurrent
|
|
54
|
+
* `store.session(id)` builds a fresh bundle instead of returning this
|
|
55
|
+
* one. Repeat calls return the same in-flight promise. Teardown
|
|
56
|
+
* failures are logged (warn), never thrown.
|
|
57
|
+
*/
|
|
37
58
|
destroy(): Promise<void>;
|
|
38
59
|
}
|
|
39
60
|
export interface WaStore {
|
|
61
|
+
/**
|
|
62
|
+
* Returns the lock-wrapped per-domain store bundle for `sessionId`,
|
|
63
|
+
* building it on first use and caching it until its `destroy()` (or the
|
|
64
|
+
* store-wide `destroy()`) releases it.
|
|
65
|
+
*/
|
|
40
66
|
session(sessionId: string): WaStoreSession;
|
|
67
|
+
/** Runs {@link WaStoreSession.destroyCaches} on every live session. */
|
|
41
68
|
destroyCaches(): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Destroys every live session bundle and the registered backends. The
|
|
71
|
+
* store is single-shot: `session()` throws afterwards.
|
|
72
|
+
*/
|
|
42
73
|
destroy(): Promise<void>;
|
|
43
74
|
}
|
|
44
75
|
export interface WaStoreBackend {
|
|
@@ -30,6 +30,7 @@ export declare class WaComms {
|
|
|
30
30
|
private usedResumeHandshake;
|
|
31
31
|
private noiseSession;
|
|
32
32
|
private lastServerStaticKey;
|
|
33
|
+
private lastInboundAtMs;
|
|
33
34
|
private frameProcessingQueue;
|
|
34
35
|
private readonly frameHandlerQueue;
|
|
35
36
|
constructor(config: WaCommsConfig, logger?: Logger);
|
|
@@ -41,6 +42,8 @@ export declare class WaComms {
|
|
|
41
42
|
closeSocketAndResume(): Promise<void>;
|
|
42
43
|
closeSocketAndPreventRetry(): Promise<void>;
|
|
43
44
|
sendFrame(payload: Uint8Array): Promise<void>;
|
|
45
|
+
/** Timestamp (`Date.now()` ms) of the last raw payload received from the socket; `0` before any inbound. */
|
|
46
|
+
getLastInboundAtMs(): number;
|
|
44
47
|
getCommsState(): Readonly<WaCommsState>;
|
|
45
48
|
getServerStaticKey(): Uint8Array | null;
|
|
46
49
|
private openSocket;
|
|
@@ -81,6 +81,7 @@ class WaComms {
|
|
|
81
81
|
this.usedResumeHandshake = false;
|
|
82
82
|
this.noiseSession = null;
|
|
83
83
|
this.lastServerStaticKey = null;
|
|
84
|
+
this.lastInboundAtMs = 0;
|
|
84
85
|
this.frameProcessingQueue = Promise.resolve();
|
|
85
86
|
this.frameHandlerQueue = new BoundedTaskQueue_1.BoundedTaskQueue(WA_FRAME_HANDLER_QUEUE_MAX_SIZE, WA_FRAME_HANDLER_MAX_CONCURRENCY);
|
|
86
87
|
}
|
|
@@ -208,6 +209,10 @@ class WaComms {
|
|
|
208
209
|
const wire = await this.noiseSession.encryptFrame(payload);
|
|
209
210
|
await this.socket.send(wire);
|
|
210
211
|
}
|
|
212
|
+
/** Timestamp (`Date.now()` ms) of the last raw payload received from the socket; `0` before any inbound. */
|
|
213
|
+
getLastInboundAtMs() {
|
|
214
|
+
return this.lastInboundAtMs;
|
|
215
|
+
}
|
|
211
216
|
getCommsState() {
|
|
212
217
|
return {
|
|
213
218
|
started: this.started,
|
|
@@ -288,6 +293,7 @@ class WaComms {
|
|
|
288
293
|
});
|
|
289
294
|
}
|
|
290
295
|
onSocketMessage(payload) {
|
|
296
|
+
this.lastInboundAtMs = Date.now();
|
|
291
297
|
this.frameProcessingQueue = this.frameProcessingQueue.then(() => this.processSocketPayload(payload), () => this.processSocketPayload(payload));
|
|
292
298
|
}
|
|
293
299
|
async processSocketPayload(payload) {
|
|
@@ -525,6 +531,7 @@ class WaComms {
|
|
|
525
531
|
this.resumeHandshakeFailures = 0;
|
|
526
532
|
}
|
|
527
533
|
this.noiseSession = null;
|
|
534
|
+
this.lastInboundAtMs = 0;
|
|
528
535
|
this.clearPendingFrames();
|
|
529
536
|
this.pendingFramesOverflowClosing = false;
|
|
530
537
|
if (options.clearHandlers) {
|
|
@@ -5,6 +5,7 @@ interface WaKeepAliveOptions {
|
|
|
5
5
|
readonly logger: Logger;
|
|
6
6
|
readonly nodeOrchestrator: {
|
|
7
7
|
hasPending(): boolean;
|
|
8
|
+
clearPending(reason: Error): void;
|
|
8
9
|
query(node: BinaryNode, timeoutMs?: number): Promise<BinaryNode>;
|
|
9
10
|
};
|
|
10
11
|
readonly getComms: () => WaComms | null;
|
|
@@ -18,8 +19,13 @@ interface WaKeepAliveOptions {
|
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
21
|
* Periodically pings the server with a ping IQ to detect dead sockets and
|
|
21
|
-
* measure clock skew.
|
|
22
|
-
*
|
|
22
|
+
* measure clock skew. A ping is skipped while another ping is in flight, or
|
|
23
|
+
* while other queries are pending and the socket shows recent inbound
|
|
24
|
+
* activity. A socket silent past the dead-socket timeout is pinged even with
|
|
25
|
+
* queries pending: pending queries on a silent socket are evidence of a dead
|
|
26
|
+
* connection, so steady outbound traffic must not starve detection. On ping
|
|
27
|
+
* failure it rejects pending queries and resumes the socket via the
|
|
28
|
+
* configured `getComms()`.
|
|
23
29
|
*/
|
|
24
30
|
export declare class WaKeepAlive {
|
|
25
31
|
private readonly logger;
|