@whanext/core 0.19.15 → 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";
@@ -2406,6 +2608,7 @@ var Browser = /* @__PURE__ */ ((Browser2) => {
2406
2608
  // src/provider/zapo/zapo-provider.ts
2407
2609
  import { mkdir, stat } from "fs/promises";
2408
2610
  import { createRequire as createRequire2 } from "module";
2611
+ import { Readable } from "stream";
2409
2612
  import { basename, dirname as dirname2, join, resolve as resolve2 } from "path";
2410
2613
  import { createMediaProcessor } from "@zapo-js/media-utils";
2411
2614
  import { createSqliteStore } from "@zapo-js/store-sqlite";
@@ -2930,6 +3133,14 @@ var fatalDisconnectReasons = /* @__PURE__ */ new Set([
2930
3133
  ]);
2931
3134
  var fatalDisconnectCodes = /* @__PURE__ */ new Set([401, 403, 405, 406, 409, 516]);
2932
3135
  var sharedMediaProcessor = createMediaProcessor();
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";
2933
3144
  var messageSnapshotRetentionSeconds = 7 * 24 * 60 * 60;
2934
3145
  var messageSnapshotMaxPerSession = 2e4;
2935
3146
  var messageSnapshotPruneInterval = 256;
@@ -2942,8 +3153,14 @@ var ZapoProvider = class {
2942
3153
  #deliveredMessageStore = /* @__PURE__ */ new Set();
2943
3154
  #handledProtocolStore = /* @__PURE__ */ new Set();
2944
3155
  #callCreatorStore = /* @__PURE__ */ new Map();
3156
+ #groupMetadataRecoveryAt = /* @__PURE__ */ new Map();
3157
+ #groupMetadataRecoveryInFlight = /* @__PURE__ */ new Set();
2945
3158
  #messageCacheSize;
3159
+ #cryptoBackend;
3160
+ #connectTimeoutMs;
3161
+ #nodeQueryTimeoutMs;
2946
3162
  #protocolMutationQueue = Promise.resolve();
3163
+ #state = "idle";
2947
3164
  #client;
2948
3165
  #storeEntry;
2949
3166
  #storePath;
@@ -2958,14 +3175,73 @@ var ZapoProvider = class {
2958
3175
  #messageSnapshotsSincePrune = 0;
2959
3176
  #pairingReady = Promise.resolve();
2960
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;
2961
3195
  constructor(options) {
2962
3196
  this.#options = options;
2963
3197
  this.#logger = options.logger ?? new Logger("silent");
2964
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();
2965
3202
  }
2966
3203
  on(event, listener) {
2967
3204
  return this.#events.on(event, listener);
2968
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
+ }
2969
3245
  async connect() {
2970
3246
  this.#intentionalClose = false;
2971
3247
  this.#closedNotified = false;
@@ -2974,8 +3250,9 @@ var ZapoProvider = class {
2974
3250
  return;
2975
3251
  }
2976
3252
  this.#preparePairingGate();
3253
+ this.#state = this.#reconnectAttempt > 0 ? "reconnecting" : "connecting";
2977
3254
  await this.#events.emit("connection", {
2978
- state: this.#reconnectAttempt > 0 ? "reconnecting" : "connecting",
3255
+ state: this.#state,
2979
3256
  attempt: this.#reconnectAttempt
2980
3257
  });
2981
3258
  this.#startConnect(client);
@@ -2986,6 +3263,10 @@ var ZapoProvider = class {
2986
3263
  clearTimeout(this.#reconnectTimer);
2987
3264
  this.#reconnectTimer = void 0;
2988
3265
  }
3266
+ if (this.#healthRefreshTimer) {
3267
+ clearTimeout(this.#healthRefreshTimer);
3268
+ this.#healthRefreshTimer = void 0;
3269
+ }
2989
3270
  const client = this.#client;
2990
3271
  this.#connectPromise = void 0;
2991
3272
  try {
@@ -2996,6 +3277,8 @@ var ZapoProvider = class {
2996
3277
  }
2997
3278
  } finally {
2998
3279
  this.#connected = false;
3280
+ this.#state = "closed";
3281
+ this.#lastDisconnectedAt = /* @__PURE__ */ new Date();
2999
3282
  await this.#releaseStore();
3000
3283
  }
3001
3284
  }
@@ -3043,50 +3326,56 @@ var ZapoProvider = class {
3043
3326
  }
3044
3327
  }
3045
3328
  async sendMessage(chatId, content, replyTo) {
3046
- if ("buttons" in content) {
3047
- return this.#sendButtons(chatId, content, replyTo);
3048
- }
3049
- if ("list" in content) {
3050
- return this.#sendList(chatId, content, replyTo);
3051
- }
3052
- const client = this.#requireClient();
3053
- const { value, mentions, viewOnce } = await this.#toContent(content);
3054
- const result = await client.message.send(chatId, value, {
3055
- ...replyTo ? { quote: this.#toZapoKey(replyTo) } : {},
3056
- ...mentions.length > 0 ? { mentions } : {},
3057
- ...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);
3058
3344
  });
3059
- return this.#sent(result, chatId);
3060
3345
  }
3061
3346
  async repostMessage(source, chatId, options = {}) {
3062
- const original = await this.#findStoredMessage(this.#toZapoKey(source));
3063
- if (!original?.message) {
3064
- throw new WhaNextError(
3065
- "MESSAGE_NOT_FOUND",
3066
- "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,
3067
3362
  {
3068
- context: { messageId: source.id, chatId: source.chatId },
3069
- recoverable: true
3363
+ forward: true,
3364
+ ...options.mentions && options.mentions.length > 0 ? { mentions: this.#mentions(options.mentions) } : {}
3070
3365
  }
3071
3366
  );
3072
- }
3073
- const result = await this.#requireClient().message.send(
3074
- chatId,
3075
- original.message,
3076
- {
3077
- forward: true,
3078
- ...options.mentions && options.mentions.length > 0 ? { mentions: this.#mentions(options.mentions) } : {}
3079
- }
3080
- );
3081
- return this.#sent(result, chatId);
3367
+ return this.#sent(result, chatId);
3368
+ });
3082
3369
  }
3083
3370
  async reactToMessage(key, emoji) {
3084
- const result = await this.#requireClient().message.send(key.chatId, {
3085
- type: "reaction",
3086
- emoji: emoji ?? "",
3087
- 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);
3088
3378
  });
3089
- return this.#sent(result, key.chatId);
3090
3379
  }
3091
3380
  async downloadMedia(key) {
3092
3381
  const message = this.#messageKeyStore.get(key) ?? await this.#findStoredMessage(this.#toZapoKey(key));
@@ -3122,17 +3411,21 @@ var ZapoProvider = class {
3122
3411
  }
3123
3412
  }
3124
3413
  async editMessage(key, content) {
3125
- const result = await this.#requireClient().message.send(
3126
- key.chatId,
3127
- content,
3128
- { editKey: this.#toZapoKey(key) }
3129
- );
3130
- 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
+ });
3131
3422
  }
3132
3423
  async deleteMessage(key) {
3133
- await this.#requireClient().message.send(key.chatId, {
3134
- type: "revoke",
3135
- 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
+ });
3136
3429
  });
3137
3430
  }
3138
3431
  async getGroup(groupId) {
@@ -3184,13 +3477,15 @@ var ZapoProvider = class {
3184
3477
  return result.code;
3185
3478
  }
3186
3479
  async setMessagePin(groupId, key, pinned) {
3187
- await this.#requireClient().message.send(groupId, pinned ? {
3188
- type: "pin",
3189
- target: this.#toZapoKey(key),
3190
- durationSecs: 604800
3191
- } : {
3192
- type: "unpin",
3193
- 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
+ });
3194
3489
  });
3195
3490
  }
3196
3491
  async updateParticipant(groupId, memberId, action) {
@@ -3231,6 +3526,14 @@ var ZapoProvider = class {
3231
3526
  ]
3232
3527
  });
3233
3528
  }
3529
+ async #trackOutgoing(operation) {
3530
+ try {
3531
+ return await operation();
3532
+ } catch (error) {
3533
+ this.#failedMessages += 1;
3534
+ throw error;
3535
+ }
3536
+ }
3234
3537
  async #ensureClient() {
3235
3538
  if (this.#client) return this.#client;
3236
3539
  const authPath = resolve2(this.#options.auth);
@@ -3254,13 +3557,29 @@ var ZapoProvider = class {
3254
3557
  markOnlineOnConnect: false,
3255
3558
  deviceBrowser: this.#deviceBrowser(),
3256
3559
  deviceOsDisplayName: this.#deviceOsDisplayName(),
3560
+ connectTimeoutMs: this.#connectTimeoutMs,
3561
+ nodeQueryTimeoutMs: this.#nodeQueryTimeoutMs,
3257
3562
  history: { enabled: true, requireFullSync: true },
3258
3563
  addons: {
3259
3564
  autoDecrypt: true,
3260
3565
  persistAllSecrets: true
3261
3566
  },
3262
3567
  media: { processor: sharedMediaProcessor }
3263
- }, 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
+ }
3264
3583
  this.#storeEntry = storeEntry;
3265
3584
  this.#storePath = storePath;
3266
3585
  this.#client = client;
@@ -3311,6 +3630,8 @@ var ZapoProvider = class {
3311
3630
  });
3312
3631
  client.on("message_send", (event) => {
3313
3632
  if (!event.id || !event.message) return;
3633
+ this.#sentMessages += 1;
3634
+ this.#lastOutgoingAt = /* @__PURE__ */ new Date();
3314
3635
  this.#remember({
3315
3636
  key: {
3316
3637
  id: event.id,
@@ -3364,6 +3685,8 @@ var ZapoProvider = class {
3364
3685
  const message = normalizeZapoMessage(event);
3365
3686
  if (!message) return;
3366
3687
  if (stored.key.id) this.#rememberDeliveredMessage(deliveryKey);
3688
+ this.#receivedMessages += 1;
3689
+ this.#lastIncomingAt = /* @__PURE__ */ new Date();
3367
3690
  this.#messageKeyStore.set(message.keys, stored);
3368
3691
  if (message.quoted && quoted?.message) {
3369
3692
  this.#messageKeyStore.set(
@@ -3620,21 +3943,121 @@ var ZapoProvider = class {
3620
3943
  }
3621
3944
  void this.#events.emit("groupChanged", { groupId });
3622
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
+ }
3623
4039
  async #handleConnectionEvent(client, event) {
3624
4040
  if (client !== this.#client) return;
3625
4041
  if (event.status === "open") {
4042
+ const wasReconnect = this.#reconnectAttempt > 0;
3626
4043
  this.#connectPromise = void 0;
3627
4044
  this.#connected = true;
4045
+ this.#state = "connected";
3628
4046
  this.#resolvePairingReady?.();
3629
4047
  this.#resolvePairingReady = void 0;
3630
4048
  this.#connectedAtSeconds = Math.floor(Date.now() / 1e3);
4049
+ this.#lastConnectedAt = /* @__PURE__ */ new Date();
4050
+ if (wasReconnect) this.#reconnects += 1;
3631
4051
  this.#reconnectAttempt = 0;
3632
4052
  this.#closedNotified = false;
4053
+ this.#groupMetadataRecoveryAt.clear();
4054
+ this.#groupMetadataRecoveryInFlight.clear();
3633
4055
  await this.#events.emit("connection", { state: "connected" });
3634
4056
  return;
3635
4057
  }
3636
4058
  this.#connectPromise = void 0;
3637
4059
  this.#connected = false;
4060
+ this.#lastDisconnectedAt = /* @__PURE__ */ new Date();
3638
4061
  const details = disconnectDetails(event.reason, event.code);
3639
4062
  const error = connectionError(event.reason, details);
3640
4063
  if (this.#intentionalClose || event.isLogout === true || shouldStopAutomaticReconnect(details)) {
@@ -3676,6 +4099,8 @@ var ZapoProvider = class {
3676
4099
  }
3677
4100
  this.#connectPromise = void 0;
3678
4101
  this.#connected = false;
4102
+ this.#state = "closed";
4103
+ this.#lastDisconnectedAt = /* @__PURE__ */ new Date();
3679
4104
  await this.#emitClosedOnce(error);
3680
4105
  this.#client = void 0;
3681
4106
  try {
@@ -3691,6 +4116,7 @@ var ZapoProvider = class {
3691
4116
  async #emitClosedOnce(error) {
3692
4117
  if (this.#closedNotified) return;
3693
4118
  this.#closedNotified = true;
4119
+ this.#state = "closed";
3694
4120
  await this.#events.emit("connection", {
3695
4121
  state: "closed",
3696
4122
  ...error ? { error } : {}
@@ -3712,15 +4138,16 @@ var ZapoProvider = class {
3712
4138
  async #scheduleReconnect(error, delayOverrideMs) {
3713
4139
  if (this.#reconnectTimer) return;
3714
4140
  const options = this.#options.reconnect;
3715
- const maxAttempts = options?.maxAttempts ?? 10;
4141
+ const maxAttempts = options?.maxAttempts ?? Number.POSITIVE_INFINITY;
3716
4142
  if (options?.enabled === false || this.#reconnectAttempt >= maxAttempts) {
3717
4143
  await this.#emitClosedOnce(error);
3718
4144
  await this.#releaseStore();
3719
4145
  return;
3720
4146
  }
3721
4147
  this.#reconnectAttempt += 1;
4148
+ this.#state = "reconnecting";
3722
4149
  await this.#events.emit("connection", {
3723
- state: "reconnecting",
4150
+ state: this.#state,
3724
4151
  attempt: this.#reconnectAttempt,
3725
4152
  ...error ? { error } : {}
3726
4153
  });
@@ -3968,8 +4395,20 @@ var ZapoProvider = class {
3968
4395
  async #media(source) {
3969
4396
  if (source instanceof Uint8Array) return source;
3970
4397
  if ("path" in source) return source.path;
3971
- const response = await fetch(source.url);
4398
+ let response;
4399
+ try {
4400
+ response = await fetch(source.url, {
4401
+ signal: AbortSignal.timeout(REMOTE_MEDIA_TIMEOUT_MS)
4402
+ });
4403
+ } catch (error) {
4404
+ throw new WhaNextError(
4405
+ "PROVIDER_ERROR",
4406
+ "Could not open the remote media source.",
4407
+ { cause: error, recoverable: true }
4408
+ );
4409
+ }
3972
4410
  if (!response.ok) {
4411
+ await response.body?.cancel().catch(() => void 0);
3973
4412
  throw new WhaNextError(
3974
4413
  "PROVIDER_ERROR",
3975
4414
  "Could not download the remote media source.",
@@ -3979,7 +4418,16 @@ var ZapoProvider = class {
3979
4418
  }
3980
4419
  );
3981
4420
  }
3982
- return new Uint8Array(await response.arrayBuffer());
4421
+ if (!response.body) {
4422
+ throw new WhaNextError(
4423
+ "PROVIDER_ERROR",
4424
+ "The remote media source returned an empty response body.",
4425
+ { recoverable: true }
4426
+ );
4427
+ }
4428
+ return Readable.fromWeb(
4429
+ response.body
4430
+ );
3983
4431
  }
3984
4432
  #mentions(mentions) {
3985
4433
  return mentions.map((mention) => typeof mention === "string" ? mention : mention.mentionId);
@@ -4385,7 +4833,12 @@ function createZapoStoreEntry(storePath) {
4385
4833
  backends: {
4386
4834
  sqlite: createSqliteStore({
4387
4835
  path: storePath,
4388
- driver: "auto"
4836
+ driver: "auto",
4837
+ pragmas: {
4838
+ journal_mode: "WAL",
4839
+ synchronous: "NORMAL",
4840
+ busy_timeout: 5e3
4841
+ }
4389
4842
  })
4390
4843
  },
4391
4844
  providers: {
@@ -4403,11 +4856,19 @@ function createZapoStoreEntry(storePath) {
4403
4856
  },
4404
4857
  cacheProviders: {
4405
4858
  messageSecret: "sqlite"
4859
+ },
4860
+ memory: {
4861
+ cacheTtlMs: {
4862
+ groupMetadataMs: GROUP_METADATA_CACHE_TTL_MS,
4863
+ deviceListMs: DEVICE_LIST_CACHE_TTL_MS
4864
+ }
4406
4865
  }
4407
4866
  });
4408
4867
  const Database = require2("better-sqlite3");
4409
4868
  const snapshotDb = new Database(messageSnapshotStorePath(storePath));
4410
4869
  snapshotDb.exec(`
4870
+ PRAGMA journal_mode = WAL;
4871
+ PRAGMA synchronous = NORMAL;
4411
4872
  PRAGMA busy_timeout = 5000;
4412
4873
  CREATE TABLE IF NOT EXISTS whanext_message_snapshots (
4413
4874
  session_id TEXT NOT NULL,
@@ -4648,6 +5109,44 @@ function booleanValue(value) {
4648
5109
  if (value === false || value === 0) return false;
4649
5110
  return void 0;
4650
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
+ }
4651
5150
  function bytesField(value) {
4652
5151
  if (value instanceof Uint8Array) return value;
4653
5152
  return void 0;
@@ -4656,9 +5155,11 @@ var WhaNextZapoLogger = class _WhaNextZapoLogger {
4656
5155
  level;
4657
5156
  #logger;
4658
5157
  #context;
4659
- constructor(logger, context = {}) {
5158
+ #onWarn;
5159
+ constructor(logger, context = {}, onWarn) {
4660
5160
  this.#logger = logger;
4661
5161
  this.#context = context;
5162
+ this.#onWarn = onWarn;
4662
5163
  this.level = logger.level === "debug" ? "debug" : logger.level === "warn" ? "warn" : logger.level === "error" || logger.level === "silent" ? "error" : "info";
4663
5164
  }
4664
5165
  trace(message, context) {
@@ -4671,7 +5172,9 @@ var WhaNextZapoLogger = class _WhaNextZapoLogger {
4671
5172
  this.#logger.info(message, this.#merge(context));
4672
5173
  }
4673
5174
  warn(message, context) {
4674
- this.#logger.warn(message, this.#merge(context));
5175
+ const merged = this.#merge(context);
5176
+ this.#logger.warn(message, merged);
5177
+ this.#onWarn?.(message, merged);
4675
5178
  }
4676
5179
  error(message, context) {
4677
5180
  this.#logger.error(message, this.#merge(context));
@@ -4680,7 +5183,7 @@ var WhaNextZapoLogger = class _WhaNextZapoLogger {
4680
5183
  return new _WhaNextZapoLogger(this.#logger, {
4681
5184
  ...this.#context,
4682
5185
  ...bindings
4683
- });
5186
+ }, this.#onWarn);
4684
5187
  }
4685
5188
  #merge(context) {
4686
5189
  return context ? { ...this.#context, ...context } : this.#context;
@@ -4708,7 +5211,9 @@ async function create(options = {}) {
4708
5211
  ...options.accountId ? { sessionId: options.accountId } : {},
4709
5212
  ...options.messageCacheSize !== void 0 ? { messageCacheSize: options.messageCacheSize } : {},
4710
5213
  ...options.processOfflineMessages !== void 0 ? { processOfflineMessages: options.processOfflineMessages } : {},
4711
- ...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 } : {}
4712
5217
  });
4713
5218
  return new WhaNextApp(provider, {
4714
5219
  ...options.accountId ? { accountId: options.accountId } : {},
@@ -4940,6 +5445,12 @@ function mergeCreateOptions(shared, account) {
4940
5445
  ...account.reconnect
4941
5446
  };
4942
5447
  }
5448
+ if (shared.providerTimeouts || account.providerTimeouts) {
5449
+ merged.providerTimeouts = {
5450
+ ...shared.providerTimeouts,
5451
+ ...account.providerTimeouts
5452
+ };
5453
+ }
4943
5454
  if (shared.logger && account.logger && typeof shared.logger === "object" && typeof account.logger === "object") {
4944
5455
  merged.logger = {
4945
5456
  ...shared.logger,