@whanext/core 0.19.16 → 0.19.18

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,50 @@ 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
+ };
2351
+ const crypto = provider?.crypto ?? {
2352
+ backend: "unknown",
2353
+ acceleration: false,
2354
+ decryptFailures: 0,
2355
+ addonDecryptFailures: 0,
2356
+ senderKeyMismatches: 0
2357
+ };
2358
+ const groups = provider?.groups ?? {
2359
+ phashMismatches: 0,
2360
+ metadataRecoveries: 0,
2361
+ metadataRecoveryFailures: 0
2362
+ };
2363
+ const timeouts = provider?.timeouts ?? {
2364
+ connectTimeoutMs: 0,
2365
+ nodeQueryTimeoutMs: 0
2366
+ };
2367
+ const stability = provider?.stability ?? this.#fallbackStability();
2212
2368
  return {
2213
2369
  status: this.#healthStatus(),
2370
+ stability,
2214
2371
  state: this.#state,
2215
2372
  ready: this.isReady,
2216
2373
  uptimeMs: Date.now() - this.#startedAt,
2217
2374
  timestamp: /* @__PURE__ */ new Date(),
2218
2375
  muteEnabled: this.mute.enabled,
2219
- logLevel: this.logger.level
2376
+ logLevel: this.logger.level,
2377
+ connection,
2378
+ messaging,
2379
+ crypto,
2380
+ groups,
2381
+ commands: this.#router.health(),
2382
+ timeouts
2220
2383
  };
2221
2384
  }
2222
2385
  router() {
@@ -2286,9 +2449,32 @@ var WhaNextApp = class {
2286
2449
  }
2287
2450
  #bind() {
2288
2451
  this.#provider.on("connection", async (update) => {
2452
+ const previousState = this.#state;
2289
2453
  this.#state = update.state;
2290
2454
  this.#logConnection(update);
2291
2455
  await this.#events.emit("connection", update);
2456
+ if (update.state === "connected" && previousState === "reconnecting") {
2457
+ const providerHealth = this.#provider.health?.();
2458
+ await this.#events.emit("connectionRecovered", {
2459
+ recoveredAt: /* @__PURE__ */ new Date(),
2460
+ reconnects: providerHealth?.connection.reconnects ?? 1
2461
+ });
2462
+ }
2463
+ await this.#refreshHealthState();
2464
+ });
2465
+ this.#provider.on("stability", async (event) => {
2466
+ if (event.type === "groupMetadataRecovered") {
2467
+ await this.#events.emit("groupMetadataRecovered", event.payload);
2468
+ } else if (event.type === "cryptoDegraded") {
2469
+ await this.#events.emit("cryptoDegraded", event.payload);
2470
+ }
2471
+ await this.#refreshHealthState();
2472
+ });
2473
+ this.#router.on("commandQueueTimeout", async (event) => {
2474
+ await this.#events.emit("commandQueueTimeout", event);
2475
+ });
2476
+ this.#router.on("commandQueueFull", async (event) => {
2477
+ await this.#events.emit("commandQueueFull", event);
2292
2478
  });
2293
2479
  this.#provider.on("groupChanged", ({ groupId }) => this.group.invalidate(groupId));
2294
2480
  this.#provider.on("groupParticipantsChanged", async (change) => {
@@ -2356,6 +2542,22 @@ var WhaNextApp = class {
2356
2542
  }
2357
2543
  });
2358
2544
  }
2545
+ #fallbackStability() {
2546
+ if (this.#state === "connected") return "healthy";
2547
+ if (this.#state === "connecting" || this.#state === "reconnecting") return "reconnecting";
2548
+ return "offline";
2549
+ }
2550
+ async #refreshHealthState() {
2551
+ const health = this.health();
2552
+ if (health.stability === this.#lastStability) return;
2553
+ const previous = this.#lastStability;
2554
+ this.#lastStability = health.stability;
2555
+ await this.#events.emit("healthChanged", {
2556
+ previous,
2557
+ current: health.stability,
2558
+ health
2559
+ });
2560
+ }
2359
2561
  #healthStatus() {
2360
2562
  if (this.#state === "connected") {
2361
2563
  return "ready";
@@ -2932,6 +3134,13 @@ var fatalDisconnectReasons = /* @__PURE__ */ new Set([
2932
3134
  var fatalDisconnectCodes = /* @__PURE__ */ new Set([401, 403, 405, 406, 409, 516]);
2933
3135
  var sharedMediaProcessor = createMediaProcessor();
2934
3136
  var REMOTE_MEDIA_TIMEOUT_MS = 12e4;
3137
+ var DEFAULT_CONNECT_TIMEOUT_MS = 15e3;
3138
+ var DEFAULT_NODE_QUERY_TIMEOUT_MS = 3e4;
3139
+ var PROVIDER_DEGRADED_WINDOW_MS = 12e4;
3140
+ var GROUP_METADATA_CACHE_TTL_MS = 18e4;
3141
+ var DEVICE_LIST_CACHE_TTL_MS = 18e4;
3142
+ var GROUP_METADATA_RECOVERY_COOLDOWN_MS = 6e4;
3143
+ var GROUP_METADATA_MISMATCH_WARNING = "group message publish acknowledged with mismatch metadata";
2935
3144
  var messageSnapshotRetentionSeconds = 7 * 24 * 60 * 60;
2936
3145
  var messageSnapshotMaxPerSession = 2e4;
2937
3146
  var messageSnapshotPruneInterval = 256;
@@ -2944,8 +3153,14 @@ var ZapoProvider = class {
2944
3153
  #deliveredMessageStore = /* @__PURE__ */ new Set();
2945
3154
  #handledProtocolStore = /* @__PURE__ */ new Set();
2946
3155
  #callCreatorStore = /* @__PURE__ */ new Map();
3156
+ #groupMetadataRecoveryAt = /* @__PURE__ */ new Map();
3157
+ #groupMetadataRecoveryInFlight = /* @__PURE__ */ new Set();
2947
3158
  #messageCacheSize;
3159
+ #cryptoBackend;
3160
+ #connectTimeoutMs;
3161
+ #nodeQueryTimeoutMs;
2948
3162
  #protocolMutationQueue = Promise.resolve();
3163
+ #state = "idle";
2949
3164
  #client;
2950
3165
  #storeEntry;
2951
3166
  #storePath;
@@ -2960,14 +3175,73 @@ var ZapoProvider = class {
2960
3175
  #messageSnapshotsSincePrune = 0;
2961
3176
  #pairingReady = Promise.resolve();
2962
3177
  #resolvePairingReady;
3178
+ #lastConnectedAt;
3179
+ #lastDisconnectedAt;
3180
+ #reconnects = 0;
3181
+ #sentMessages = 0;
3182
+ #receivedMessages = 0;
3183
+ #failedMessages = 0;
3184
+ #lastIncomingAt;
3185
+ #lastOutgoingAt;
3186
+ #decryptFailures = 0;
3187
+ #addonDecryptFailures = 0;
3188
+ #senderKeyMismatches = 0;
3189
+ #phashMismatches = 0;
3190
+ #metadataRecoveries = 0;
3191
+ #metadataRecoveryFailures = 0;
3192
+ #degradedUntil = 0;
3193
+ #healthRefreshTimer;
3194
+ #cryptoBackendLogged = false;
2963
3195
  constructor(options) {
2964
3196
  this.#options = options;
2965
3197
  this.#logger = options.logger ?? new Logger("silent");
2966
3198
  this.#messageCacheSize = Math.max(1, options.messageCacheSize ?? 1e3);
3199
+ this.#connectTimeoutMs = normalizeProviderTimeout(options.connectTimeoutMs, DEFAULT_CONNECT_TIMEOUT_MS);
3200
+ this.#nodeQueryTimeoutMs = normalizeProviderTimeout(options.nodeQueryTimeoutMs, DEFAULT_NODE_QUERY_TIMEOUT_MS);
3201
+ this.#cryptoBackend = detectCryptoBackend();
2967
3202
  }
2968
3203
  on(event, listener) {
2969
3204
  return this.#events.on(event, listener);
2970
3205
  }
3206
+ health() {
3207
+ const now = Date.now();
3208
+ const state = this.#state;
3209
+ const stability = state === "connected" ? now < this.#degradedUntil ? "degraded" : "healthy" : state === "reconnecting" || state === "connecting" ? "reconnecting" : "offline";
3210
+ return {
3211
+ stability,
3212
+ connection: {
3213
+ state,
3214
+ uptimeMs: this.#connectedAtSeconds > 0 && this.#connected ? Math.max(0, now - this.#connectedAtSeconds * 1e3) : 0,
3215
+ reconnects: this.#reconnects,
3216
+ reconnectAttempt: this.#reconnectAttempt,
3217
+ ...this.#lastConnectedAt ? { lastConnectedAt: new Date(this.#lastConnectedAt) } : {},
3218
+ ...this.#lastDisconnectedAt ? { lastDisconnectedAt: new Date(this.#lastDisconnectedAt) } : {}
3219
+ },
3220
+ messaging: {
3221
+ sent: this.#sentMessages,
3222
+ received: this.#receivedMessages,
3223
+ failed: this.#failedMessages,
3224
+ ...this.#lastIncomingAt ? { lastIncomingAt: new Date(this.#lastIncomingAt) } : {},
3225
+ ...this.#lastOutgoingAt ? { lastOutgoingAt: new Date(this.#lastOutgoingAt) } : {}
3226
+ },
3227
+ crypto: {
3228
+ backend: this.#cryptoBackend,
3229
+ acceleration: this.#cryptoBackend === "napi" || this.#cryptoBackend === "wasm",
3230
+ decryptFailures: this.#decryptFailures,
3231
+ addonDecryptFailures: this.#addonDecryptFailures,
3232
+ senderKeyMismatches: this.#senderKeyMismatches
3233
+ },
3234
+ groups: {
3235
+ phashMismatches: this.#phashMismatches,
3236
+ metadataRecoveries: this.#metadataRecoveries,
3237
+ metadataRecoveryFailures: this.#metadataRecoveryFailures
3238
+ },
3239
+ timeouts: {
3240
+ connectTimeoutMs: this.#connectTimeoutMs,
3241
+ nodeQueryTimeoutMs: this.#nodeQueryTimeoutMs
3242
+ }
3243
+ };
3244
+ }
2971
3245
  async connect() {
2972
3246
  this.#intentionalClose = false;
2973
3247
  this.#closedNotified = false;
@@ -2976,8 +3250,9 @@ var ZapoProvider = class {
2976
3250
  return;
2977
3251
  }
2978
3252
  this.#preparePairingGate();
3253
+ this.#state = this.#reconnectAttempt > 0 ? "reconnecting" : "connecting";
2979
3254
  await this.#events.emit("connection", {
2980
- state: this.#reconnectAttempt > 0 ? "reconnecting" : "connecting",
3255
+ state: this.#state,
2981
3256
  attempt: this.#reconnectAttempt
2982
3257
  });
2983
3258
  this.#startConnect(client);
@@ -2988,6 +3263,10 @@ var ZapoProvider = class {
2988
3263
  clearTimeout(this.#reconnectTimer);
2989
3264
  this.#reconnectTimer = void 0;
2990
3265
  }
3266
+ if (this.#healthRefreshTimer) {
3267
+ clearTimeout(this.#healthRefreshTimer);
3268
+ this.#healthRefreshTimer = void 0;
3269
+ }
2991
3270
  const client = this.#client;
2992
3271
  this.#connectPromise = void 0;
2993
3272
  try {
@@ -2998,6 +3277,8 @@ var ZapoProvider = class {
2998
3277
  }
2999
3278
  } finally {
3000
3279
  this.#connected = false;
3280
+ this.#state = "closed";
3281
+ this.#lastDisconnectedAt = /* @__PURE__ */ new Date();
3001
3282
  await this.#releaseStore();
3002
3283
  }
3003
3284
  }
@@ -3045,50 +3326,56 @@ var ZapoProvider = class {
3045
3326
  }
3046
3327
  }
3047
3328
  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 } : {}
3329
+ return this.#trackOutgoing(async () => {
3330
+ if ("buttons" in content) {
3331
+ return this.#sendButtons(chatId, content, replyTo);
3332
+ }
3333
+ if ("list" in content) {
3334
+ return this.#sendList(chatId, content, replyTo);
3335
+ }
3336
+ const client = this.#requireClient();
3337
+ const { value, mentions, viewOnce } = await this.#toContent(content);
3338
+ const result = await client.message.send(chatId, value, {
3339
+ ...replyTo ? { quote: this.#toZapoKey(replyTo) } : {},
3340
+ ...mentions.length > 0 ? { mentions } : {},
3341
+ ...viewOnce !== void 0 ? { viewOnce } : {}
3342
+ });
3343
+ return this.#sent(result, chatId);
3060
3344
  });
3061
- return this.#sent(result, chatId);
3062
3345
  }
3063
3346
  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.",
3347
+ return this.#trackOutgoing(async () => {
3348
+ const original = await this.#findStoredMessage(this.#toZapoKey(source));
3349
+ if (!original?.message) {
3350
+ throw new WhaNextError(
3351
+ "MESSAGE_NOT_FOUND",
3352
+ "The source message is no longer available in the recent-message cache.",
3353
+ {
3354
+ context: { messageId: source.id, chatId: source.chatId },
3355
+ recoverable: true
3356
+ }
3357
+ );
3358
+ }
3359
+ const result = await this.#requireClient().message.send(
3360
+ chatId,
3361
+ original.message,
3069
3362
  {
3070
- context: { messageId: source.id, chatId: source.chatId },
3071
- recoverable: true
3363
+ forward: true,
3364
+ ...options.mentions && options.mentions.length > 0 ? { mentions: this.#mentions(options.mentions) } : {}
3072
3365
  }
3073
3366
  );
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);
3367
+ return this.#sent(result, chatId);
3368
+ });
3084
3369
  }
3085
3370
  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)
3371
+ return this.#trackOutgoing(async () => {
3372
+ const result = await this.#requireClient().message.send(key.chatId, {
3373
+ type: "reaction",
3374
+ emoji: emoji ?? "",
3375
+ target: this.#toZapoKey(key)
3376
+ });
3377
+ return this.#sent(result, key.chatId);
3090
3378
  });
3091
- return this.#sent(result, key.chatId);
3092
3379
  }
3093
3380
  async downloadMedia(key) {
3094
3381
  const message = this.#messageKeyStore.get(key) ?? await this.#findStoredMessage(this.#toZapoKey(key));
@@ -3124,17 +3411,21 @@ var ZapoProvider = class {
3124
3411
  }
3125
3412
  }
3126
3413
  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);
3414
+ return this.#trackOutgoing(async () => {
3415
+ const result = await this.#requireClient().message.send(
3416
+ key.chatId,
3417
+ content,
3418
+ { editKey: this.#toZapoKey(key) }
3419
+ );
3420
+ return this.#sent(result, key.chatId);
3421
+ });
3133
3422
  }
3134
3423
  async deleteMessage(key) {
3135
- await this.#requireClient().message.send(key.chatId, {
3136
- type: "revoke",
3137
- target: this.#toZapoKey(key)
3424
+ await this.#trackOutgoing(async () => {
3425
+ await this.#requireClient().message.send(key.chatId, {
3426
+ type: "revoke",
3427
+ target: this.#toZapoKey(key)
3428
+ });
3138
3429
  });
3139
3430
  }
3140
3431
  async getGroup(groupId) {
@@ -3186,13 +3477,15 @@ var ZapoProvider = class {
3186
3477
  return result.code;
3187
3478
  }
3188
3479
  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)
3480
+ await this.#trackOutgoing(async () => {
3481
+ await this.#requireClient().message.send(groupId, pinned ? {
3482
+ type: "pin",
3483
+ target: this.#toZapoKey(key),
3484
+ durationSecs: 604800
3485
+ } : {
3486
+ type: "unpin",
3487
+ target: this.#toZapoKey(key)
3488
+ });
3196
3489
  });
3197
3490
  }
3198
3491
  async updateParticipant(groupId, memberId, action) {
@@ -3233,6 +3526,14 @@ var ZapoProvider = class {
3233
3526
  ]
3234
3527
  });
3235
3528
  }
3529
+ async #trackOutgoing(operation) {
3530
+ try {
3531
+ return await operation();
3532
+ } catch (error) {
3533
+ this.#failedMessages += 1;
3534
+ throw error;
3535
+ }
3536
+ }
3236
3537
  async #ensureClient() {
3237
3538
  if (this.#client) return this.#client;
3238
3539
  const authPath = resolve2(this.#options.auth);
@@ -3256,13 +3557,29 @@ var ZapoProvider = class {
3256
3557
  markOnlineOnConnect: false,
3257
3558
  deviceBrowser: this.#deviceBrowser(),
3258
3559
  deviceOsDisplayName: this.#deviceOsDisplayName(),
3560
+ connectTimeoutMs: this.#connectTimeoutMs,
3561
+ nodeQueryTimeoutMs: this.#nodeQueryTimeoutMs,
3259
3562
  history: { enabled: true, requireFullSync: true },
3260
3563
  addons: {
3261
3564
  autoDecrypt: true,
3262
3565
  persistAllSecrets: true
3263
3566
  },
3264
3567
  media: { processor: sharedMediaProcessor }
3265
- }, new WhaNextZapoLogger(this.#logger));
3568
+ }, new WhaNextZapoLogger(
3569
+ this.#logger,
3570
+ {},
3571
+ (message, context) => this.#observeZapoWarning(message, context)
3572
+ ));
3573
+ if (!this.#cryptoBackendLogged) {
3574
+ this.#cryptoBackendLogged = true;
3575
+ if (this.#cryptoBackend === "napi" || this.#cryptoBackend === "wasm") {
3576
+ this.#logger.info("Crypto acceleration enabled.", { backend: this.#cryptoBackend });
3577
+ } else {
3578
+ this.#logger.debug("Crypto acceleration unavailable; using JavaScript backend.", {
3579
+ backend: this.#cryptoBackend
3580
+ });
3581
+ }
3582
+ }
3266
3583
  this.#storeEntry = storeEntry;
3267
3584
  this.#storePath = storePath;
3268
3585
  this.#client = client;
@@ -3313,6 +3630,8 @@ var ZapoProvider = class {
3313
3630
  });
3314
3631
  client.on("message_send", (event) => {
3315
3632
  if (!event.id || !event.message) return;
3633
+ this.#sentMessages += 1;
3634
+ this.#lastOutgoingAt = /* @__PURE__ */ new Date();
3316
3635
  this.#remember({
3317
3636
  key: {
3318
3637
  id: event.id,
@@ -3366,6 +3685,8 @@ var ZapoProvider = class {
3366
3685
  const message = normalizeZapoMessage(event);
3367
3686
  if (!message) return;
3368
3687
  if (stored.key.id) this.#rememberDeliveredMessage(deliveryKey);
3688
+ this.#receivedMessages += 1;
3689
+ this.#lastIncomingAt = /* @__PURE__ */ new Date();
3369
3690
  this.#messageKeyStore.set(message.keys, stored);
3370
3691
  if (message.quoted && quoted?.message) {
3371
3692
  this.#messageKeyStore.set(
@@ -3622,21 +3943,121 @@ var ZapoProvider = class {
3622
3943
  }
3623
3944
  void this.#events.emit("groupChanged", { groupId });
3624
3945
  }
3946
+ #observeZapoWarning(message, context) {
3947
+ if (message === GROUP_METADATA_MISMATCH_WARNING) {
3948
+ this.#phashMismatches += 1;
3949
+ this.#markDegraded();
3950
+ const groupId = stringValue(context.groupJid);
3951
+ if (groupId?.endsWith("@g.us")) void this.#recoverGroupMetadata(groupId);
3952
+ return;
3953
+ }
3954
+ if (message === "failed to decrypt incoming message") {
3955
+ this.#decryptFailures += 1;
3956
+ const detail = stringValue(context.message);
3957
+ const kind = detail === "sender key id mismatch" ? "sender_key_mismatch" : "decrypt_failure";
3958
+ if (kind === "sender_key_mismatch") this.#senderKeyMismatches += 1;
3959
+ this.#markDegraded();
3960
+ this.#emitCryptoDegraded(kind, context);
3961
+ return;
3962
+ }
3963
+ if (message === "addon auto-decrypt failed") {
3964
+ this.#addonDecryptFailures += 1;
3965
+ this.#markDegraded();
3966
+ this.#emitCryptoDegraded("addon_decrypt_failure", context);
3967
+ }
3968
+ }
3969
+ #emitCryptoDegraded(kind, context) {
3970
+ const messageId = stringValue(context.id);
3971
+ const chatId = stringValue(context.from) ?? stringValue(context.groupJid);
3972
+ const participantId = stringValue(context.participant);
3973
+ this.#emitStability({
3974
+ type: "cryptoDegraded",
3975
+ payload: {
3976
+ kind,
3977
+ occurredAt: /* @__PURE__ */ new Date(),
3978
+ ...messageId ? { messageId } : {},
3979
+ ...chatId ? { chatId } : {},
3980
+ ...participantId ? { participantId } : {}
3981
+ }
3982
+ });
3983
+ }
3984
+ #markDegraded() {
3985
+ this.#degradedUntil = Math.max(this.#degradedUntil, Date.now() + PROVIDER_DEGRADED_WINDOW_MS);
3986
+ if (this.#healthRefreshTimer) clearTimeout(this.#healthRefreshTimer);
3987
+ const delay = Math.max(1, this.#degradedUntil - Date.now() + 5);
3988
+ this.#healthRefreshTimer = setTimeout(() => {
3989
+ this.#healthRefreshTimer = void 0;
3990
+ this.#emitStability({
3991
+ type: "healthRefresh",
3992
+ payload: { occurredAt: /* @__PURE__ */ new Date() }
3993
+ });
3994
+ }, delay);
3995
+ }
3996
+ #emitStability(event) {
3997
+ void this.#events.emit("stability", event).catch((error) => {
3998
+ this.#logger.warn("Stability event listener failed.", {
3999
+ error: error instanceof Error ? error : new Error(String(error))
4000
+ });
4001
+ });
4002
+ }
4003
+ async #recoverGroupMetadata(groupId) {
4004
+ const client = this.#client;
4005
+ const storeEntry = this.#storeEntry;
4006
+ if (!client || !storeEntry || !this.#connected) return;
4007
+ const now = Date.now();
4008
+ const lastRecoveryAt = this.#groupMetadataRecoveryAt.get(groupId);
4009
+ if (lastRecoveryAt !== void 0 && now - lastRecoveryAt < GROUP_METADATA_RECOVERY_COOLDOWN_MS) {
4010
+ return;
4011
+ }
4012
+ if (this.#groupMetadataRecoveryInFlight.has(groupId)) return;
4013
+ this.#groupMetadataRecoveryAt.set(groupId, now);
4014
+ this.#groupMetadataRecoveryInFlight.add(groupId);
4015
+ try {
4016
+ const sessionStore = storeEntry.store.session(this.#sessionId());
4017
+ await sessionStore.groupMetadata.deleteGroupMetadata(groupId);
4018
+ await client.group.queryGroupMetadata(groupId);
4019
+ await this.#events.emit("groupChanged", { groupId });
4020
+ this.#metadataRecoveries += 1;
4021
+ const recoveredAt = /* @__PURE__ */ new Date();
4022
+ this.#emitStability({
4023
+ type: "groupMetadataRecovered",
4024
+ payload: { groupId, recoveredAt }
4025
+ });
4026
+ this.#logger.info("Refreshed group metadata after participant-hash mismatch.", {
4027
+ groupId
4028
+ });
4029
+ } catch (error) {
4030
+ this.#metadataRecoveryFailures += 1;
4031
+ this.#logger.warn("Could not refresh group metadata after participant-hash mismatch.", {
4032
+ groupId,
4033
+ error: error instanceof Error ? error : new Error(String(error))
4034
+ });
4035
+ } finally {
4036
+ this.#groupMetadataRecoveryInFlight.delete(groupId);
4037
+ }
4038
+ }
3625
4039
  async #handleConnectionEvent(client, event) {
3626
4040
  if (client !== this.#client) return;
3627
4041
  if (event.status === "open") {
4042
+ const wasReconnect = this.#reconnectAttempt > 0;
3628
4043
  this.#connectPromise = void 0;
3629
4044
  this.#connected = true;
4045
+ this.#state = "connected";
3630
4046
  this.#resolvePairingReady?.();
3631
4047
  this.#resolvePairingReady = void 0;
3632
4048
  this.#connectedAtSeconds = Math.floor(Date.now() / 1e3);
4049
+ this.#lastConnectedAt = /* @__PURE__ */ new Date();
4050
+ if (wasReconnect) this.#reconnects += 1;
3633
4051
  this.#reconnectAttempt = 0;
3634
4052
  this.#closedNotified = false;
4053
+ this.#groupMetadataRecoveryAt.clear();
4054
+ this.#groupMetadataRecoveryInFlight.clear();
3635
4055
  await this.#events.emit("connection", { state: "connected" });
3636
4056
  return;
3637
4057
  }
3638
4058
  this.#connectPromise = void 0;
3639
4059
  this.#connected = false;
4060
+ this.#lastDisconnectedAt = /* @__PURE__ */ new Date();
3640
4061
  const details = disconnectDetails(event.reason, event.code);
3641
4062
  const error = connectionError(event.reason, details);
3642
4063
  if (this.#intentionalClose || event.isLogout === true || shouldStopAutomaticReconnect(details)) {
@@ -3678,6 +4099,8 @@ var ZapoProvider = class {
3678
4099
  }
3679
4100
  this.#connectPromise = void 0;
3680
4101
  this.#connected = false;
4102
+ this.#state = "closed";
4103
+ this.#lastDisconnectedAt = /* @__PURE__ */ new Date();
3681
4104
  await this.#emitClosedOnce(error);
3682
4105
  this.#client = void 0;
3683
4106
  try {
@@ -3693,6 +4116,7 @@ var ZapoProvider = class {
3693
4116
  async #emitClosedOnce(error) {
3694
4117
  if (this.#closedNotified) return;
3695
4118
  this.#closedNotified = true;
4119
+ this.#state = "closed";
3696
4120
  await this.#events.emit("connection", {
3697
4121
  state: "closed",
3698
4122
  ...error ? { error } : {}
@@ -3714,15 +4138,16 @@ var ZapoProvider = class {
3714
4138
  async #scheduleReconnect(error, delayOverrideMs) {
3715
4139
  if (this.#reconnectTimer) return;
3716
4140
  const options = this.#options.reconnect;
3717
- const maxAttempts = options?.maxAttempts ?? 10;
4141
+ const maxAttempts = options?.maxAttempts ?? Number.POSITIVE_INFINITY;
3718
4142
  if (options?.enabled === false || this.#reconnectAttempt >= maxAttempts) {
3719
4143
  await this.#emitClosedOnce(error);
3720
4144
  await this.#releaseStore();
3721
4145
  return;
3722
4146
  }
3723
4147
  this.#reconnectAttempt += 1;
4148
+ this.#state = "reconnecting";
3724
4149
  await this.#events.emit("connection", {
3725
- state: "reconnecting",
4150
+ state: this.#state,
3726
4151
  attempt: this.#reconnectAttempt,
3727
4152
  ...error ? { error } : {}
3728
4153
  });
@@ -4408,7 +4833,12 @@ function createZapoStoreEntry(storePath) {
4408
4833
  backends: {
4409
4834
  sqlite: createSqliteStore({
4410
4835
  path: storePath,
4411
- driver: "auto"
4836
+ driver: "auto",
4837
+ pragmas: {
4838
+ journal_mode: "WAL",
4839
+ synchronous: "NORMAL",
4840
+ busy_timeout: 5e3
4841
+ }
4412
4842
  })
4413
4843
  },
4414
4844
  providers: {
@@ -4426,11 +4856,19 @@ function createZapoStoreEntry(storePath) {
4426
4856
  },
4427
4857
  cacheProviders: {
4428
4858
  messageSecret: "sqlite"
4859
+ },
4860
+ memory: {
4861
+ cacheTtlMs: {
4862
+ groupMetadataMs: GROUP_METADATA_CACHE_TTL_MS,
4863
+ deviceListMs: DEVICE_LIST_CACHE_TTL_MS
4864
+ }
4429
4865
  }
4430
4866
  });
4431
4867
  const Database = require2("better-sqlite3");
4432
4868
  const snapshotDb = new Database(messageSnapshotStorePath(storePath));
4433
4869
  snapshotDb.exec(`
4870
+ PRAGMA journal_mode = WAL;
4871
+ PRAGMA synchronous = NORMAL;
4434
4872
  PRAGMA busy_timeout = 5000;
4435
4873
  CREATE TABLE IF NOT EXISTS whanext_message_snapshots (
4436
4874
  session_id TEXT NOT NULL,
@@ -4671,6 +5109,44 @@ function booleanValue(value) {
4671
5109
  if (value === false || value === 0) return false;
4672
5110
  return void 0;
4673
5111
  }
5112
+ function normalizeProviderTimeout(value, fallback) {
5113
+ if (value === void 0 || !Number.isFinite(value)) return fallback;
5114
+ return Math.max(1e3, Math.floor(value));
5115
+ }
5116
+ function detectCryptoBackend() {
5117
+ const requested = process.env.ZAPO_NATIVE_BACKEND?.trim().toLowerCase() ?? "auto";
5118
+ if (requested === "js" || requested === "none") return "js";
5119
+ if ((requested === "auto" || requested === "napi") && nativeNapiAvailable()) {
5120
+ return "napi";
5121
+ }
5122
+ 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")) {
5123
+ return "wasm";
5124
+ }
5125
+ return "js";
5126
+ }
5127
+ function nativeNapiAvailable() {
5128
+ try {
5129
+ require2("@zapo-js/native");
5130
+ return true;
5131
+ } catch {
5132
+ return false;
5133
+ }
5134
+ }
5135
+ function moduleResolvable(specifier) {
5136
+ try {
5137
+ require2.resolve(specifier);
5138
+ return true;
5139
+ } catch {
5140
+ return false;
5141
+ }
5142
+ }
5143
+ function supportsZapoWasmRuntime() {
5144
+ const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
5145
+ if (major > 22) return true;
5146
+ if (major === 22) return minor >= 12;
5147
+ if (major === 20) return minor >= 19;
5148
+ return false;
5149
+ }
4674
5150
  function bytesField(value) {
4675
5151
  if (value instanceof Uint8Array) return value;
4676
5152
  return void 0;
@@ -4679,9 +5155,11 @@ var WhaNextZapoLogger = class _WhaNextZapoLogger {
4679
5155
  level;
4680
5156
  #logger;
4681
5157
  #context;
4682
- constructor(logger, context = {}) {
5158
+ #onWarn;
5159
+ constructor(logger, context = {}, onWarn) {
4683
5160
  this.#logger = logger;
4684
5161
  this.#context = context;
5162
+ this.#onWarn = onWarn;
4685
5163
  this.level = logger.level === "debug" ? "debug" : logger.level === "warn" ? "warn" : logger.level === "error" || logger.level === "silent" ? "error" : "info";
4686
5164
  }
4687
5165
  trace(message, context) {
@@ -4694,7 +5172,9 @@ var WhaNextZapoLogger = class _WhaNextZapoLogger {
4694
5172
  this.#logger.info(message, this.#merge(context));
4695
5173
  }
4696
5174
  warn(message, context) {
4697
- this.#logger.warn(message, this.#merge(context));
5175
+ const merged = this.#merge(context);
5176
+ this.#logger.warn(message, merged);
5177
+ this.#onWarn?.(message, merged);
4698
5178
  }
4699
5179
  error(message, context) {
4700
5180
  this.#logger.error(message, this.#merge(context));
@@ -4703,7 +5183,7 @@ var WhaNextZapoLogger = class _WhaNextZapoLogger {
4703
5183
  return new _WhaNextZapoLogger(this.#logger, {
4704
5184
  ...this.#context,
4705
5185
  ...bindings
4706
- });
5186
+ }, this.#onWarn);
4707
5187
  }
4708
5188
  #merge(context) {
4709
5189
  return context ? { ...this.#context, ...context } : this.#context;
@@ -4731,7 +5211,9 @@ async function create(options = {}) {
4731
5211
  ...options.accountId ? { sessionId: options.accountId } : {},
4732
5212
  ...options.messageCacheSize !== void 0 ? { messageCacheSize: options.messageCacheSize } : {},
4733
5213
  ...options.processOfflineMessages !== void 0 ? { processOfflineMessages: options.processOfflineMessages } : {},
4734
- ...options.reconnect ? { reconnect: options.reconnect } : {}
5214
+ ...options.reconnect ? { reconnect: options.reconnect } : {},
5215
+ ...options.providerTimeouts?.connectTimeoutMs !== void 0 ? { connectTimeoutMs: options.providerTimeouts.connectTimeoutMs } : {},
5216
+ ...options.providerTimeouts?.nodeQueryTimeoutMs !== void 0 ? { nodeQueryTimeoutMs: options.providerTimeouts.nodeQueryTimeoutMs } : {}
4735
5217
  });
4736
5218
  return new WhaNextApp(provider, {
4737
5219
  ...options.accountId ? { accountId: options.accountId } : {},
@@ -4963,6 +5445,12 @@ function mergeCreateOptions(shared, account) {
4963
5445
  ...account.reconnect
4964
5446
  };
4965
5447
  }
5448
+ if (shared.providerTimeouts || account.providerTimeouts) {
5449
+ merged.providerTimeouts = {
5450
+ ...shared.providerTimeouts,
5451
+ ...account.providerTimeouts
5452
+ };
5453
+ }
4966
5454
  if (shared.logger && account.logger && typeof shared.logger === "object" && typeof account.logger === "object") {
4967
5455
  merged.logger = {
4968
5456
  ...shared.logger,