libzapitu 1.0.0-alpha.20 → 1.0.0-alpha.22
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/lib/Socket/chats.js +1 -1
- package/lib/Socket/messages-recv.js +5 -5
- package/lib/Types/Socket.d.ts +4 -3
- package/lib/Utils/logger.js +4 -1
- package/lib/worker/child.js +86 -10
- package/lib/worker/proxy.js +212 -35
- package/package.json +2 -1
package/lib/Socket/chats.js
CHANGED
|
@@ -740,7 +740,7 @@ const makeChatsSocket = (config) => {
|
|
|
740
740
|
}
|
|
741
741
|
const historyMsg = (0, Utils_1.getHistoryMsg)(msg.message);
|
|
742
742
|
const shouldProcessHistoryMsg = historyMsg
|
|
743
|
-
? shouldSyncHistoryMessage(historyMsg) && Defaults_1.PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType)
|
|
743
|
+
? (await shouldSyncHistoryMessage(historyMsg)) && Defaults_1.PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType)
|
|
744
744
|
: false;
|
|
745
745
|
if (historyMsg && !authState.creds.myAppStateKeyId) {
|
|
746
746
|
logger.warn('skipping app state sync, as myAppStateKeyId is not set');
|
|
@@ -542,7 +542,7 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
542
542
|
fromMe,
|
|
543
543
|
participant: attrs.participant
|
|
544
544
|
};
|
|
545
|
-
if (remoteJid && shouldIgnoreJid(remoteJid) && remoteJid !== '@s.whatsapp.net') {
|
|
545
|
+
if (remoteJid && (await shouldIgnoreJid(remoteJid)) && remoteJid !== '@s.whatsapp.net') {
|
|
546
546
|
logger.debug({ remoteJid }, 'ignoring receipt from jid');
|
|
547
547
|
await sendMessageAck(node);
|
|
548
548
|
return;
|
|
@@ -610,7 +610,7 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
610
610
|
};
|
|
611
611
|
const handleNotification = async (node) => {
|
|
612
612
|
const remoteJid = node.attrs.from;
|
|
613
|
-
if (shouldIgnoreJid(remoteJid) && remoteJid !== '@s.whatsapp.net') {
|
|
613
|
+
if ((await shouldIgnoreJid(remoteJid)) && remoteJid !== '@s.whatsapp.net') {
|
|
614
614
|
logger.debug({ remoteJid, id: node.attrs.id }, 'ignored notification');
|
|
615
615
|
await sendMessageAck(node);
|
|
616
616
|
return;
|
|
@@ -643,7 +643,7 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
643
643
|
};
|
|
644
644
|
const handleMessage = async (node) => {
|
|
645
645
|
var _a, _b, _c, _d, _e;
|
|
646
|
-
if (shouldIgnoreJid(node.attrs.from) && node.attrs.from !== '@s.whatsapp.net') {
|
|
646
|
+
if ((await shouldIgnoreJid(node.attrs.from)) && node.attrs.from !== '@s.whatsapp.net') {
|
|
647
647
|
logger.debug({ key: node.attrs.key }, 'ignored message');
|
|
648
648
|
await sendMessageAck(node);
|
|
649
649
|
return;
|
|
@@ -1165,12 +1165,12 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
1165
1165
|
}
|
|
1166
1166
|
}
|
|
1167
1167
|
};
|
|
1168
|
-
const handlePresenceUpdate = ({ tag, attrs, content }) => {
|
|
1168
|
+
const handlePresenceUpdate = async ({ tag, attrs, content }) => {
|
|
1169
1169
|
var _a;
|
|
1170
1170
|
let presence;
|
|
1171
1171
|
const jid = attrs.from;
|
|
1172
1172
|
const participant = attrs.participant || attrs.from;
|
|
1173
|
-
if (shouldIgnoreJid(jid)) {
|
|
1173
|
+
if (await shouldIgnoreJid(jid)) {
|
|
1174
1174
|
return;
|
|
1175
1175
|
}
|
|
1176
1176
|
if (tag === 'presence') {
|
package/lib/Types/Socket.d.ts
CHANGED
|
@@ -62,7 +62,7 @@ export type SocketConfig = {
|
|
|
62
62
|
/** provide an auth state object to maintain the auth state */
|
|
63
63
|
auth: AuthenticationState;
|
|
64
64
|
/** manage history processing with this control; by default will sync up everything */
|
|
65
|
-
shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => boolean
|
|
65
|
+
shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => boolean | Promise<boolean>;
|
|
66
66
|
/** transaction capability options for SignalKeyStore */
|
|
67
67
|
transactionOpts: TransactionCapabilityOptions;
|
|
68
68
|
/** marks the client as online whenever the socket successfully connects */
|
|
@@ -95,9 +95,10 @@ export type SocketConfig = {
|
|
|
95
95
|
/**
|
|
96
96
|
* Returns if a jid should be ignored,
|
|
97
97
|
* no event for that jid will be triggered.
|
|
98
|
-
* Messages from that jid will also not be decrypted
|
|
98
|
+
* Messages from that jid will also not be decrypted.
|
|
99
|
+
* May return a boolean or a Promise resolving to a boolean.
|
|
99
100
|
* */
|
|
100
|
-
shouldIgnoreJid: (jid: string) => boolean | undefined
|
|
101
|
+
shouldIgnoreJid: (jid: string) => boolean | undefined | Promise<boolean | undefined>;
|
|
101
102
|
/**
|
|
102
103
|
* Optionally patch the message before sending out
|
|
103
104
|
* */
|
package/lib/Utils/logger.js
CHANGED
|
@@ -4,4 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const pino_1 = __importDefault(require("pino"));
|
|
7
|
-
exports.default = (0, pino_1.default)({
|
|
7
|
+
exports.default = (0, pino_1.default)({
|
|
8
|
+
timestamp: () => `,"time":"${new Date().toJSON()}"`,
|
|
9
|
+
level: process.env.BAILEYS_LOG_LEVEL || 'info'
|
|
10
|
+
});
|
package/lib/worker/child.js
CHANGED
|
@@ -12,8 +12,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
* by a unique `socketId` sent in the `init` message from the parent.
|
|
13
13
|
*/
|
|
14
14
|
const worker_threads_1 = require("worker_threads");
|
|
15
|
+
const node_cache_1 = __importDefault(require("@cacheable/node-cache"));
|
|
15
16
|
const Socket_1 = __importDefault(require("../Socket"));
|
|
16
|
-
const
|
|
17
|
+
const libsignal_1 = require("../Signal/libsignal");
|
|
18
|
+
const logger_1 = __importDefault(require("../Utils/logger"));
|
|
17
19
|
// ---------------------------------------------------------------------------
|
|
18
20
|
// Ensure we never run this in the main thread
|
|
19
21
|
// ---------------------------------------------------------------------------
|
|
@@ -28,8 +30,7 @@ const CALLBACK_PROXIED_KEYS = [
|
|
|
28
30
|
'shouldIgnoreJid',
|
|
29
31
|
'patchMessageBeforeSending',
|
|
30
32
|
'cachedGroupMetadata',
|
|
31
|
-
'shouldSyncHistoryMessage'
|
|
32
|
-
'makeSignalRepository',
|
|
33
|
+
'shouldSyncHistoryMessage'
|
|
33
34
|
];
|
|
34
35
|
const sockets = new Map();
|
|
35
36
|
let _reqId = 0;
|
|
@@ -44,7 +45,7 @@ function createNoopLogger() {
|
|
|
44
45
|
debug: noop,
|
|
45
46
|
info: noop,
|
|
46
47
|
warn: noop,
|
|
47
|
-
error: noop
|
|
48
|
+
error: noop
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
// ---------------------------------------------------------------------------
|
|
@@ -136,11 +137,10 @@ function createSocket(socketId, rawConfig) {
|
|
|
136
137
|
const config = reviveBuffers(rawConfig);
|
|
137
138
|
// The proxy replaces the logger with a sentinel string because functions
|
|
138
139
|
// can't be serialized across postMessage. Create a real logger here.
|
|
139
|
-
// Use
|
|
140
|
+
// Use the default logger (level controlled by BAILEYS_LOG_LEVEL) so the
|
|
141
|
+
// real makeWASocket has a working logger.
|
|
140
142
|
const hasLogger = config.logger === '__proxy_logger__';
|
|
141
|
-
const realLogger = hasLogger
|
|
142
|
-
? (0, pino_1.default)({ timestamp: () => `,"time":"${new Date().toJSON()}"` }).child({ worker: 'child', socketId })
|
|
143
|
-
: createNoopLogger();
|
|
143
|
+
const realLogger = hasLogger ? logger_1.default.child({ worker: 'child', socketId }) : createNoopLogger();
|
|
144
144
|
config.logger = realLogger;
|
|
145
145
|
// Create a child logger for our own worker-level logging
|
|
146
146
|
const log = realLogger.child({ component: 'worker-bridge' });
|
|
@@ -152,6 +152,13 @@ function createSocket(socketId, rawConfig) {
|
|
|
152
152
|
config[k] = (...args) => proxyCallback(socketId, k, log, ...args);
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
+
// Recreate the signal repository inside the worker. The repository object
|
|
156
|
+
// contains closures and methods that cannot cross postMessage, so the parent
|
|
157
|
+
// sends a sentinel instead of proxying the factory.
|
|
158
|
+
if (config.makeSignalRepository === '__worker_signal_repository__') {
|
|
159
|
+
;
|
|
160
|
+
config.makeSignalRepository = libsignal_1.makeLibSignalRepository;
|
|
161
|
+
}
|
|
155
162
|
// Replace sentinel keystore with forwarding stubs
|
|
156
163
|
if (((_a = config.auth) === null || _a === void 0 ? void 0 : _a.keys) === '__proxy_keystore__') {
|
|
157
164
|
const keystoreMethods = ['get', 'set', 'clear', 'isInTransaction', 'transaction'];
|
|
@@ -162,8 +169,60 @@ function createSocket(socketId, rawConfig) {
|
|
|
162
169
|
;
|
|
163
170
|
config.auth.keys = stub;
|
|
164
171
|
}
|
|
172
|
+
// Replace worker-local cache sentinels with real cache instances.
|
|
173
|
+
// The parent's cache cannot be cloned (methods live on prototype).
|
|
174
|
+
const cacheKeys = [
|
|
175
|
+
'msgRetryCounterCache',
|
|
176
|
+
'mediaCache',
|
|
177
|
+
'userDevicesCache',
|
|
178
|
+
'callOfferCache',
|
|
179
|
+
'placeholderResendCache'
|
|
180
|
+
];
|
|
181
|
+
for (const k of cacheKeys) {
|
|
182
|
+
if (config[k] === '__worker_cache__') {
|
|
183
|
+
;
|
|
184
|
+
config[k] = new node_cache_1.default({ useClones: false });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
165
187
|
const sock = (0, Socket_1.default)(config);
|
|
166
188
|
log.info('socket created successfully');
|
|
189
|
+
// Send user and authState to the parent when they become available.
|
|
190
|
+
// These are needed for wsocket.user and wsocket.authState on the proxy side.
|
|
191
|
+
const sendSocketInfo = () => {
|
|
192
|
+
try {
|
|
193
|
+
const info = {};
|
|
194
|
+
if (sock.user)
|
|
195
|
+
info.user = sock.user;
|
|
196
|
+
if (sock.authState)
|
|
197
|
+
info.authState = sock.authState;
|
|
198
|
+
if (Object.keys(info).length > 0) {
|
|
199
|
+
worker_threads_1.parentPort.postMessage({ type: 'socket-info', socketId, info });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (_) {
|
|
203
|
+
/* best-effort */
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
// Send immediately (may be empty) and also on connection.open.
|
|
207
|
+
// IMPORTANT: we send socket-info synchronously in the connection.update
|
|
208
|
+
// listener so it arrives at the parent BEFORE the forwarded event.
|
|
209
|
+
// Otherwise wsocket.user would be undefined when the parent's
|
|
210
|
+
// connection.update handler runs.
|
|
211
|
+
sendSocketInfo();
|
|
212
|
+
sock.ev.on('connection.update', (update) => {
|
|
213
|
+
if ((update === null || update === void 0 ? void 0 : update.connection) === 'open') {
|
|
214
|
+
sendSocketInfo();
|
|
215
|
+
}
|
|
216
|
+
// When the socket closes, clean up this socket entry so a new
|
|
217
|
+
// init with a different socketId can reuse this worker.
|
|
218
|
+
if ((update === null || update === void 0 ? void 0 : update.connection) === 'close') {
|
|
219
|
+
const entry = sockets.get(socketId);
|
|
220
|
+
if (entry) {
|
|
221
|
+
log.info('socket closed — removing from worker');
|
|
222
|
+
sockets.delete(socketId);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
167
226
|
sock.ev.on('event', (map) => {
|
|
168
227
|
log.trace({ eventKeys: Object.keys(map) }, 'event → parent');
|
|
169
228
|
try {
|
|
@@ -181,6 +240,23 @@ function createSocket(socketId, rawConfig) {
|
|
|
181
240
|
}
|
|
182
241
|
}
|
|
183
242
|
});
|
|
243
|
+
// Forward raw WS events to the parent so ws.on() works on the proxy side.
|
|
244
|
+
// We forward all events by monkey-patching sock.ws.emit.
|
|
245
|
+
const ws = sock.ws;
|
|
246
|
+
if (ws) {
|
|
247
|
+
const origEmit = ws.emit.bind(ws);
|
|
248
|
+
ws.emit = (event, ...args) => {
|
|
249
|
+
// Forward to parent first (best-effort, non-cloneable data is dropped)
|
|
250
|
+
try {
|
|
251
|
+
worker_threads_1.parentPort.postMessage({ type: 'ws-event', socketId, event, args });
|
|
252
|
+
}
|
|
253
|
+
catch (_) {
|
|
254
|
+
// silently drop — ws events with non-cloneable data are rare
|
|
255
|
+
}
|
|
256
|
+
// Then call the original emit so local listeners still work
|
|
257
|
+
return origEmit(event, ...args);
|
|
258
|
+
};
|
|
259
|
+
}
|
|
184
260
|
sockets.set(socketId, { sock, config, log });
|
|
185
261
|
}
|
|
186
262
|
// ---------------------------------------------------------------------------
|
|
@@ -210,7 +286,7 @@ worker_threads_1.parentPort.on('message', async (msg) => {
|
|
|
210
286
|
type: 'result',
|
|
211
287
|
socketId,
|
|
212
288
|
id,
|
|
213
|
-
error: `Socket ${socketId} not found
|
|
289
|
+
error: `Socket ${socketId} not found`
|
|
214
290
|
});
|
|
215
291
|
return;
|
|
216
292
|
}
|
|
@@ -242,7 +318,7 @@ worker_threads_1.parentPort.on('message', async (msg) => {
|
|
|
242
318
|
type: 'result',
|
|
243
319
|
socketId,
|
|
244
320
|
id,
|
|
245
|
-
error: (err === null || err === void 0 ? void 0 : err.message) || String(err)
|
|
321
|
+
error: (err === null || err === void 0 ? void 0 : err.message) || String(err)
|
|
246
322
|
});
|
|
247
323
|
}
|
|
248
324
|
break;
|
package/lib/worker/proxy.js
CHANGED
|
@@ -36,9 +36,21 @@ const CALLBACK_CONFIG_KEYS = [
|
|
|
36
36
|
'shouldIgnoreJid',
|
|
37
37
|
'patchMessageBeforeSending',
|
|
38
38
|
'cachedGroupMetadata',
|
|
39
|
-
'shouldSyncHistoryMessage'
|
|
40
|
-
'makeSignalRepository',
|
|
39
|
+
'shouldSyncHistoryMessage'
|
|
41
40
|
];
|
|
41
|
+
/**
|
|
42
|
+
* Cache stores that live inside the worker thread. They cannot be safely
|
|
43
|
+
* cloned via postMessage because their methods live on the prototype, so we
|
|
44
|
+
* replace them with a sentinel and recreate a fresh NodeCache in the worker.
|
|
45
|
+
*/
|
|
46
|
+
const CACHE_CONFIG_KEYS = [
|
|
47
|
+
'msgRetryCounterCache',
|
|
48
|
+
'mediaCache',
|
|
49
|
+
'userDevicesCache',
|
|
50
|
+
'callOfferCache',
|
|
51
|
+
'placeholderResendCache'
|
|
52
|
+
];
|
|
53
|
+
const CACHE_SENTINEL = '__worker_cache__';
|
|
42
54
|
// ---------------------------------------------------------------------------
|
|
43
55
|
// Globals
|
|
44
56
|
// ---------------------------------------------------------------------------
|
|
@@ -69,12 +81,16 @@ function createNoopLogger() {
|
|
|
69
81
|
debug: noop,
|
|
70
82
|
info: noop,
|
|
71
83
|
warn: noop,
|
|
72
|
-
error: noop
|
|
84
|
+
error: noop
|
|
73
85
|
};
|
|
74
86
|
}
|
|
75
87
|
/**
|
|
76
88
|
* Recursively revive objects that were sanitized by the child's
|
|
77
89
|
* sanitizeForPostMessage (e.g. Error sentinels).
|
|
90
|
+
*
|
|
91
|
+
* IMPORTANT: structured clone already produces a perfect copy of the data.
|
|
92
|
+
* We only need to recursively look for __error__ sentinels and convert
|
|
93
|
+
* them back to Error instances. Everything else passes through unchanged.
|
|
78
94
|
*/
|
|
79
95
|
function revivePostMessage(value) {
|
|
80
96
|
if (value !== null && typeof value === 'object') {
|
|
@@ -85,13 +101,30 @@ function revivePostMessage(value) {
|
|
|
85
101
|
return err;
|
|
86
102
|
}
|
|
87
103
|
if (Array.isArray(value)) {
|
|
88
|
-
|
|
104
|
+
// Arrays: recurse to revive any Error sentinels inside
|
|
105
|
+
let hasError = false;
|
|
106
|
+
const result = value.map(v => {
|
|
107
|
+
const revived = revivePostMessage(v);
|
|
108
|
+
if (revived !== v)
|
|
109
|
+
hasError = true;
|
|
110
|
+
return revived;
|
|
111
|
+
});
|
|
112
|
+
return hasError ? result : value;
|
|
89
113
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
114
|
+
// Plain objects: only recurse if they might contain __error__ sentinels.
|
|
115
|
+
// Check one level deep to avoid unnecessary cloning of large objects.
|
|
116
|
+
if (value.constructor === Object || value.constructor === undefined) {
|
|
117
|
+
let changed = false;
|
|
118
|
+
const result = {};
|
|
119
|
+
for (const key of Object.keys(value)) {
|
|
120
|
+
const orig = value[key];
|
|
121
|
+
const revived = revivePostMessage(orig);
|
|
122
|
+
if (revived !== orig)
|
|
123
|
+
changed = true;
|
|
124
|
+
result[key] = revived;
|
|
125
|
+
}
|
|
126
|
+
return changed ? result : value;
|
|
93
127
|
}
|
|
94
|
-
return result;
|
|
95
128
|
}
|
|
96
129
|
return value;
|
|
97
130
|
}
|
|
@@ -104,7 +137,7 @@ function getStats() {
|
|
|
104
137
|
size: slots.length,
|
|
105
138
|
active,
|
|
106
139
|
idle: slots.length - active,
|
|
107
|
-
totalSockets: slots.reduce((sum, s) => sum + s.load, 0)
|
|
140
|
+
totalSockets: slots.reduce((sum, s) => sum + s.load, 0)
|
|
108
141
|
};
|
|
109
142
|
}
|
|
110
143
|
/**
|
|
@@ -131,19 +164,23 @@ function acquireSlot() {
|
|
|
131
164
|
}
|
|
132
165
|
function spawnWorker() {
|
|
133
166
|
const worker = new worker_threads_1.Worker(workerScript, {
|
|
134
|
-
execArgv: workerExecArgv ? [...workerExecArgv] : undefined
|
|
167
|
+
execArgv: workerExecArgv ? [...workerExecArgv] : undefined
|
|
135
168
|
});
|
|
136
169
|
const slot = {
|
|
137
170
|
worker,
|
|
138
171
|
load: 0,
|
|
139
172
|
emitters: new Map(),
|
|
173
|
+
wsEmitters: new Map(),
|
|
174
|
+
props: new Map(),
|
|
140
175
|
callbacks: new Map(),
|
|
141
176
|
keystores: new Map(),
|
|
142
177
|
loggers: new Map(),
|
|
143
|
-
|
|
178
|
+
configs: new Map(),
|
|
179
|
+
pending: new Map()
|
|
144
180
|
};
|
|
145
181
|
// Global message handler — dispatches to the right socket's emitter
|
|
146
182
|
worker.on('message', (msg) => {
|
|
183
|
+
var _a, _b, _c, _d;
|
|
147
184
|
if (!msg || typeof msg !== 'object')
|
|
148
185
|
return;
|
|
149
186
|
const socketId = msg.socketId;
|
|
@@ -155,12 +192,56 @@ function spawnWorker() {
|
|
|
155
192
|
if (ev) {
|
|
156
193
|
const map = revivePostMessage(msg.map);
|
|
157
194
|
log === null || log === void 0 ? void 0 : log.trace({ eventKeys: Object.keys(map) }, 'worker event received');
|
|
195
|
+
// Sync creds.update back to the parent's auth.creds object.
|
|
196
|
+
// The worker mutates its own copy; without this the parent's
|
|
197
|
+
// copy stays stale and saveCreds() writes old data.
|
|
198
|
+
if (map['creds.update']) {
|
|
199
|
+
const cfg = slot.configs.get(socketId);
|
|
200
|
+
if ((_a = cfg === null || cfg === void 0 ? void 0 : cfg.auth) === null || _a === void 0 ? void 0 : _a.creds) {
|
|
201
|
+
Object.assign(cfg.auth.creds, map['creds.update']);
|
|
202
|
+
log === null || log === void 0 ? void 0 : log.debug({ me: (_b = cfg.auth.creds.me) === null || _b === void 0 ? void 0 : _b.id }, 'synced creds.update to parent');
|
|
203
|
+
}
|
|
204
|
+
// Also update localProps.user so wsocket.user and its
|
|
205
|
+
// nested properties (id, lid, name, verifiedName, etc.)
|
|
206
|
+
// stay in sync with the worker's authState.creds.me.
|
|
207
|
+
const credsUpdate = map['creds.update'];
|
|
208
|
+
if (credsUpdate === null || credsUpdate === void 0 ? void 0 : credsUpdate.me) {
|
|
209
|
+
const props = slot.props.get(socketId);
|
|
210
|
+
if (props) {
|
|
211
|
+
props.user = { ...props.user, ...credsUpdate.me };
|
|
212
|
+
log === null || log === void 0 ? void 0 : log.debug({ me: (_c = credsUpdate.me) === null || _c === void 0 ? void 0 : _c.id }, 'synced user to localProps');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
158
216
|
// Emit the aggregated 'event' for ev.process() compatibility
|
|
159
217
|
ev.emit('event', map);
|
|
160
218
|
// Also emit individual typed events for ev.on() listeners
|
|
161
219
|
for (const [event, data] of Object.entries(map)) {
|
|
162
220
|
ev.emit(event, data);
|
|
163
221
|
}
|
|
222
|
+
// When the socket closes, clean up proxy-side resources.
|
|
223
|
+
// The worker already removed its entry; we remove the
|
|
224
|
+
// emitter so a future startSock() can create a fresh one.
|
|
225
|
+
if (((_d = map['connection.update']) === null || _d === void 0 ? void 0 : _d.connection) === 'close') {
|
|
226
|
+
cleanupSocket(slot, socketId, log || createNoopLogger());
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
case 'ws-event':
|
|
232
|
+
if (socketId !== undefined) {
|
|
233
|
+
const wsEv = slot.wsEmitters.get(socketId);
|
|
234
|
+
if (wsEv) {
|
|
235
|
+
const { event, args } = msg;
|
|
236
|
+
wsEv.emit(event, ...args);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
case 'socket-info':
|
|
241
|
+
if (socketId !== undefined) {
|
|
242
|
+
const props = slot.props.get(socketId);
|
|
243
|
+
if (props && msg.info) {
|
|
244
|
+
Object.assign(props, msg.info);
|
|
164
245
|
}
|
|
165
246
|
}
|
|
166
247
|
break;
|
|
@@ -187,7 +268,7 @@ function spawnWorker() {
|
|
|
187
268
|
break;
|
|
188
269
|
}
|
|
189
270
|
});
|
|
190
|
-
worker.on('error',
|
|
271
|
+
worker.on('error', err => {
|
|
191
272
|
// Log to all socket loggers on this worker
|
|
192
273
|
for (const log of slot.loggers.values()) {
|
|
193
274
|
log.error({ err, workerPid: worker.threadId }, 'worker thread error');
|
|
@@ -196,7 +277,7 @@ function spawnWorker() {
|
|
|
196
277
|
if (idx !== -1)
|
|
197
278
|
slots.splice(idx, 1);
|
|
198
279
|
});
|
|
199
|
-
worker.on('exit',
|
|
280
|
+
worker.on('exit', code => {
|
|
200
281
|
for (const log of slot.loggers.values()) {
|
|
201
282
|
log.info({ exitCode: code, workerPid: worker.threadId }, 'worker thread exited');
|
|
202
283
|
}
|
|
@@ -220,7 +301,7 @@ async function handleCallbackCall(slot, socketId, msg) {
|
|
|
220
301
|
type: 'callback-result',
|
|
221
302
|
socketId,
|
|
222
303
|
id,
|
|
223
|
-
error: `No keystore method "${method}"
|
|
304
|
+
error: `No keystore method "${method}"`
|
|
224
305
|
});
|
|
225
306
|
}
|
|
226
307
|
try {
|
|
@@ -234,7 +315,7 @@ async function handleCallbackCall(slot, socketId, msg) {
|
|
|
234
315
|
type: 'callback-result',
|
|
235
316
|
socketId,
|
|
236
317
|
id,
|
|
237
|
-
error: (err === null || err === void 0 ? void 0 : err.message) || String(err)
|
|
318
|
+
error: (err === null || err === void 0 ? void 0 : err.message) || String(err)
|
|
238
319
|
});
|
|
239
320
|
}
|
|
240
321
|
return;
|
|
@@ -248,7 +329,7 @@ async function handleCallbackCall(slot, socketId, msg) {
|
|
|
248
329
|
type: 'callback-result',
|
|
249
330
|
socketId,
|
|
250
331
|
id,
|
|
251
|
-
error: `No callback registered for "${key}"
|
|
332
|
+
error: `No callback registered for "${key}"`
|
|
252
333
|
});
|
|
253
334
|
}
|
|
254
335
|
try {
|
|
@@ -262,7 +343,7 @@ async function handleCallbackCall(slot, socketId, msg) {
|
|
|
262
343
|
type: 'callback-result',
|
|
263
344
|
socketId,
|
|
264
345
|
id,
|
|
265
|
-
error: (err === null || err === void 0 ? void 0 : err.message) || String(err)
|
|
346
|
+
error: (err === null || err === void 0 ? void 0 : err.message) || String(err)
|
|
266
347
|
});
|
|
267
348
|
}
|
|
268
349
|
}
|
|
@@ -355,12 +436,10 @@ function deepStripFunctions(value, seen = new WeakSet()) {
|
|
|
355
436
|
return value;
|
|
356
437
|
}
|
|
357
438
|
const makeWASocket = (config) => {
|
|
358
|
-
var _a;
|
|
439
|
+
var _a, _b;
|
|
359
440
|
const socketId = nextSocketId();
|
|
360
441
|
// ---- Create a child logger for this socket ---------------------------
|
|
361
|
-
const log = config.logger
|
|
362
|
-
? config.logger.child({ worker: 'proxy', socketId })
|
|
363
|
-
: createNoopLogger();
|
|
442
|
+
const log = config.logger ? config.logger.child({ worker: 'proxy', socketId }) : createNoopLogger();
|
|
364
443
|
// ---- Extract non-serializable callbacks ------------------------------
|
|
365
444
|
const callbackStore = {};
|
|
366
445
|
// Save the real keystore before stripping
|
|
@@ -369,7 +448,14 @@ const makeWASocket = (config) => {
|
|
|
369
448
|
// Functions are replaced with the sentinel string '__proxy_fn__'.
|
|
370
449
|
// The child will create forwarding stubs for known callback keys and
|
|
371
450
|
// the keystore; other stripped functions become no-ops.
|
|
372
|
-
|
|
451
|
+
//
|
|
452
|
+
// makeSignalRepository returns a complex object (closures + methods) that
|
|
453
|
+
// cannot be cloned back to the worker. We replace it with a sentinel and
|
|
454
|
+
// recreate the default libsignal repository inside the worker.
|
|
455
|
+
const configWithSignalRepo = typeof config.makeSignalRepository === 'function'
|
|
456
|
+
? { ...config, makeSignalRepository: '__worker_signal_repository__' }
|
|
457
|
+
: config;
|
|
458
|
+
const serializableConfig = deepStripFunctions(configWithSignalRepo);
|
|
373
459
|
// Mark the logger so the child knows to create a real one
|
|
374
460
|
if (config.logger) {
|
|
375
461
|
;
|
|
@@ -381,6 +467,23 @@ const makeWASocket = (config) => {
|
|
|
381
467
|
serializableConfig.auth = {};
|
|
382
468
|
serializableConfig.auth.keys = '__proxy_keystore__';
|
|
383
469
|
}
|
|
470
|
+
// Worker-local cache stores cannot be cloned (their methods live on the
|
|
471
|
+
// prototype and would become broken plain objects). Replace them with a
|
|
472
|
+
// sentinel so the child creates a fresh cache inside the worker.
|
|
473
|
+
for (const k of CACHE_CONFIG_KEYS) {
|
|
474
|
+
if (typeof ((_b = config[k]) === null || _b === void 0 ? void 0 : _b.get) === 'function') {
|
|
475
|
+
;
|
|
476
|
+
serializableConfig[k] = CACHE_SENTINEL;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// HTTP agents contain sockets and native state; they cannot cross
|
|
480
|
+
// postMessage. The worker will create its own agents.
|
|
481
|
+
if (serializableConfig.agent) {
|
|
482
|
+
serializableConfig.agent = undefined;
|
|
483
|
+
}
|
|
484
|
+
if (serializableConfig.fetchAgent) {
|
|
485
|
+
serializableConfig.fetchAgent = undefined;
|
|
486
|
+
}
|
|
384
487
|
// Restore known callback sentinels with the proper key names
|
|
385
488
|
for (const k of CALLBACK_CONFIG_KEYS) {
|
|
386
489
|
if (typeof config[k] === 'function') {
|
|
@@ -392,20 +495,23 @@ const makeWASocket = (config) => {
|
|
|
392
495
|
const slot = acquireSlot();
|
|
393
496
|
slot.load++;
|
|
394
497
|
slot.emitters.set(socketId, new events_1.EventEmitter());
|
|
498
|
+
slot.wsEmitters.set(socketId, new events_1.EventEmitter());
|
|
499
|
+
slot.props.set(socketId, {});
|
|
395
500
|
slot.callbacks.set(socketId, callbackStore);
|
|
396
501
|
slot.keystores.set(socketId, realKeystore);
|
|
397
502
|
slot.loggers.set(socketId, log);
|
|
503
|
+
slot.configs.set(socketId, config);
|
|
398
504
|
log.info({
|
|
399
505
|
workerPid: slot.worker.threadId,
|
|
400
506
|
workerLoad: slot.load,
|
|
401
507
|
poolSize: slots.length,
|
|
402
|
-
maxWorkers
|
|
508
|
+
maxWorkers
|
|
403
509
|
}, 'socket assigned to worker');
|
|
404
510
|
// ---- Send init message to the worker for this socket -----------------
|
|
405
511
|
slot.worker.postMessage({
|
|
406
512
|
type: 'init',
|
|
407
513
|
socketId,
|
|
408
|
-
config: serializableConfig
|
|
514
|
+
config: serializableConfig
|
|
409
515
|
});
|
|
410
516
|
log.debug('init message sent to worker');
|
|
411
517
|
// ---- Public event emitter (local to this socket) ---------------------
|
|
@@ -418,13 +524,21 @@ const makeWASocket = (config) => {
|
|
|
418
524
|
const ev = new Proxy(rawEv, {
|
|
419
525
|
get(target, prop) {
|
|
420
526
|
// Local EventEmitter methods
|
|
421
|
-
if (prop === 'on' ||
|
|
422
|
-
prop === '
|
|
423
|
-
prop === '
|
|
424
|
-
prop === '
|
|
425
|
-
prop === '
|
|
426
|
-
prop === '
|
|
427
|
-
prop === '
|
|
527
|
+
if (prop === 'on' ||
|
|
528
|
+
prop === 'off' ||
|
|
529
|
+
prop === 'emit' ||
|
|
530
|
+
prop === 'removeListener' ||
|
|
531
|
+
prop === 'addListener' ||
|
|
532
|
+
prop === 'removeAllListeners' ||
|
|
533
|
+
prop === 'listeners' ||
|
|
534
|
+
prop === 'listenerCount' ||
|
|
535
|
+
prop === 'eventNames' ||
|
|
536
|
+
prop === 'getMaxListeners' ||
|
|
537
|
+
prop === 'setMaxListeners' ||
|
|
538
|
+
prop === 'rawListeners' ||
|
|
539
|
+
prop === 'prependListener' ||
|
|
540
|
+
prop === 'prependOnceListener' ||
|
|
541
|
+
prop === 'once') {
|
|
428
542
|
return target[prop].bind(target);
|
|
429
543
|
}
|
|
430
544
|
// process() is implemented locally: it listens to the aggregated
|
|
@@ -448,52 +562,115 @@ const makeWASocket = (config) => {
|
|
|
448
562
|
const id = nextReqId();
|
|
449
563
|
log.trace({ rpcId: id, method, argCount: args.length }, 'RPC call → worker');
|
|
450
564
|
return new Promise((resolve, reject) => {
|
|
451
|
-
slot.pending.set(id, { resolve, reject });
|
|
565
|
+
slot.pending.set(id, { socketId, resolve, reject });
|
|
452
566
|
slot.worker.postMessage({ type: 'call', socketId, id, method, args });
|
|
453
567
|
});
|
|
454
568
|
};
|
|
455
569
|
// ---- Build the proxy object ------------------------------------------
|
|
456
|
-
const
|
|
570
|
+
const rawWs = slot.wsEmitters.get(socketId);
|
|
571
|
+
const localProps = slot.props.get(socketId);
|
|
572
|
+
// Wrap ws in a proxy: EventEmitter methods work locally,
|
|
573
|
+
// close() is forwarded to the worker via RPC.
|
|
574
|
+
const wsProxy = new Proxy(rawWs, {
|
|
575
|
+
get(target, prop) {
|
|
576
|
+
if (prop === 'close') {
|
|
577
|
+
return (...args) => {
|
|
578
|
+
if (!isSocketActive(slot, socketId)) {
|
|
579
|
+
log.debug('ws.close() ignored — socket already cleaned up');
|
|
580
|
+
return Promise.resolve();
|
|
581
|
+
}
|
|
582
|
+
return rpcCall('ws.close', args);
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
// Delegate everything else (on, off, removeAllListeners, etc.) to the EventEmitter
|
|
586
|
+
const value = target[prop];
|
|
587
|
+
if (typeof value === 'function') {
|
|
588
|
+
return value.bind(target);
|
|
589
|
+
}
|
|
590
|
+
return value;
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
const socketProxy = new Proxy({ ev, ws: wsProxy }, {
|
|
457
594
|
get(_target, prop) {
|
|
458
595
|
if (prop === 'ev')
|
|
459
596
|
return ev;
|
|
597
|
+
if (prop === 'ws')
|
|
598
|
+
return wsProxy;
|
|
460
599
|
if (prop === 'then')
|
|
461
600
|
return undefined;
|
|
462
601
|
if (prop === 'pool')
|
|
463
602
|
return getStats();
|
|
603
|
+
// Check local property store first (wsocket.id, wsocket.user, etc.)
|
|
604
|
+
if (prop in localProps)
|
|
605
|
+
return localProps[prop];
|
|
464
606
|
if (prop === 'end') {
|
|
465
607
|
return async (...args) => {
|
|
608
|
+
if (!isSocketActive(slot, socketId)) {
|
|
609
|
+
log.debug('socket.end() ignored — already cleaned up');
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
466
612
|
log.info('socket.end() called — cleaning up');
|
|
467
613
|
try {
|
|
468
614
|
await rpcCall('end', args);
|
|
469
615
|
}
|
|
470
|
-
catch (_) {
|
|
616
|
+
catch (_) {
|
|
617
|
+
/* ok */
|
|
618
|
+
}
|
|
471
619
|
cleanupSocket(slot, socketId, log);
|
|
472
620
|
};
|
|
473
621
|
}
|
|
474
622
|
if (prop === 'logout') {
|
|
475
623
|
return async (...args) => {
|
|
624
|
+
if (!isSocketActive(slot, socketId)) {
|
|
625
|
+
log.debug('socket.logout() ignored — already cleaned up');
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
476
628
|
log.info('socket.logout() called — cleaning up');
|
|
477
629
|
try {
|
|
478
630
|
await rpcCall('logout', args);
|
|
479
631
|
}
|
|
480
|
-
catch (_) {
|
|
632
|
+
catch (_) {
|
|
633
|
+
/* ok */
|
|
634
|
+
}
|
|
481
635
|
cleanupSocket(slot, socketId, log);
|
|
482
636
|
};
|
|
483
637
|
}
|
|
484
638
|
return (...args) => rpcCall(prop, args);
|
|
485
639
|
},
|
|
640
|
+
set(_target, prop, value) {
|
|
641
|
+
localProps[prop] = value;
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
486
644
|
});
|
|
487
645
|
return socketProxy;
|
|
488
646
|
};
|
|
489
647
|
exports.makeWASocket = makeWASocket;
|
|
648
|
+
function isSocketActive(slot, socketId) {
|
|
649
|
+
return slot.emitters.has(socketId);
|
|
650
|
+
}
|
|
490
651
|
function cleanupSocket(slot, socketId, log) {
|
|
652
|
+
// Idempotent cleanup: only run once per socketId.
|
|
653
|
+
if (!isSocketActive(slot, socketId)) {
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
// Reject any pending RPC calls for this socket so they don't hang or
|
|
657
|
+
// receive the worker's "Socket X not found" error after reconnection.
|
|
658
|
+
for (const [id, p] of slot.pending) {
|
|
659
|
+
if (p.socketId === socketId) {
|
|
660
|
+
slot.pending.delete(id);
|
|
661
|
+
p.reject(new Error(`Socket ${socketId} closed`));
|
|
662
|
+
}
|
|
663
|
+
}
|
|
491
664
|
slot.emitters.delete(socketId);
|
|
665
|
+
slot.wsEmitters.delete(socketId);
|
|
666
|
+
slot.props.delete(socketId);
|
|
492
667
|
slot.callbacks.delete(socketId);
|
|
493
668
|
slot.keystores.delete(socketId);
|
|
494
669
|
slot.loggers.delete(socketId);
|
|
670
|
+
slot.configs.delete(socketId);
|
|
495
671
|
slot.load = Math.max(0, slot.load - 1);
|
|
496
672
|
log.info({ workerLoad: slot.load, poolSize: slots.length }, 'socket cleaned up');
|
|
673
|
+
return true;
|
|
497
674
|
// If the worker is now idle and we're over capacity, terminate it
|
|
498
675
|
if (slot.load === 0 && slots.length > maxWorkers) {
|
|
499
676
|
const idx = slots.indexOf(slot);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libzapitu",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.22",
|
|
4
4
|
"description": "A WebSockets library for interacting with WhatsApp Web",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"whatsapp",
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
"jimp": "^1.6.0",
|
|
84
84
|
"json": "^11.0.0",
|
|
85
85
|
"link-preview-js": "^3.0.0",
|
|
86
|
+
"node-cache": "^5.1.2",
|
|
86
87
|
"open": "^8.4.2",
|
|
87
88
|
"prettier": "^3.5.3",
|
|
88
89
|
"protobufjs-cli": "^1.1.3",
|