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.
@@ -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') {
@@ -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
  * */
@@ -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)({ timestamp: () => `,"time":"${new Date().toJSON()}"` });
7
+ exports.default = (0, pino_1.default)({
8
+ timestamp: () => `,"time":"${new Date().toJSON()}"`,
9
+ level: process.env.BAILEYS_LOG_LEVEL || 'info'
10
+ });
@@ -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 pino_1 = __importDefault(require("pino"));
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 a basic pino logger so the real makeWASocket has a working logger.
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;
@@ -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
- return value.map(revivePostMessage);
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
- const result = {};
91
- for (const key of Object.keys(value)) {
92
- result[key] = revivePostMessage(value[key]);
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
- pending: new Map(),
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', (err) => {
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', (code) => {
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
- const serializableConfig = deepStripFunctions(config);
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' || prop === 'off' || prop === 'emit' ||
422
- prop === 'removeListener' || prop === 'addListener' ||
423
- prop === 'removeAllListeners' || prop === 'listeners' ||
424
- prop === 'listenerCount' || prop === 'eventNames' ||
425
- prop === 'getMaxListeners' || prop === 'setMaxListeners' ||
426
- prop === 'rawListeners' || prop === 'prependListener' ||
427
- prop === 'prependOnceListener' || prop === 'once') {
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 socketProxy = new Proxy({ ev }, {
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 (_) { /* ok */ }
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 (_) { /* ok */ }
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.20",
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",