@whanext/core 0.19.16 → 0.19.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -379,15 +379,37 @@ function isCommandGroup(definition) {
379
379
  }
380
380
 
381
381
  // src/commands/concurrency.ts
382
+ var DEFAULT_MAX_QUEUE = 10;
383
+ var DEFAULT_QUEUE_TIMEOUT_MS = 6e4;
382
384
  var CommandConcurrencyController = class {
383
385
  #states = /* @__PURE__ */ new Map();
384
- async run(key, options, execute) {
386
+ #onEvent;
387
+ #queueTimeouts = 0;
388
+ #queueRejected = 0;
389
+ constructor(onEvent) {
390
+ this.#onEvent = onEvent;
391
+ }
392
+ health() {
393
+ let running = 0;
394
+ let queued = 0;
395
+ for (const state of this.#states.values()) {
396
+ running += state.active;
397
+ queued += state.queue.length;
398
+ }
399
+ return {
400
+ running,
401
+ queued,
402
+ queueTimeouts: this.#queueTimeouts,
403
+ queueRejected: this.#queueRejected
404
+ };
405
+ }
406
+ async run(key, options, execute, context) {
385
407
  const strategy = options?.strategy ?? "parallel";
386
408
  if (strategy === "parallel") {
387
409
  await execute(new AbortController().signal);
388
410
  return;
389
411
  }
390
- const max = Math.max(1, options?.max ?? 1);
412
+ const max = normalizePositiveInteger(options?.max, 1);
391
413
  const state = this.#states.get(key) ?? { active: 0, queue: [], controllers: /* @__PURE__ */ new Set() };
392
414
  this.#states.set(key, state);
393
415
  if (strategy === "replace") {
@@ -399,7 +421,7 @@ var CommandConcurrencyController = class {
399
421
  recoverable: true
400
422
  });
401
423
  } else if (strategy === "queue" && state.active >= max) {
402
- await new Promise((resolve4) => state.queue.push(resolve4));
424
+ await this.#waitForQueue(key, state, options ?? {}, context);
403
425
  }
404
426
  const controller = new AbortController();
405
427
  state.controllers.add(controller);
@@ -409,13 +431,97 @@ var CommandConcurrencyController = class {
409
431
  } finally {
410
432
  state.controllers.delete(controller);
411
433
  state.active -= 1;
412
- state.queue.shift()?.();
434
+ this.#releaseNext(state);
413
435
  if (state.active === 0 && state.queue.length === 0) {
414
436
  this.#states.delete(key);
415
437
  }
416
438
  }
417
439
  }
440
+ async #waitForQueue(key, state, options, context) {
441
+ const maxQueue = normalizeQueueLimit(options.maxQueue);
442
+ if (state.queue.length >= maxQueue) {
443
+ this.#queueRejected += 1;
444
+ if (context && Number.isFinite(maxQueue)) {
445
+ this.#onEvent?.({
446
+ type: "commandQueueFull",
447
+ payload: {
448
+ ...context,
449
+ key,
450
+ maxQueue,
451
+ queued: state.queue.length
452
+ }
453
+ });
454
+ }
455
+ throw new WhaNextError("COMMAND_QUEUE_FULL", "This command queue is full.", {
456
+ context: { key, maxQueue, queued: state.queue.length },
457
+ recoverable: true
458
+ });
459
+ }
460
+ const queueTimeoutMs = normalizeQueueTimeout(options.queueTimeoutMs);
461
+ await new Promise((resolve4, reject) => {
462
+ const entry = {
463
+ resolve: resolve4,
464
+ reject,
465
+ enqueuedAt: Date.now(),
466
+ settled: false
467
+ };
468
+ if (queueTimeoutMs > 0) {
469
+ entry.timer = setTimeout(() => {
470
+ if (entry.settled) return;
471
+ entry.settled = true;
472
+ const index = state.queue.indexOf(entry);
473
+ if (index >= 0) state.queue.splice(index, 1);
474
+ const queuedForMs = Date.now() - entry.enqueuedAt;
475
+ this.#queueTimeouts += 1;
476
+ if (context) {
477
+ this.#onEvent?.({
478
+ type: "commandQueueTimeout",
479
+ payload: {
480
+ ...context,
481
+ key,
482
+ queuedForMs,
483
+ queueTimeoutMs,
484
+ queued: state.queue.length
485
+ }
486
+ });
487
+ }
488
+ reject(new WhaNextError(
489
+ "COMMAND_QUEUE_TIMEOUT",
490
+ "This command waited too long in the execution queue.",
491
+ {
492
+ context: { key, queuedForMs, queueTimeoutMs },
493
+ recoverable: true
494
+ }
495
+ ));
496
+ }, queueTimeoutMs);
497
+ }
498
+ state.queue.push(entry);
499
+ });
500
+ }
501
+ #releaseNext(state) {
502
+ while (state.queue.length > 0) {
503
+ const next = state.queue.shift();
504
+ if (!next || next.settled) continue;
505
+ next.settled = true;
506
+ if (next.timer) clearTimeout(next.timer);
507
+ next.resolve();
508
+ return;
509
+ }
510
+ }
418
511
  };
512
+ function normalizePositiveInteger(value, fallback) {
513
+ if (value === void 0 || !Number.isFinite(value)) return fallback;
514
+ return Math.max(1, Math.floor(value));
515
+ }
516
+ function normalizeQueueLimit(value) {
517
+ if (value === 0) return Number.POSITIVE_INFINITY;
518
+ return normalizePositiveInteger(value, DEFAULT_MAX_QUEUE);
519
+ }
520
+ function normalizeQueueTimeout(value) {
521
+ if (value === 0) return 0;
522
+ if (value === void 0 || !Number.isFinite(value)) return DEFAULT_QUEUE_TIMEOUT_MS;
523
+ return Math.max(1, Math.floor(value));
524
+ }
419
525
 
420
526
  // src/commands/context.ts
421
527
  var CommandContextImplementation = class {
@@ -739,6 +845,24 @@ function invalid(name, received, expected) {
739
845
  });
740
846
  }
741
847
 
848
+ // src/provider/event-emitter.ts
849
+ var TypedEventEmitter = class {
850
+ #listeners = /* @__PURE__ */ new Map();
851
+ on(event, listener) {
852
+ const listeners = this.#listeners.get(event) ?? /* @__PURE__ */ new Set();
853
+ listeners.add(listener);
854
+ this.#listeners.set(event, listeners);
855
+ return () => listeners.delete(listener);
856
+ }
857
+ async emit(event, payload) {
858
+ const listeners = this.#listeners.get(event);
859
+ if (!listeners) {
860
+ return;
861
+ }
862
+ await Promise.all([...listeners].map((listener) => listener(payload)));
863
+ }
864
+ };
865
+
742
866
  // src/services/user-service.ts
743
867
  var UserService = class {
744
868
  #group;
@@ -781,11 +905,19 @@ var CommandRouter = class {
781
905
  #globalMiddleware = [];
782
906
  #errorHandlers = [];
783
907
  #cooldowns = /* @__PURE__ */ new Map();
784
- #concurrency = new CommandConcurrencyController();
908
+ #events = new TypedEventEmitter();
909
+ #concurrency;
785
910
  #beforeExecute;
786
911
  #afterExecute;
787
912
  #cooldownOperations = 0;
788
913
  constructor(servicesOrGroup, options = {}) {
914
+ this.#concurrency = new CommandConcurrencyController((event) => {
915
+ if (event.type === "commandQueueTimeout") {
916
+ void this.#events.emit("commandQueueTimeout", event.payload).catch(() => void 0);
917
+ } else {
918
+ void this.#events.emit("commandQueueFull", event.payload).catch(() => void 0);
919
+ }
920
+ });
789
921
  this.#services = isRuntimeServices(servicesOrGroup) ? servicesOrGroup : createLegacyServices(servicesOrGroup);
790
922
  this.setPrefixes(options.prefix ?? "!");
791
923
  this.#legacyOnError = options.onError;
@@ -809,6 +941,12 @@ var CommandRouter = class {
809
941
  get size() {
810
942
  return this.catalog({ includeHidden: true }).length;
811
943
  }
944
+ health() {
945
+ return this.#concurrency.health();
946
+ }
947
+ on(event, listener) {
948
+ return this.#events.on(event, listener);
949
+ }
812
950
  command(definition) {
813
951
  this.#validateTree(definition, []);
814
952
  for (const name of commandNames(definition)) {
@@ -970,6 +1108,12 @@ _Nenhum comando dispon\xEDvel._`;
970
1108
  await this.#authorize(resolved.layers, context);
971
1109
  this.#consumeCooldown(resolved, context);
972
1110
  await this.#execute(resolved, context);
1111
+ },
1112
+ {
1113
+ command: resolved.registered.path.join(" "),
1114
+ messageId: message.id,
1115
+ chatId: message.chatId,
1116
+ userId: message.sender.id
973
1117
  }
974
1118
  );
975
1119
  return true;
@@ -1797,24 +1941,6 @@ var SqliteMuteStore = class {
1797
1941
  }
1798
1942
  };
1799
1943
 
1800
- // src/provider/event-emitter.ts
1801
- var TypedEventEmitter = class {
1802
- #listeners = /* @__PURE__ */ new Map();
1803
- on(event, listener) {
1804
- const listeners = this.#listeners.get(event) ?? /* @__PURE__ */ new Set();
1805
- listeners.add(listener);
1806
- this.#listeners.set(event, listeners);
1807
- return () => listeners.delete(listener);
1808
- }
1809
- async emit(event, payload) {
1810
- const listeners = this.#listeners.get(event);
1811
- if (!listeners) {
1812
- return;
1813
- }
1814
- await Promise.all([...listeners].map((listener) => listener(payload)));
1815
- }
1816
- };
1817
-
1818
1944
  // src/services/account-service.ts
1819
1945
  var AccountService = class {
1820
1946
  id;
@@ -2165,6 +2291,7 @@ var WhaNextApp = class {
2165
2291
  #router;
2166
2292
  #startedAt = Date.now();
2167
2293
  #state = "idle";
2294
+ #lastStability = "offline";
2168
2295
  constructor(provider, options = {}, logger = new Logger(options.logger)) {
2169
2296
  this.#provider = provider;
2170
2297
  this.#phone = options.phone;
@@ -2209,14 +2336,61 @@ var WhaNextApp = class {
2209
2336
  return this.#state === "connected";
2210
2337
  }
2211
2338
  health() {
2339
+ const provider = this.#provider.health?.();
2340
+ const connection = provider?.connection ?? {
2341
+ state: this.#state,
2342
+ uptimeMs: this.isReady ? Date.now() - this.#startedAt : 0,
2343
+ reconnects: 0,
2344
+ reconnectAttempt: 0
2345
+ };
2346
+ const messaging = provider?.messaging ?? {
2347
+ sent: 0,
2348
+ received: 0,
2349
+ failed: 0,
2350
+ decryptedPayloads: 0,
2351
+ unavailable: 0,
2352
+ resendRequested: 0,
2353
+ recovered: 0,
2354
+ recoveryFailed: 0,
2355
+ unavailableUnrecoverable: 0,
2356
+ decodeFailures: 0,
2357
+ unhandledStanzas: 0,
2358
+ ignoredOffline: 0,
2359
+ duplicates: 0,
2360
+ normalizationFailures: 0
2361
+ };
2362
+ const crypto = provider?.crypto ?? {
2363
+ backend: "unknown",
2364
+ acceleration: false,
2365
+ decryptFailures: 0,
2366
+ addonDecryptFailures: 0,
2367
+ senderKeyMismatches: 0
2368
+ };
2369
+ const groups = provider?.groups ?? {
2370
+ phashMismatches: 0,
2371
+ metadataRecoveries: 0,
2372
+ metadataRecoveryFailures: 0
2373
+ };
2374
+ const timeouts = provider?.timeouts ?? {
2375
+ connectTimeoutMs: 0,
2376
+ nodeQueryTimeoutMs: 0
2377
+ };
2378
+ const stability = provider?.stability ?? this.#fallbackStability();
2212
2379
  return {
2213
2380
  status: this.#healthStatus(),
2381
+ stability,
2214
2382
  state: this.#state,
2215
2383
  ready: this.isReady,
2216
2384
  uptimeMs: Date.now() - this.#startedAt,
2217
2385
  timestamp: /* @__PURE__ */ new Date(),
2218
2386
  muteEnabled: this.mute.enabled,
2219
- logLevel: this.logger.level
2387
+ logLevel: this.logger.level,
2388
+ connection,
2389
+ messaging,
2390
+ crypto,
2391
+ groups,
2392
+ commands: this.#router.health(),
2393
+ timeouts
2220
2394
  };
2221
2395
  }
2222
2396
  router() {
@@ -2286,9 +2460,42 @@ var WhaNextApp = class {
2286
2460
  }
2287
2461
  #bind() {
2288
2462
  this.#provider.on("connection", async (update) => {
2463
+ const previousState = this.#state;
2289
2464
  this.#state = update.state;
2290
2465
  this.#logConnection(update);
2291
2466
  await this.#events.emit("connection", update);
2467
+ if (update.state === "connected" && previousState === "reconnecting") {
2468
+ const providerHealth = this.#provider.health?.();
2469
+ await this.#events.emit("connectionRecovered", {
2470
+ recoveredAt: /* @__PURE__ */ new Date(),
2471
+ reconnects: providerHealth?.connection.reconnects ?? 1
2472
+ });
2473
+ }
2474
+ await this.#refreshHealthState();
2475
+ });
2476
+ this.#provider.on("stability", async (event) => {
2477
+ if (event.type === "groupMetadataRecovered") {
2478
+ await this.#events.emit("groupMetadataRecovered", event.payload);
2479
+ } else if (event.type === "cryptoDegraded") {
2480
+ await this.#events.emit("cryptoDegraded", event.payload);
2481
+ } else if (event.type === "messageUnavailable") {
2482
+ await this.#events.emit("messageUnavailable", event.payload);
2483
+ } else if (event.type === "messageRecovered") {
2484
+ await this.#events.emit("messageRecovered", event.payload);
2485
+ } else if (event.type === "messageRecoveryFailed") {
2486
+ await this.#events.emit("messageRecoveryFailed", event.payload);
2487
+ } else if (event.type === "messageDecodeFailure") {
2488
+ await this.#events.emit("messageDecodeFailure", event.payload);
2489
+ } else if (event.type === "messageDiscarded") {
2490
+ await this.#events.emit("messageDiscarded", event.payload);
2491
+ }
2492
+ await this.#refreshHealthState();
2493
+ });
2494
+ this.#router.on("commandQueueTimeout", async (event) => {
2495
+ await this.#events.emit("commandQueueTimeout", event);
2496
+ });
2497
+ this.#router.on("commandQueueFull", async (event) => {
2498
+ await this.#events.emit("commandQueueFull", event);
2292
2499
  });
2293
2500
  this.#provider.on("groupChanged", ({ groupId }) => this.group.invalidate(groupId));
2294
2501
  this.#provider.on("groupParticipantsChanged", async (change) => {
@@ -2356,6 +2563,22 @@ var WhaNextApp = class {
2356
2563
  }
2357
2564
  });
2358
2565
  }
2566
+ #fallbackStability() {
2567
+ if (this.#state === "connected") return "healthy";
2568
+ if (this.#state === "connecting" || this.#state === "reconnecting") return "reconnecting";
2569
+ return "offline";
2570
+ }
2571
+ async #refreshHealthState() {
2572
+ const health = this.health();
2573
+ if (health.stability === this.#lastStability) return;
2574
+ const previous = this.#lastStability;
2575
+ this.#lastStability = health.stability;
2576
+ await this.#events.emit("healthChanged", {
2577
+ previous,
2578
+ current: health.stability,
2579
+ health
2580
+ });
2581
+ }
2359
2582
  #healthStatus() {
2360
2583
  if (this.#state === "connected") {
2361
2584
  return "ready";
@@ -2932,6 +3155,15 @@ var fatalDisconnectReasons = /* @__PURE__ */ new Set([
2932
3155
  var fatalDisconnectCodes = /* @__PURE__ */ new Set([401, 403, 405, 406, 409, 516]);
2933
3156
  var sharedMediaProcessor = createMediaProcessor();
2934
3157
  var REMOTE_MEDIA_TIMEOUT_MS = 12e4;
3158
+ var DEFAULT_CONNECT_TIMEOUT_MS = 15e3;
3159
+ var DEFAULT_NODE_QUERY_TIMEOUT_MS = 3e4;
3160
+ var PROVIDER_DEGRADED_WINDOW_MS = 12e4;
3161
+ var GROUP_METADATA_CACHE_TTL_MS = 18e4;
3162
+ var DEVICE_LIST_CACHE_TTL_MS = 18e4;
3163
+ var GROUP_METADATA_RECOVERY_COOLDOWN_MS = 6e4;
3164
+ var GROUP_METADATA_MISMATCH_WARNING = "group message publish acknowledged with mismatch metadata";
3165
+ var MESSAGE_RECOVERY_TIMEOUT_MS = 3e4;
3166
+ var DECRYPT_CORRELATION_TTL_MS = 15e3;
2935
3167
  var messageSnapshotRetentionSeconds = 7 * 24 * 60 * 60;
2936
3168
  var messageSnapshotMaxPerSession = 2e4;
2937
3169
  var messageSnapshotPruneInterval = 256;
@@ -2942,10 +3174,18 @@ var ZapoProvider = class {
2942
3174
  #messageStore = /* @__PURE__ */ new Map();
2943
3175
  #messageKeyStore = /* @__PURE__ */ new WeakMap();
2944
3176
  #deliveredMessageStore = /* @__PURE__ */ new Set();
3177
+ #pendingUnavailableRecoveries = /* @__PURE__ */ new Map();
3178
+ #recentDecryptedPayloads = /* @__PURE__ */ new Map();
2945
3179
  #handledProtocolStore = /* @__PURE__ */ new Set();
2946
3180
  #callCreatorStore = /* @__PURE__ */ new Map();
3181
+ #groupMetadataRecoveryAt = /* @__PURE__ */ new Map();
3182
+ #groupMetadataRecoveryInFlight = /* @__PURE__ */ new Set();
2947
3183
  #messageCacheSize;
3184
+ #cryptoBackend;
3185
+ #connectTimeoutMs;
3186
+ #nodeQueryTimeoutMs;
2948
3187
  #protocolMutationQueue = Promise.resolve();
3188
+ #state = "idle";
2949
3189
  #client;
2950
3190
  #storeEntry;
2951
3191
  #storePath;
@@ -2960,14 +3200,95 @@ var ZapoProvider = class {
2960
3200
  #messageSnapshotsSincePrune = 0;
2961
3201
  #pairingReady = Promise.resolve();
2962
3202
  #resolvePairingReady;
3203
+ #lastConnectedAt;
3204
+ #lastDisconnectedAt;
3205
+ #reconnects = 0;
3206
+ #sentMessages = 0;
3207
+ #receivedMessages = 0;
3208
+ #failedMessages = 0;
3209
+ #decryptedPayloads = 0;
3210
+ #unavailableMessages = 0;
3211
+ #resendRequestedMessages = 0;
3212
+ #recoveredMessages = 0;
3213
+ #recoveryFailedMessages = 0;
3214
+ #unavailableUnrecoverableMessages = 0;
3215
+ #decodeFailures = 0;
3216
+ #unhandledStanzas = 0;
3217
+ #ignoredOfflineMessages = 0;
3218
+ #duplicateMessages = 0;
3219
+ #normalizationFailures = 0;
3220
+ #lastIncomingAt;
3221
+ #lastOutgoingAt;
3222
+ #decryptFailures = 0;
3223
+ #addonDecryptFailures = 0;
3224
+ #senderKeyMismatches = 0;
3225
+ #phashMismatches = 0;
3226
+ #metadataRecoveries = 0;
3227
+ #metadataRecoveryFailures = 0;
3228
+ #degradedUntil = 0;
3229
+ #healthRefreshTimer;
3230
+ #cryptoBackendLogged = false;
2963
3231
  constructor(options) {
2964
3232
  this.#options = options;
2965
3233
  this.#logger = options.logger ?? new Logger("silent");
2966
3234
  this.#messageCacheSize = Math.max(1, options.messageCacheSize ?? 1e3);
3235
+ this.#connectTimeoutMs = normalizeProviderTimeout(options.connectTimeoutMs, DEFAULT_CONNECT_TIMEOUT_MS);
3236
+ this.#nodeQueryTimeoutMs = normalizeProviderTimeout(options.nodeQueryTimeoutMs, DEFAULT_NODE_QUERY_TIMEOUT_MS);
3237
+ this.#cryptoBackend = detectCryptoBackend();
2967
3238
  }
2968
3239
  on(event, listener) {
2969
3240
  return this.#events.on(event, listener);
2970
3241
  }
3242
+ health() {
3243
+ const now = Date.now();
3244
+ const state = this.#state;
3245
+ const stability = state === "connected" ? now < this.#degradedUntil ? "degraded" : "healthy" : state === "reconnecting" || state === "connecting" ? "reconnecting" : "offline";
3246
+ return {
3247
+ stability,
3248
+ connection: {
3249
+ state,
3250
+ uptimeMs: this.#connectedAtSeconds > 0 && this.#connected ? Math.max(0, now - this.#connectedAtSeconds * 1e3) : 0,
3251
+ reconnects: this.#reconnects,
3252
+ reconnectAttempt: this.#reconnectAttempt,
3253
+ ...this.#lastConnectedAt ? { lastConnectedAt: new Date(this.#lastConnectedAt) } : {},
3254
+ ...this.#lastDisconnectedAt ? { lastDisconnectedAt: new Date(this.#lastDisconnectedAt) } : {}
3255
+ },
3256
+ messaging: {
3257
+ sent: this.#sentMessages,
3258
+ received: this.#receivedMessages,
3259
+ failed: this.#failedMessages,
3260
+ decryptedPayloads: this.#decryptedPayloads,
3261
+ unavailable: this.#unavailableMessages,
3262
+ resendRequested: this.#resendRequestedMessages,
3263
+ recovered: this.#recoveredMessages,
3264
+ recoveryFailed: this.#recoveryFailedMessages,
3265
+ unavailableUnrecoverable: this.#unavailableUnrecoverableMessages,
3266
+ decodeFailures: this.#decodeFailures,
3267
+ unhandledStanzas: this.#unhandledStanzas,
3268
+ ignoredOffline: this.#ignoredOfflineMessages,
3269
+ duplicates: this.#duplicateMessages,
3270
+ normalizationFailures: this.#normalizationFailures,
3271
+ ...this.#lastIncomingAt ? { lastIncomingAt: new Date(this.#lastIncomingAt) } : {},
3272
+ ...this.#lastOutgoingAt ? { lastOutgoingAt: new Date(this.#lastOutgoingAt) } : {}
3273
+ },
3274
+ crypto: {
3275
+ backend: this.#cryptoBackend,
3276
+ acceleration: this.#cryptoBackend === "napi" || this.#cryptoBackend === "wasm",
3277
+ decryptFailures: this.#decryptFailures,
3278
+ addonDecryptFailures: this.#addonDecryptFailures,
3279
+ senderKeyMismatches: this.#senderKeyMismatches
3280
+ },
3281
+ groups: {
3282
+ phashMismatches: this.#phashMismatches,
3283
+ metadataRecoveries: this.#metadataRecoveries,
3284
+ metadataRecoveryFailures: this.#metadataRecoveryFailures
3285
+ },
3286
+ timeouts: {
3287
+ connectTimeoutMs: this.#connectTimeoutMs,
3288
+ nodeQueryTimeoutMs: this.#nodeQueryTimeoutMs
3289
+ }
3290
+ };
3291
+ }
2971
3292
  async connect() {
2972
3293
  this.#intentionalClose = false;
2973
3294
  this.#closedNotified = false;
@@ -2976,8 +3297,9 @@ var ZapoProvider = class {
2976
3297
  return;
2977
3298
  }
2978
3299
  this.#preparePairingGate();
3300
+ this.#state = this.#reconnectAttempt > 0 ? "reconnecting" : "connecting";
2979
3301
  await this.#events.emit("connection", {
2980
- state: this.#reconnectAttempt > 0 ? "reconnecting" : "connecting",
3302
+ state: this.#state,
2981
3303
  attempt: this.#reconnectAttempt
2982
3304
  });
2983
3305
  this.#startConnect(client);
@@ -2988,6 +3310,15 @@ var ZapoProvider = class {
2988
3310
  clearTimeout(this.#reconnectTimer);
2989
3311
  this.#reconnectTimer = void 0;
2990
3312
  }
3313
+ if (this.#healthRefreshTimer) {
3314
+ clearTimeout(this.#healthRefreshTimer);
3315
+ this.#healthRefreshTimer = void 0;
3316
+ }
3317
+ for (const pending of this.#pendingUnavailableRecoveries.values()) {
3318
+ clearTimeout(pending.timer);
3319
+ }
3320
+ this.#pendingUnavailableRecoveries.clear();
3321
+ this.#recentDecryptedPayloads.clear();
2991
3322
  const client = this.#client;
2992
3323
  this.#connectPromise = void 0;
2993
3324
  try {
@@ -2998,6 +3329,8 @@ var ZapoProvider = class {
2998
3329
  }
2999
3330
  } finally {
3000
3331
  this.#connected = false;
3332
+ this.#state = "closed";
3333
+ this.#lastDisconnectedAt = /* @__PURE__ */ new Date();
3001
3334
  await this.#releaseStore();
3002
3335
  }
3003
3336
  }
@@ -3045,50 +3378,56 @@ var ZapoProvider = class {
3045
3378
  }
3046
3379
  }
3047
3380
  async sendMessage(chatId, content, replyTo) {
3048
- if ("buttons" in content) {
3049
- return this.#sendButtons(chatId, content, replyTo);
3050
- }
3051
- if ("list" in content) {
3052
- return this.#sendList(chatId, content, replyTo);
3053
- }
3054
- const client = this.#requireClient();
3055
- const { value, mentions, viewOnce } = await this.#toContent(content);
3056
- const result = await client.message.send(chatId, value, {
3057
- ...replyTo ? { quote: this.#toZapoKey(replyTo) } : {},
3058
- ...mentions.length > 0 ? { mentions } : {},
3059
- ...viewOnce !== void 0 ? { viewOnce } : {}
3381
+ return this.#trackOutgoing(async () => {
3382
+ if ("buttons" in content) {
3383
+ return this.#sendButtons(chatId, content, replyTo);
3384
+ }
3385
+ if ("list" in content) {
3386
+ return this.#sendList(chatId, content, replyTo);
3387
+ }
3388
+ const client = this.#requireClient();
3389
+ const { value, mentions, viewOnce } = await this.#toContent(content);
3390
+ const result = await client.message.send(chatId, value, {
3391
+ ...replyTo ? { quote: this.#toZapoKey(replyTo) } : {},
3392
+ ...mentions.length > 0 ? { mentions } : {},
3393
+ ...viewOnce !== void 0 ? { viewOnce } : {}
3394
+ });
3395
+ return this.#sent(result, chatId);
3060
3396
  });
3061
- return this.#sent(result, chatId);
3062
3397
  }
3063
3398
  async repostMessage(source, chatId, options = {}) {
3064
- const original = await this.#findStoredMessage(this.#toZapoKey(source));
3065
- if (!original?.message) {
3066
- throw new WhaNextError(
3067
- "MESSAGE_NOT_FOUND",
3068
- "The source message is no longer available in the recent-message cache.",
3399
+ return this.#trackOutgoing(async () => {
3400
+ const original = await this.#findStoredMessage(this.#toZapoKey(source));
3401
+ if (!original?.message) {
3402
+ throw new WhaNextError(
3403
+ "MESSAGE_NOT_FOUND",
3404
+ "The source message is no longer available in the recent-message cache.",
3405
+ {
3406
+ context: { messageId: source.id, chatId: source.chatId },
3407
+ recoverable: true
3408
+ }
3409
+ );
3410
+ }
3411
+ const result = await this.#requireClient().message.send(
3412
+ chatId,
3413
+ original.message,
3069
3414
  {
3070
- context: { messageId: source.id, chatId: source.chatId },
3071
- recoverable: true
3415
+ forward: true,
3416
+ ...options.mentions && options.mentions.length > 0 ? { mentions: this.#mentions(options.mentions) } : {}
3072
3417
  }
3073
3418
  );
3074
- }
3075
- const result = await this.#requireClient().message.send(
3076
- chatId,
3077
- original.message,
3078
- {
3079
- forward: true,
3080
- ...options.mentions && options.mentions.length > 0 ? { mentions: this.#mentions(options.mentions) } : {}
3081
- }
3082
- );
3083
- return this.#sent(result, chatId);
3419
+ return this.#sent(result, chatId);
3420
+ });
3084
3421
  }
3085
3422
  async reactToMessage(key, emoji) {
3086
- const result = await this.#requireClient().message.send(key.chatId, {
3087
- type: "reaction",
3088
- emoji: emoji ?? "",
3089
- target: this.#toZapoKey(key)
3423
+ return this.#trackOutgoing(async () => {
3424
+ const result = await this.#requireClient().message.send(key.chatId, {
3425
+ type: "reaction",
3426
+ emoji: emoji ?? "",
3427
+ target: this.#toZapoKey(key)
3428
+ });
3429
+ return this.#sent(result, key.chatId);
3090
3430
  });
3091
- return this.#sent(result, key.chatId);
3092
3431
  }
3093
3432
  async downloadMedia(key) {
3094
3433
  const message = this.#messageKeyStore.get(key) ?? await this.#findStoredMessage(this.#toZapoKey(key));
@@ -3124,17 +3463,21 @@ var ZapoProvider = class {
3124
3463
  }
3125
3464
  }
3126
3465
  async editMessage(key, content) {
3127
- const result = await this.#requireClient().message.send(
3128
- key.chatId,
3129
- content,
3130
- { editKey: this.#toZapoKey(key) }
3131
- );
3132
- return this.#sent(result, key.chatId);
3466
+ return this.#trackOutgoing(async () => {
3467
+ const result = await this.#requireClient().message.send(
3468
+ key.chatId,
3469
+ content,
3470
+ { editKey: this.#toZapoKey(key) }
3471
+ );
3472
+ return this.#sent(result, key.chatId);
3473
+ });
3133
3474
  }
3134
3475
  async deleteMessage(key) {
3135
- await this.#requireClient().message.send(key.chatId, {
3136
- type: "revoke",
3137
- target: this.#toZapoKey(key)
3476
+ await this.#trackOutgoing(async () => {
3477
+ await this.#requireClient().message.send(key.chatId, {
3478
+ type: "revoke",
3479
+ target: this.#toZapoKey(key)
3480
+ });
3138
3481
  });
3139
3482
  }
3140
3483
  async getGroup(groupId) {
@@ -3186,13 +3529,15 @@ var ZapoProvider = class {
3186
3529
  return result.code;
3187
3530
  }
3188
3531
  async setMessagePin(groupId, key, pinned) {
3189
- await this.#requireClient().message.send(groupId, pinned ? {
3190
- type: "pin",
3191
- target: this.#toZapoKey(key),
3192
- durationSecs: 604800
3193
- } : {
3194
- type: "unpin",
3195
- target: this.#toZapoKey(key)
3532
+ await this.#trackOutgoing(async () => {
3533
+ await this.#requireClient().message.send(groupId, pinned ? {
3534
+ type: "pin",
3535
+ target: this.#toZapoKey(key),
3536
+ durationSecs: 604800
3537
+ } : {
3538
+ type: "unpin",
3539
+ target: this.#toZapoKey(key)
3540
+ });
3196
3541
  });
3197
3542
  }
3198
3543
  async updateParticipant(groupId, memberId, action) {
@@ -3233,6 +3578,14 @@ var ZapoProvider = class {
3233
3578
  ]
3234
3579
  });
3235
3580
  }
3581
+ async #trackOutgoing(operation) {
3582
+ try {
3583
+ return await operation();
3584
+ } catch (error) {
3585
+ this.#failedMessages += 1;
3586
+ throw error;
3587
+ }
3588
+ }
3236
3589
  async #ensureClient() {
3237
3590
  if (this.#client) return this.#client;
3238
3591
  const authPath = resolve2(this.#options.auth);
@@ -3256,13 +3609,29 @@ var ZapoProvider = class {
3256
3609
  markOnlineOnConnect: false,
3257
3610
  deviceBrowser: this.#deviceBrowser(),
3258
3611
  deviceOsDisplayName: this.#deviceOsDisplayName(),
3612
+ connectTimeoutMs: this.#connectTimeoutMs,
3613
+ nodeQueryTimeoutMs: this.#nodeQueryTimeoutMs,
3259
3614
  history: { enabled: true, requireFullSync: true },
3260
3615
  addons: {
3261
3616
  autoDecrypt: true,
3262
3617
  persistAllSecrets: true
3263
3618
  },
3264
3619
  media: { processor: sharedMediaProcessor }
3265
- }, new WhaNextZapoLogger(this.#logger));
3620
+ }, new WhaNextZapoLogger(
3621
+ this.#logger,
3622
+ {},
3623
+ (message, context) => this.#observeZapoWarning(message, context)
3624
+ ));
3625
+ if (!this.#cryptoBackendLogged) {
3626
+ this.#cryptoBackendLogged = true;
3627
+ if (this.#cryptoBackend === "napi" || this.#cryptoBackend === "wasm") {
3628
+ this.#logger.info("Crypto acceleration enabled.", { backend: this.#cryptoBackend });
3629
+ } else {
3630
+ this.#logger.debug("Crypto acceleration unavailable; using JavaScript backend.", {
3631
+ backend: this.#cryptoBackend
3632
+ });
3633
+ }
3634
+ }
3266
3635
  this.#storeEntry = storeEntry;
3267
3636
  this.#storePath = storePath;
3268
3637
  this.#client = client;
@@ -3311,8 +3680,19 @@ var ZapoProvider = class {
3311
3680
  }
3312
3681
  this.#handleMessage(event);
3313
3682
  });
3683
+ client.on("message_unavailable", (event) => {
3684
+ this.#handleUnavailableMessage(event);
3685
+ });
3686
+ client.on("debug_decrypted_payload", (event) => {
3687
+ this.#handleDecryptedPayload(event);
3688
+ });
3689
+ client.on("debug_unhandled_stanza", (event) => {
3690
+ this.#handleUnhandledStanza(event);
3691
+ });
3314
3692
  client.on("message_send", (event) => {
3315
3693
  if (!event.id || !event.message) return;
3694
+ this.#sentMessages += 1;
3695
+ this.#lastOutgoingAt = /* @__PURE__ */ new Date();
3316
3696
  this.#remember({
3317
3697
  key: {
3318
3698
  id: event.id,
@@ -3347,7 +3727,16 @@ var ZapoProvider = class {
3347
3727
  if (quoted?.key.id && quoted.message) {
3348
3728
  this.#remember(quoted);
3349
3729
  }
3730
+ const deliveryKey = this.#messageDeliveryKey(stored.key);
3731
+ this.#resolveUnavailableRecovery(deliveryKey, stored.key);
3732
+ const decryptedCorrelationKey = this.#stanzaCorrelationKey(
3733
+ stored.key.remoteJid ?? void 0,
3734
+ stored.key.id ?? void 0
3735
+ );
3736
+ if (decryptedCorrelationKey) this.#recentDecryptedPayloads.delete(decryptedCorrelationKey);
3350
3737
  if (this.#isOfflineMessage(stored)) {
3738
+ this.#ignoredOfflineMessages += 1;
3739
+ this.#emitMessageDiscarded("offline", stored.key);
3351
3740
  this.#logger.debug("Ignored message queued before the current live connection.", {
3352
3741
  messageId: stored.key.id ?? void 0,
3353
3742
  chatId: stored.key.remoteJid ?? void 0,
@@ -3355,8 +3744,9 @@ var ZapoProvider = class {
3355
3744
  });
3356
3745
  return;
3357
3746
  }
3358
- const deliveryKey = this.#messageDeliveryKey(stored.key);
3359
3747
  if (stored.key.id && this.#deliveredMessageStore.has(deliveryKey)) {
3748
+ this.#duplicateMessages += 1;
3749
+ this.#emitMessageDiscarded("duplicate", stored.key);
3360
3750
  this.#logger.debug("Ignored duplicate Zapo message event.", {
3361
3751
  messageId: stored.key.id,
3362
3752
  chatId: stored.key.remoteJid ?? void 0
@@ -3364,8 +3754,18 @@ var ZapoProvider = class {
3364
3754
  return;
3365
3755
  }
3366
3756
  const message = normalizeZapoMessage(event);
3367
- if (!message) return;
3757
+ if (!message) {
3758
+ this.#normalizationFailures += 1;
3759
+ this.#emitMessageDiscarded("normalization_failed", stored.key);
3760
+ this.#logger.warn("Could not normalize incoming Zapo message.", {
3761
+ messageId: stored.key.id ?? void 0,
3762
+ chatId: stored.key.remoteJid ?? void 0
3763
+ });
3764
+ return;
3765
+ }
3368
3766
  if (stored.key.id) this.#rememberDeliveredMessage(deliveryKey);
3767
+ this.#receivedMessages += 1;
3768
+ this.#lastIncomingAt = /* @__PURE__ */ new Date();
3369
3769
  this.#messageKeyStore.set(message.keys, stored);
3370
3770
  if (message.quoted && quoted?.message) {
3371
3771
  this.#messageKeyStore.set(
@@ -3375,6 +3775,162 @@ var ZapoProvider = class {
3375
3775
  }
3376
3776
  void this.#events.emit("message", message);
3377
3777
  }
3778
+ #handleUnavailableMessage(event) {
3779
+ this.#unavailableMessages += 1;
3780
+ const occurredAt = /* @__PURE__ */ new Date();
3781
+ const messageId = event.key.id || void 0;
3782
+ const chatId = event.key.remoteJid || void 0;
3783
+ const participantId = event.key.participant ?? event.key.participantAlt ?? void 0;
3784
+ this.#emitStability({
3785
+ type: "messageUnavailable",
3786
+ payload: {
3787
+ kind: event.kind,
3788
+ resendRequested: event.resendRequested,
3789
+ occurredAt,
3790
+ ...messageId ? { messageId } : {},
3791
+ ...chatId ? { chatId } : {},
3792
+ ...participantId ? { participantId } : {}
3793
+ }
3794
+ });
3795
+ if (!event.resendRequested) {
3796
+ this.#unavailableUnrecoverableMessages += 1;
3797
+ const log = event.kind === "other" ? this.#logger.warn.bind(this.#logger) : this.#logger.debug.bind(this.#logger);
3798
+ log("Incoming message is unavailable and was not queued for recovery.", {
3799
+ kind: event.kind,
3800
+ ...messageId ? { messageId } : {},
3801
+ ...chatId ? { chatId } : {},
3802
+ ...participantId ? { participantId } : {}
3803
+ });
3804
+ return;
3805
+ }
3806
+ this.#resendRequestedMessages += 1;
3807
+ const deliveryKey = this.#messageDeliveryKey(event.key);
3808
+ const previous = this.#pendingUnavailableRecoveries.get(deliveryKey);
3809
+ if (previous) clearTimeout(previous.timer);
3810
+ const requestedAt = Date.now();
3811
+ const timer = setTimeout(() => {
3812
+ const pending = this.#pendingUnavailableRecoveries.get(deliveryKey);
3813
+ if (!pending || pending.requestedAt !== requestedAt) return;
3814
+ this.#pendingUnavailableRecoveries.delete(deliveryKey);
3815
+ this.#recoveryFailedMessages += 1;
3816
+ this.#markDegraded();
3817
+ const failedAt = /* @__PURE__ */ new Date();
3818
+ this.#emitStability({
3819
+ type: "messageRecoveryFailed",
3820
+ payload: {
3821
+ failedAt,
3822
+ waitedMs: failedAt.getTime() - pending.requestedAt,
3823
+ ...pending.messageId ? { messageId: pending.messageId } : {},
3824
+ ...pending.chatId ? { chatId: pending.chatId } : {},
3825
+ ...pending.participantId ? { participantId: pending.participantId } : {}
3826
+ }
3827
+ });
3828
+ this.#logger.warn("Unavailable message recovery did not arrive in time.", {
3829
+ ...pending.messageId ? { messageId: pending.messageId } : {},
3830
+ ...pending.chatId ? { chatId: pending.chatId } : {},
3831
+ waitedMs: failedAt.getTime() - pending.requestedAt
3832
+ });
3833
+ }, MESSAGE_RECOVERY_TIMEOUT_MS);
3834
+ this.#pendingUnavailableRecoveries.set(deliveryKey, {
3835
+ requestedAt,
3836
+ timer,
3837
+ ...messageId ? { messageId } : {},
3838
+ ...chatId ? { chatId } : {},
3839
+ ...participantId ? { participantId } : {}
3840
+ });
3841
+ this.#logger.info("Incoming message unavailable; primary-device resend requested.", {
3842
+ ...messageId ? { messageId } : {},
3843
+ ...chatId ? { chatId } : {},
3844
+ ...participantId ? { participantId } : {}
3845
+ });
3846
+ }
3847
+ #resolveUnavailableRecovery(deliveryKey, key) {
3848
+ const pending = this.#pendingUnavailableRecoveries.get(deliveryKey);
3849
+ if (!pending) return;
3850
+ clearTimeout(pending.timer);
3851
+ this.#pendingUnavailableRecoveries.delete(deliveryKey);
3852
+ this.#recoveredMessages += 1;
3853
+ const recoveredAt = /* @__PURE__ */ new Date();
3854
+ const participantId = key.participant ?? key.participantAlt ?? pending.participantId;
3855
+ this.#emitStability({
3856
+ type: "messageRecovered",
3857
+ payload: {
3858
+ recoveredAt,
3859
+ recoveryMs: recoveredAt.getTime() - pending.requestedAt,
3860
+ ...key.id ? { messageId: key.id } : pending.messageId ? { messageId: pending.messageId } : {},
3861
+ ...key.remoteJid ? { chatId: key.remoteJid } : pending.chatId ? { chatId: pending.chatId } : {},
3862
+ ...participantId ? { participantId } : {}
3863
+ }
3864
+ });
3865
+ this.#logger.info("Recovered unavailable message from the primary device.", {
3866
+ ...key.id ? { messageId: key.id } : {},
3867
+ ...key.remoteJid ? { chatId: key.remoteJid } : {},
3868
+ recoveryMs: recoveredAt.getTime() - pending.requestedAt
3869
+ });
3870
+ }
3871
+ #handleDecryptedPayload(event) {
3872
+ this.#decryptedPayloads += 1;
3873
+ const correlationKey = this.#stanzaCorrelationKey(event.chatJid, event.stanzaId);
3874
+ if (!correlationKey) return;
3875
+ const now = Date.now();
3876
+ this.#recentDecryptedPayloads.set(correlationKey, {
3877
+ observedAt: now,
3878
+ encType: event.encType
3879
+ });
3880
+ for (const [key, value] of this.#recentDecryptedPayloads) {
3881
+ if (now - value.observedAt > DECRYPT_CORRELATION_TTL_MS) {
3882
+ this.#recentDecryptedPayloads.delete(key);
3883
+ }
3884
+ }
3885
+ }
3886
+ #handleUnhandledStanza(event) {
3887
+ this.#unhandledStanzas += 1;
3888
+ const correlationKey = this.#stanzaCorrelationKey(event.chatJid, event.stanzaId);
3889
+ const decrypted = correlationKey ? this.#recentDecryptedPayloads.get(correlationKey) : void 0;
3890
+ const now = Date.now();
3891
+ if (decrypted && now - decrypted.observedAt <= DECRYPT_CORRELATION_TTL_MS) {
3892
+ this.#decodeFailures += 1;
3893
+ this.#markDegraded();
3894
+ this.#emitStability({
3895
+ type: "messageDecodeFailure",
3896
+ payload: {
3897
+ occurredAt: new Date(now),
3898
+ reason: event.reason,
3899
+ encType: decrypted.encType,
3900
+ ...event.stanzaId ? { stanzaId: event.stanzaId } : {},
3901
+ ...event.chatJid ? { chatId: event.chatJid } : {}
3902
+ }
3903
+ });
3904
+ this.#logger.warn("Decrypted incoming payload could not be decoded into a supported stanza.", {
3905
+ reason: event.reason,
3906
+ encType: decrypted.encType,
3907
+ ...event.stanzaId ? { stanzaId: event.stanzaId } : {},
3908
+ ...event.chatJid ? { chatId: event.chatJid } : {}
3909
+ });
3910
+ if (correlationKey) this.#recentDecryptedPayloads.delete(correlationKey);
3911
+ return;
3912
+ }
3913
+ this.#logger.debug("Incoming stanza was not handled by Zapo.", {
3914
+ reason: event.reason,
3915
+ ...event.stanzaId ? { stanzaId: event.stanzaId } : {},
3916
+ ...event.chatJid ? { chatId: event.chatJid } : {}
3917
+ });
3918
+ }
3919
+ #emitMessageDiscarded(reason, key) {
3920
+ this.#emitStability({
3921
+ type: "messageDiscarded",
3922
+ payload: {
3923
+ reason,
3924
+ occurredAt: /* @__PURE__ */ new Date(),
3925
+ ...key.id ? { messageId: key.id } : {},
3926
+ ...key.remoteJid ? { chatId: key.remoteJid } : {}
3927
+ }
3928
+ });
3929
+ }
3930
+ #stanzaCorrelationKey(chatJid, stanzaId) {
3931
+ if (!stanzaId) return void 0;
3932
+ return `${chatJid ?? ""}:${stanzaId}`;
3933
+ }
3378
3934
  #handleAddonEvent(event) {
3379
3935
  const decrypted = this.#addonRecord(event.decrypted);
3380
3936
  const protocol = this.#addonProtocolMessage(decrypted);
@@ -3622,21 +4178,121 @@ var ZapoProvider = class {
3622
4178
  }
3623
4179
  void this.#events.emit("groupChanged", { groupId });
3624
4180
  }
4181
+ #observeZapoWarning(message, context) {
4182
+ if (message === GROUP_METADATA_MISMATCH_WARNING) {
4183
+ this.#phashMismatches += 1;
4184
+ this.#markDegraded();
4185
+ const groupId = stringValue(context.groupJid);
4186
+ if (groupId?.endsWith("@g.us")) void this.#recoverGroupMetadata(groupId);
4187
+ return;
4188
+ }
4189
+ if (message === "failed to decrypt incoming message") {
4190
+ this.#decryptFailures += 1;
4191
+ const detail = stringValue(context.message);
4192
+ const kind = detail === "sender key id mismatch" ? "sender_key_mismatch" : "decrypt_failure";
4193
+ if (kind === "sender_key_mismatch") this.#senderKeyMismatches += 1;
4194
+ this.#markDegraded();
4195
+ this.#emitCryptoDegraded(kind, context);
4196
+ return;
4197
+ }
4198
+ if (message === "addon auto-decrypt failed") {
4199
+ this.#addonDecryptFailures += 1;
4200
+ this.#markDegraded();
4201
+ this.#emitCryptoDegraded("addon_decrypt_failure", context);
4202
+ }
4203
+ }
4204
+ #emitCryptoDegraded(kind, context) {
4205
+ const messageId = stringValue(context.id);
4206
+ const chatId = stringValue(context.from) ?? stringValue(context.groupJid);
4207
+ const participantId = stringValue(context.participant);
4208
+ this.#emitStability({
4209
+ type: "cryptoDegraded",
4210
+ payload: {
4211
+ kind,
4212
+ occurredAt: /* @__PURE__ */ new Date(),
4213
+ ...messageId ? { messageId } : {},
4214
+ ...chatId ? { chatId } : {},
4215
+ ...participantId ? { participantId } : {}
4216
+ }
4217
+ });
4218
+ }
4219
+ #markDegraded() {
4220
+ this.#degradedUntil = Math.max(this.#degradedUntil, Date.now() + PROVIDER_DEGRADED_WINDOW_MS);
4221
+ if (this.#healthRefreshTimer) clearTimeout(this.#healthRefreshTimer);
4222
+ const delay = Math.max(1, this.#degradedUntil - Date.now() + 5);
4223
+ this.#healthRefreshTimer = setTimeout(() => {
4224
+ this.#healthRefreshTimer = void 0;
4225
+ this.#emitStability({
4226
+ type: "healthRefresh",
4227
+ payload: { occurredAt: /* @__PURE__ */ new Date() }
4228
+ });
4229
+ }, delay);
4230
+ }
4231
+ #emitStability(event) {
4232
+ void this.#events.emit("stability", event).catch((error) => {
4233
+ this.#logger.warn("Stability event listener failed.", {
4234
+ error: error instanceof Error ? error : new Error(String(error))
4235
+ });
4236
+ });
4237
+ }
4238
+ async #recoverGroupMetadata(groupId) {
4239
+ const client = this.#client;
4240
+ const storeEntry = this.#storeEntry;
4241
+ if (!client || !storeEntry || !this.#connected) return;
4242
+ const now = Date.now();
4243
+ const lastRecoveryAt = this.#groupMetadataRecoveryAt.get(groupId);
4244
+ if (lastRecoveryAt !== void 0 && now - lastRecoveryAt < GROUP_METADATA_RECOVERY_COOLDOWN_MS) {
4245
+ return;
4246
+ }
4247
+ if (this.#groupMetadataRecoveryInFlight.has(groupId)) return;
4248
+ this.#groupMetadataRecoveryAt.set(groupId, now);
4249
+ this.#groupMetadataRecoveryInFlight.add(groupId);
4250
+ try {
4251
+ const sessionStore = storeEntry.store.session(this.#sessionId());
4252
+ await sessionStore.groupMetadata.deleteGroupMetadata(groupId);
4253
+ await client.group.queryGroupMetadata(groupId);
4254
+ await this.#events.emit("groupChanged", { groupId });
4255
+ this.#metadataRecoveries += 1;
4256
+ const recoveredAt = /* @__PURE__ */ new Date();
4257
+ this.#emitStability({
4258
+ type: "groupMetadataRecovered",
4259
+ payload: { groupId, recoveredAt }
4260
+ });
4261
+ this.#logger.info("Refreshed group metadata after participant-hash mismatch.", {
4262
+ groupId
4263
+ });
4264
+ } catch (error) {
4265
+ this.#metadataRecoveryFailures += 1;
4266
+ this.#logger.warn("Could not refresh group metadata after participant-hash mismatch.", {
4267
+ groupId,
4268
+ error: error instanceof Error ? error : new Error(String(error))
4269
+ });
4270
+ } finally {
4271
+ this.#groupMetadataRecoveryInFlight.delete(groupId);
4272
+ }
4273
+ }
3625
4274
  async #handleConnectionEvent(client, event) {
3626
4275
  if (client !== this.#client) return;
3627
4276
  if (event.status === "open") {
4277
+ const wasReconnect = this.#reconnectAttempt > 0;
3628
4278
  this.#connectPromise = void 0;
3629
4279
  this.#connected = true;
4280
+ this.#state = "connected";
3630
4281
  this.#resolvePairingReady?.();
3631
4282
  this.#resolvePairingReady = void 0;
3632
4283
  this.#connectedAtSeconds = Math.floor(Date.now() / 1e3);
4284
+ this.#lastConnectedAt = /* @__PURE__ */ new Date();
4285
+ if (wasReconnect) this.#reconnects += 1;
3633
4286
  this.#reconnectAttempt = 0;
3634
4287
  this.#closedNotified = false;
4288
+ this.#groupMetadataRecoveryAt.clear();
4289
+ this.#groupMetadataRecoveryInFlight.clear();
3635
4290
  await this.#events.emit("connection", { state: "connected" });
3636
4291
  return;
3637
4292
  }
3638
4293
  this.#connectPromise = void 0;
3639
4294
  this.#connected = false;
4295
+ this.#lastDisconnectedAt = /* @__PURE__ */ new Date();
3640
4296
  const details = disconnectDetails(event.reason, event.code);
3641
4297
  const error = connectionError(event.reason, details);
3642
4298
  if (this.#intentionalClose || event.isLogout === true || shouldStopAutomaticReconnect(details)) {
@@ -3678,6 +4334,8 @@ var ZapoProvider = class {
3678
4334
  }
3679
4335
  this.#connectPromise = void 0;
3680
4336
  this.#connected = false;
4337
+ this.#state = "closed";
4338
+ this.#lastDisconnectedAt = /* @__PURE__ */ new Date();
3681
4339
  await this.#emitClosedOnce(error);
3682
4340
  this.#client = void 0;
3683
4341
  try {
@@ -3693,6 +4351,7 @@ var ZapoProvider = class {
3693
4351
  async #emitClosedOnce(error) {
3694
4352
  if (this.#closedNotified) return;
3695
4353
  this.#closedNotified = true;
4354
+ this.#state = "closed";
3696
4355
  await this.#events.emit("connection", {
3697
4356
  state: "closed",
3698
4357
  ...error ? { error } : {}
@@ -3714,15 +4373,16 @@ var ZapoProvider = class {
3714
4373
  async #scheduleReconnect(error, delayOverrideMs) {
3715
4374
  if (this.#reconnectTimer) return;
3716
4375
  const options = this.#options.reconnect;
3717
- const maxAttempts = options?.maxAttempts ?? 10;
4376
+ const maxAttempts = options?.maxAttempts ?? Number.POSITIVE_INFINITY;
3718
4377
  if (options?.enabled === false || this.#reconnectAttempt >= maxAttempts) {
3719
4378
  await this.#emitClosedOnce(error);
3720
4379
  await this.#releaseStore();
3721
4380
  return;
3722
4381
  }
3723
4382
  this.#reconnectAttempt += 1;
4383
+ this.#state = "reconnecting";
3724
4384
  await this.#events.emit("connection", {
3725
- state: "reconnecting",
4385
+ state: this.#state,
3726
4386
  attempt: this.#reconnectAttempt,
3727
4387
  ...error ? { error } : {}
3728
4388
  });
@@ -4008,11 +4668,16 @@ var ZapoProvider = class {
4008
4668
  return mentions.map((mention) => typeof mention === "string" ? mention : mention.mentionId);
4009
4669
  }
4010
4670
  #toZapoKey(key) {
4671
+ const original = this.#messageKeyStore.get(key)?.key;
4672
+ const participant = original?.participant ?? key.participantId;
4011
4673
  return {
4012
4674
  id: key.id,
4013
4675
  remoteJid: key.chatId,
4014
4676
  fromMe: key.fromMe,
4015
- ...key.participantId ? { participant: key.participantId } : {}
4677
+ ...original?.remoteJidAlt ? { remoteJidAlt: original.remoteJidAlt } : {},
4678
+ ...participant ? { participant } : {},
4679
+ ...original?.participantAlt ? { participantAlt: original.participantAlt } : {},
4680
+ ...original?.addressingMode ? { addressingMode: original.addressingMode } : {}
4016
4681
  };
4017
4682
  }
4018
4683
  #sent(result, chatId) {
@@ -4408,7 +5073,12 @@ function createZapoStoreEntry(storePath) {
4408
5073
  backends: {
4409
5074
  sqlite: createSqliteStore({
4410
5075
  path: storePath,
4411
- driver: "auto"
5076
+ driver: "auto",
5077
+ pragmas: {
5078
+ journal_mode: "WAL",
5079
+ synchronous: "NORMAL",
5080
+ busy_timeout: 5e3
5081
+ }
4412
5082
  })
4413
5083
  },
4414
5084
  providers: {
@@ -4426,11 +5096,19 @@ function createZapoStoreEntry(storePath) {
4426
5096
  },
4427
5097
  cacheProviders: {
4428
5098
  messageSecret: "sqlite"
5099
+ },
5100
+ memory: {
5101
+ cacheTtlMs: {
5102
+ groupMetadataMs: GROUP_METADATA_CACHE_TTL_MS,
5103
+ deviceListMs: DEVICE_LIST_CACHE_TTL_MS
5104
+ }
4429
5105
  }
4430
5106
  });
4431
5107
  const Database = require2("better-sqlite3");
4432
5108
  const snapshotDb = new Database(messageSnapshotStorePath(storePath));
4433
5109
  snapshotDb.exec(`
5110
+ PRAGMA journal_mode = WAL;
5111
+ PRAGMA synchronous = NORMAL;
4434
5112
  PRAGMA busy_timeout = 5000;
4435
5113
  CREATE TABLE IF NOT EXISTS whanext_message_snapshots (
4436
5114
  session_id TEXT NOT NULL,
@@ -4671,6 +5349,44 @@ function booleanValue(value) {
4671
5349
  if (value === false || value === 0) return false;
4672
5350
  return void 0;
4673
5351
  }
5352
+ function normalizeProviderTimeout(value, fallback) {
5353
+ if (value === void 0 || !Number.isFinite(value)) return fallback;
5354
+ return Math.max(1e3, Math.floor(value));
5355
+ }
5356
+ function detectCryptoBackend() {
5357
+ const requested = process.env.ZAPO_NATIVE_BACKEND?.trim().toLowerCase() ?? "auto";
5358
+ if (requested === "js" || requested === "none") return "js";
5359
+ if ((requested === "auto" || requested === "napi") && nativeNapiAvailable()) {
5360
+ return "napi";
5361
+ }
5362
+ if ((requested === "auto" || requested === "wasm") && supportsZapoWasmRuntime() && moduleResolvable("@zapo-js/native/wasm/pkg/zapo_native_wasm.js") && moduleResolvable("@zapo-js/native/wasm/pkg/zapo_native_wasm_bg.wasm")) {
5363
+ return "wasm";
5364
+ }
5365
+ return "js";
5366
+ }
5367
+ function nativeNapiAvailable() {
5368
+ try {
5369
+ require2("@zapo-js/native");
5370
+ return true;
5371
+ } catch {
5372
+ return false;
5373
+ }
5374
+ }
5375
+ function moduleResolvable(specifier) {
5376
+ try {
5377
+ require2.resolve(specifier);
5378
+ return true;
5379
+ } catch {
5380
+ return false;
5381
+ }
5382
+ }
5383
+ function supportsZapoWasmRuntime() {
5384
+ const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
5385
+ if (major > 22) return true;
5386
+ if (major === 22) return minor >= 12;
5387
+ if (major === 20) return minor >= 19;
5388
+ return false;
5389
+ }
4674
5390
  function bytesField(value) {
4675
5391
  if (value instanceof Uint8Array) return value;
4676
5392
  return void 0;
@@ -4679,9 +5395,11 @@ var WhaNextZapoLogger = class _WhaNextZapoLogger {
4679
5395
  level;
4680
5396
  #logger;
4681
5397
  #context;
4682
- constructor(logger, context = {}) {
5398
+ #onWarn;
5399
+ constructor(logger, context = {}, onWarn) {
4683
5400
  this.#logger = logger;
4684
5401
  this.#context = context;
5402
+ this.#onWarn = onWarn;
4685
5403
  this.level = logger.level === "debug" ? "debug" : logger.level === "warn" ? "warn" : logger.level === "error" || logger.level === "silent" ? "error" : "info";
4686
5404
  }
4687
5405
  trace(message, context) {
@@ -4694,7 +5412,9 @@ var WhaNextZapoLogger = class _WhaNextZapoLogger {
4694
5412
  this.#logger.info(message, this.#merge(context));
4695
5413
  }
4696
5414
  warn(message, context) {
4697
- this.#logger.warn(message, this.#merge(context));
5415
+ const merged = this.#merge(context);
5416
+ this.#logger.warn(message, merged);
5417
+ this.#onWarn?.(message, merged);
4698
5418
  }
4699
5419
  error(message, context) {
4700
5420
  this.#logger.error(message, this.#merge(context));
@@ -4703,7 +5423,7 @@ var WhaNextZapoLogger = class _WhaNextZapoLogger {
4703
5423
  return new _WhaNextZapoLogger(this.#logger, {
4704
5424
  ...this.#context,
4705
5425
  ...bindings
4706
- });
5426
+ }, this.#onWarn);
4707
5427
  }
4708
5428
  #merge(context) {
4709
5429
  return context ? { ...this.#context, ...context } : this.#context;
@@ -4731,7 +5451,9 @@ async function create(options = {}) {
4731
5451
  ...options.accountId ? { sessionId: options.accountId } : {},
4732
5452
  ...options.messageCacheSize !== void 0 ? { messageCacheSize: options.messageCacheSize } : {},
4733
5453
  ...options.processOfflineMessages !== void 0 ? { processOfflineMessages: options.processOfflineMessages } : {},
4734
- ...options.reconnect ? { reconnect: options.reconnect } : {}
5454
+ ...options.reconnect ? { reconnect: options.reconnect } : {},
5455
+ ...options.providerTimeouts?.connectTimeoutMs !== void 0 ? { connectTimeoutMs: options.providerTimeouts.connectTimeoutMs } : {},
5456
+ ...options.providerTimeouts?.nodeQueryTimeoutMs !== void 0 ? { nodeQueryTimeoutMs: options.providerTimeouts.nodeQueryTimeoutMs } : {}
4735
5457
  });
4736
5458
  return new WhaNextApp(provider, {
4737
5459
  ...options.accountId ? { accountId: options.accountId } : {},
@@ -4963,6 +5685,12 @@ function mergeCreateOptions(shared, account) {
4963
5685
  ...account.reconnect
4964
5686
  };
4965
5687
  }
5688
+ if (shared.providerTimeouts || account.providerTimeouts) {
5689
+ merged.providerTimeouts = {
5690
+ ...shared.providerTimeouts,
5691
+ ...account.providerTimeouts
5692
+ };
5693
+ }
4966
5694
  if (shared.logger && account.logger && typeof shared.logger === "object" && typeof account.logger === "object") {
4967
5695
  merged.logger = {
4968
5696
  ...shared.logger,