@prototypevip/baileys 0.1.0 → 0.1.2

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.
Files changed (62) hide show
  1. package/lib/Defaults/index.js +3 -3
  2. package/lib/Defaults/index.js.bak-1788476907801 +187 -0
  3. package/lib/Defaults/index.js.bak-1788479166288 +187 -0
  4. package/lib/Socket/groups.js.bak-1788476973954 +475 -0
  5. package/lib/Socket/index.js +42 -126
  6. package/lib/Socket/index.js.bak-1788479281092 +38 -0
  7. package/lib/Socket/messages-send.js +88 -51
  8. package/lib/Store/prototype-store.js +34 -22
  9. package/lib/Store/prototype-store.js.bak-1788477006895 +983 -0
  10. package/lib/Utils/index.js +2 -0
  11. package/lib/Utils/integrity-guard.js +281 -0
  12. package/lib/Utils/interactive-message-handler.js +28 -8
  13. package/lib/Utils/interactive-message-handler.js.bak-1788476898661 +478 -0
  14. package/lib/Utils/socket-guard.js +154 -0
  15. package/package.json +2 -2
  16. package/lib/AntiBan/antiban.js +0 -922
  17. package/lib/AntiBan/banRecoveryOrchestrator.js +0 -284
  18. package/lib/AntiBan/cli.js +0 -177
  19. package/lib/AntiBan/contactGraph.js +0 -236
  20. package/lib/AntiBan/contentVariator.js +0 -150
  21. package/lib/AntiBan/credsSnapshot.js +0 -121
  22. package/lib/AntiBan/deliveryTracker.js +0 -107
  23. package/lib/AntiBan/deviceFingerprint.js +0 -106
  24. package/lib/AntiBan/fleetEventStore.js +0 -157
  25. package/lib/AntiBan/groupOperationGuard.js +0 -116
  26. package/lib/AntiBan/health.js +0 -207
  27. package/lib/AntiBan/humanEntropy.js +0 -337
  28. package/lib/AntiBan/index.js +0 -77
  29. package/lib/AntiBan/instanceCoordinator.js +0 -175
  30. package/lib/AntiBan/jidCanonicalizer.js +0 -256
  31. package/lib/AntiBan/jidCircuitBreaker.js +0 -186
  32. package/lib/AntiBan/legitimacySignalInjector.js +0 -231
  33. package/lib/AntiBan/lidFirstResolver.js +0 -174
  34. package/lib/AntiBan/lidResolver.js +0 -324
  35. package/lib/AntiBan/messageQueue.js +0 -187
  36. package/lib/AntiBan/messageRecovery.js +0 -283
  37. package/lib/AntiBan/messageTypeRegistry.js +0 -369
  38. package/lib/AntiBan/observability.js +0 -145
  39. package/lib/AntiBan/patch.js +0 -302
  40. package/lib/AntiBan/persist.js +0 -87
  41. package/lib/AntiBan/presenceChoreographer.js +0 -430
  42. package/lib/AntiBan/presets.js +0 -105
  43. package/lib/AntiBan/profiles.js +0 -31
  44. package/lib/AntiBan/proxyRotator.js +0 -306
  45. package/lib/AntiBan/rateLimiter.js +0 -261
  46. package/lib/AntiBan/readReceiptVariance.js +0 -88
  47. package/lib/AntiBan/reconnectThrottle.js +0 -180
  48. package/lib/AntiBan/replyRatio.js +0 -161
  49. package/lib/AntiBan/reputationVoucher.js +0 -373
  50. package/lib/AntiBan/retryReason.js +0 -91
  51. package/lib/AntiBan/retryTracker.js +0 -172
  52. package/lib/AntiBan/scheduler.js +0 -111
  53. package/lib/AntiBan/sessionFingerprint.js +0 -248
  54. package/lib/AntiBan/sessionStability.js +0 -330
  55. package/lib/AntiBan/stateAdapter.js +0 -73
  56. package/lib/AntiBan/stateExport.js +0 -152
  57. package/lib/AntiBan/stealthConnect.js +0 -130
  58. package/lib/AntiBan/timelockGuard.js +0 -181
  59. package/lib/AntiBan/topologyThrottler.js +0 -339
  60. package/lib/AntiBan/warmup.js +0 -140
  61. package/lib/AntiBan/webhooks.js +0 -80
  62. package/lib/AntiBan/wrapper.js +0 -456
@@ -1,73 +0,0 @@
1
- /**
2
- * State Adapter — Interface for persisting anti-ban state to disk/DB
3
- *
4
- * Implement this interface to save/load state between restarts.
5
- * Without persistence, warm-up progress is lost on every restart.
6
- *
7
- * Example implementations:
8
- * - File-based: JSON files in a directory
9
- * - Database: SQLite, PostgreSQL, MongoDB
10
- * - Redis: For distributed systems
11
- *
12
- * Usage:
13
- * const adapter = new FileStateAdapter('./state');
14
- * const antiban = new AntiBan({ stateAdapter: adapter });
15
- * await antiban.loadState(); // On startup
16
- * await antiban.saveState(); // Periodically or on shutdown
17
- */
18
- /**
19
- * Example file-based adapter using JSON files
20
- */
21
- export class FileStateAdapter {
22
- basePath;
23
- constructor(basePath) {
24
- this.basePath = basePath;
25
- }
26
- async save(key, state) {
27
- const fs = await import('fs/promises');
28
- const path = await import('path');
29
- const filePath = path.join(this.basePath, `${key}.json`);
30
- await fs.mkdir(this.basePath, { recursive: true });
31
- await fs.writeFile(filePath, JSON.stringify(state, null, 2), 'utf-8');
32
- }
33
- async load(key) {
34
- const fs = await import('fs/promises');
35
- const path = await import('path');
36
- const filePath = path.join(this.basePath, `${key}.json`);
37
- try {
38
- const data = await fs.readFile(filePath, 'utf-8');
39
- return JSON.parse(data);
40
- }
41
- catch (err) {
42
- if (err.code === 'ENOENT')
43
- return null;
44
- throw err;
45
- }
46
- }
47
- async delete(key) {
48
- const fs = await import('fs/promises');
49
- const path = await import('path');
50
- const filePath = path.join(this.basePath, `${key}.json`);
51
- try {
52
- await fs.unlink(filePath);
53
- }
54
- catch (err) {
55
- if (err.code !== 'ENOENT')
56
- throw err;
57
- }
58
- }
59
- async list() {
60
- const fs = await import('fs/promises');
61
- try {
62
- const files = await fs.readdir(this.basePath);
63
- return files
64
- .filter(f => f.endsWith('.json'))
65
- .map(f => f.replace(/\.json$/, ''));
66
- }
67
- catch (err) {
68
- if (err.code === 'ENOENT')
69
- return [];
70
- throw err;
71
- }
72
- }
73
- }
@@ -1,152 +0,0 @@
1
- /**
2
- * Unified State Export — Single-call export/import of ALL antiban state
3
- *
4
- * Enables Redis failover and cross-instance state migration.
5
- * CRDT-safe rate limit counters using increment-only approach.
6
- *
7
- * Features:
8
- * - Single snapshot of all module states
9
- * - CRDT-safe rate limiter state (never overwrites higher counts)
10
- * - Version tracking for migration
11
- * - Instance tracking for debugging
12
- */
13
- /**
14
- * Export antiban state from individual modules.
15
- * Call with references to active module instances.
16
- */
17
- export function exportAntibanState(modules) {
18
- const snapshot = {
19
- version: 1,
20
- exportedAt: Date.now(),
21
- instanceId: modules.instanceId || `instance-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
22
- };
23
- // Export warmup state
24
- if (modules.warmup) {
25
- snapshot.warmup = modules.warmup.exportState();
26
- }
27
- // Export health state
28
- if (modules.health) {
29
- const status = modules.health.getStatus();
30
- snapshot.health = {
31
- riskScore: status.score,
32
- disconnectEvents: [], // Health module doesn't expose raw events, only aggregate stats
33
- };
34
- }
35
- // Export rate limiter state (CRDT-safe)
36
- if (modules.rateLimiter) {
37
- const stats = modules.rateLimiter.getStats();
38
- snapshot.rateLimiter = {
39
- messages: [], // We don't expose raw messages, rely on knownChats + counters
40
- knownChats: Array.from(modules.rateLimiter.getKnownChats()),
41
- identicalCount: {},
42
- burstCount: 0,
43
- lastMessageTime: Date.now(),
44
- currentFactor: modules.rateLimiter.getCurrentFactor(),
45
- sentSinceExport: stats.lastDay, // Use last day count as increment baseline
46
- };
47
- }
48
- // Export circuit breaker states (BUG FIX 2)
49
- if (modules.circuits?.exportState) {
50
- snapshot.circuits = modules.circuits.exportState();
51
- }
52
- // Export timelock guard state
53
- if (modules.timelockGuard) {
54
- const state = modules.timelockGuard.getState();
55
- const knownChats = modules.timelockGuard.getKnownChats ? Array.from(modules.timelockGuard.getKnownChats()) : [];
56
- snapshot.timelockGuard = {
57
- active: state.isActive,
58
- expiresAt: state.expiresAt ? state.expiresAt.getTime() : undefined,
59
- affectedJids: knownChats,
60
- enforcementType: state.enforcementType,
61
- };
62
- }
63
- // Export message registry state
64
- if (modules.messageRegistry) {
65
- snapshot.messageRegistry = modules.messageRegistry.exportState();
66
- }
67
- // Export topology throttler state
68
- if (modules.topologyThrottler) {
69
- snapshot.topologyThrottler = modules.topologyThrottler.exportState();
70
- }
71
- // Export reputation voucher state
72
- if (modules.reputationVoucher) {
73
- snapshot.reputationVoucher = modules.reputationVoucher.exportState();
74
- }
75
- // Export engagement scores
76
- if (modules.engagementScores) {
77
- snapshot.engagementScores = Object.fromEntries(modules.engagementScores.entries());
78
- }
79
- return snapshot;
80
- }
81
- /**
82
- * Import antiban state into modules.
83
- * CRDT-safe for rate limiters (never overwrites higher counts).
84
- */
85
- export function importAntibanState(snapshot, modules) {
86
- // Import warmup state
87
- if (snapshot.warmup && modules.warmup?.importState) {
88
- modules.warmup.importState(snapshot.warmup);
89
- }
90
- // Import health state (reset to clean state, can't import raw events)
91
- if (snapshot.health && modules.health?.reset) {
92
- modules.health.reset();
93
- }
94
- // Import rate limiter state (CRDT-safe)
95
- if (snapshot.rateLimiter && modules.rateLimiter) {
96
- // Restore known chats
97
- if (snapshot.rateLimiter.knownChats) {
98
- modules.rateLimiter.restoreKnownChats(snapshot.rateLimiter.knownChats);
99
- }
100
- // CRDT-safe merge: only restore factor if not already higher
101
- const currentStats = modules.rateLimiter.getStats();
102
- const currentCount = currentStats.lastDay;
103
- const snapshotCount = snapshot.rateLimiter.sentSinceExport;
104
- // If current count is lower, we're likely a fresh instance, so restore factor
105
- if (currentCount < snapshotCount) {
106
- modules.rateLimiter.adaptLimits(snapshot.rateLimiter.currentFactor);
107
- }
108
- // If current count is higher, keep current state (concurrent instance ahead)
109
- }
110
- // Import circuit breaker states (BUG FIX 2)
111
- if (snapshot.circuits && modules.circuits?.importState) {
112
- modules.circuits.importState(snapshot.circuits);
113
- }
114
- // Import timelock guard state
115
- if (snapshot.timelockGuard && modules.timelockGuard) {
116
- if (snapshot.timelockGuard.active && modules.timelockGuard.onTimelockUpdate) {
117
- const expiresAt = snapshot.timelockGuard.expiresAt
118
- ? new Date(snapshot.timelockGuard.expiresAt)
119
- : undefined;
120
- modules.timelockGuard.onTimelockUpdate({
121
- isActive: snapshot.timelockGuard.active,
122
- timeEnforcementEnds: expiresAt,
123
- enforcementType: snapshot.timelockGuard.enforcementType,
124
- });
125
- // Restore known chats
126
- if (snapshot.timelockGuard.affectedJids && modules.timelockGuard.registerKnownChats) {
127
- modules.timelockGuard.registerKnownChats(snapshot.timelockGuard.affectedJids);
128
- }
129
- }
130
- else {
131
- modules.timelockGuard.reset();
132
- }
133
- }
134
- // Import message registry state
135
- if (snapshot.messageRegistry && modules.messageRegistry) {
136
- modules.messageRegistry.importState(snapshot.messageRegistry);
137
- }
138
- // Import topology throttler state
139
- if (snapshot.topologyThrottler && modules.topologyThrottler) {
140
- modules.topologyThrottler.importState(snapshot.topologyThrottler);
141
- }
142
- // Import reputation voucher state
143
- if (snapshot.reputationVoucher && modules.reputationVoucher) {
144
- modules.reputationVoucher.importState(snapshot.reputationVoucher);
145
- }
146
- // Import engagement scores
147
- if (snapshot.engagementScores && modules.engagementScores) {
148
- for (const [jid, score] of Object.entries(snapshot.engagementScores)) {
149
- modules.engagementScores.set(jid, score);
150
- }
151
- }
152
- }
@@ -1,130 +0,0 @@
1
- /**
2
- * Stealth Connect — Reduce ban signal on socket connect + presence ramp
3
- *
4
- * Inspired by GOWA's --presence-on-connect=unavailable flag. Bots that
5
- * snap online immediately and start blasting messages look suspicious to
6
- * WhatsApp's anti-spam classifier. This module ships two helpers:
7
- *
8
- * - `getStealthSocketConfig()` returns a partial Baileys socket config
9
- * that disables `markOnlineOnConnect` and provides a non-default
10
- * browser fingerprint (random pick from a small pool unless overridden).
11
- *
12
- * - `rampPresenceAfterConnect()` waits a randomized delay, then issues
13
- * `sendPresenceUpdate('available')`. Supports `AbortSignal` so the
14
- * caller can cancel if the socket dies before the timer fires.
15
- *
16
- * Usage:
17
- * const config = getStealthSocketConfig({ os: 'MyApp' });
18
- * const sock = makeWASocket({ ...config, auth: state });
19
- *
20
- * const ac = new AbortController();
21
- * sock.ev.on('connection.update', u => {
22
- * if (u.connection === 'close') ac.abort();
23
- * });
24
- * await rampPresenceAfterConnect(sock, { signal: ac.signal });
25
- */
26
- /**
27
- * Pool of realistic browser fingerprints.
28
- *
29
- * Values match formats actually emitted by WhatsApp Web clients in the wild.
30
- * `getStealthSocketConfig()` picks one at random when caller does not supply
31
- * an explicit `browser` or `os` option, so multiple consumers of the library
32
- * do not all advertise an identical fingerprint (which would be trivially
33
- * cluster-able by WhatsApp).
34
- *
35
- * Exported so callers can extend or override the pool if desired.
36
- */
37
- export const STEALTH_BROWSER_POOL = Object.freeze([
38
- ['Mac OS', 'Chrome', '120.0.6099.109'],
39
- ['Mac OS', 'Safari', '17.2.1'],
40
- ['Windows', 'Chrome', '121.0.6167.85'],
41
- ['Windows', 'Firefox', '122.0'],
42
- ['Windows', 'Edge', '120.0.2210.144'],
43
- ['Linux', 'Chrome', '120.0.6099.109'],
44
- ['Linux', 'Firefox', '122.0'],
45
- ['Ubuntu', 'Chrome', '121.0.6167.85'],
46
- ]);
47
- /**
48
- * Returns a partial Baileys socket config tuned for stealth connect.
49
- *
50
- * - `markOnlineOnConnect` set to `false` so the socket joins without
51
- * broadcasting `available` (matches GOWA's `presence-on-connect=unavailable`).
52
- * - `browser` is a randomized realistic tuple from `STEALTH_BROWSER_POOL`
53
- * unless overridden via `opts.browser` or `opts.os`.
54
- *
55
- * Merge the result into your `makeWASocket` options:
56
- *
57
- * const sock = makeWASocket({ ...getStealthSocketConfig(), auth: state });
58
- */
59
- export function getStealthSocketConfig(opts) {
60
- const random = opts?.random ?? Math.random;
61
- let browser;
62
- if (opts?.browser) {
63
- browser = opts.browser;
64
- }
65
- else {
66
- const pick = STEALTH_BROWSER_POOL[Math.floor(random() * STEALTH_BROWSER_POOL.length)];
67
- // pick is guaranteed defined — pool is non-empty and frozen.
68
- const tuple = pick;
69
- if (opts?.os) {
70
- browser = [opts.os, tuple[1], tuple[2]];
71
- }
72
- else {
73
- browser = [tuple[0], tuple[1], tuple[2]];
74
- }
75
- }
76
- return {
77
- markOnlineOnConnect: false,
78
- browser,
79
- };
80
- }
81
- /**
82
- * Custom error thrown when `rampPresenceAfterConnect` is aborted via signal.
83
- * Mirrors DOM `AbortError` semantics so consumers can `instanceof` check.
84
- */
85
- export class AbortError extends Error {
86
- name = 'AbortError';
87
- constructor(message = 'rampPresenceAfterConnect aborted') {
88
- super(message);
89
- }
90
- }
91
- /**
92
- * Waits a randomized delay then calls `sock.sendPresenceUpdate(targetState)`.
93
- *
94
- * Supports `AbortSignal` — abort during the delay window cancels the timer
95
- * and rejects the returned promise with `AbortError`. Aborting after the
96
- * presence update has already been sent is a no-op.
97
- *
98
- * Caller is responsible for invoking abort when the socket disconnects;
99
- * otherwise the post-delay `sendPresenceUpdate` may run against a dead
100
- * socket.
101
- */
102
- export async function rampPresenceAfterConnect(sock, opts) {
103
- const minDelayMs = opts?.minDelayMs ?? 30000;
104
- const maxDelayMs = opts?.maxDelayMs ?? 90000;
105
- const targetState = opts?.targetState ?? 'available';
106
- const random = opts?.random ?? Math.random;
107
- const signal = opts?.signal;
108
- if (signal?.aborted) {
109
- throw new AbortError();
110
- }
111
- const range = Math.max(0, maxDelayMs - minDelayMs);
112
- const delayMs = Math.floor(random() * (range + 1)) + minDelayMs;
113
- await new Promise((resolve, reject) => {
114
- const timer = setTimeout(() => {
115
- if (signal) {
116
- signal.removeEventListener('abort', onAbort);
117
- }
118
- resolve();
119
- }, delayMs);
120
- const onAbort = () => {
121
- clearTimeout(timer);
122
- reject(new AbortError());
123
- };
124
- if (signal) {
125
- signal.addEventListener('abort', onAbort, { once: true });
126
- }
127
- });
128
- // jid undefined => broadcast presence to all conversations
129
- await sock.sendPresenceUpdate(targetState, undefined);
130
- }
@@ -1,181 +0,0 @@
1
- /**
2
- * Timelock Guard — Manages reachout timelock state and routing decisions
3
- *
4
- * When WhatsApp timelocks an account (463 error), this guard:
5
- * - Tracks the timelock state (active, expiry, enforcement type)
6
- * - Blocks messages to NEW contacts (no tctoken / no prior chat)
7
- * - Allows messages to EXISTING contacts (have tctoken / prior chat history)
8
- * - Auto-resumes when the timelock expires
9
- * - Fires callbacks for alerting (Telegram/Discord/webhook)
10
- */
11
- const DEFAULT_CONFIG = {
12
- resumeBufferMs: 10_000,
13
- };
14
- export class TimelockGuard {
15
- config;
16
- state = {
17
- isActive: false,
18
- errorCount: 0,
19
- };
20
- knownChats = new Set();
21
- resumeTimer = null;
22
- timerGeneration = 0; // BUG FIX 4: Track timer validity to prevent race conditions
23
- constructor(config = {}) {
24
- this.config = { ...DEFAULT_CONFIG, ...config };
25
- }
26
- /**
27
- * Update timelock state from Baileys connection.update event
28
- */
29
- onTimelockUpdate(data) {
30
- const wasActive = this.state.isActive;
31
- this.state.isActive = !!data.isActive;
32
- this.state.enforcementType = data.enforcementType;
33
- this.state.expiresAt = data.timeEnforcementEnds;
34
- if (this.state.isActive && !wasActive) {
35
- this.state.detectedAt = new Date();
36
- this.state.errorCount = 0;
37
- this.config.onTimelockDetected?.(this.getState());
38
- this.scheduleResume();
39
- }
40
- else if (this.state.isActive && wasActive) {
41
- // Already locked but expiry updated — reschedule timer with new expiry
42
- this.scheduleResume();
43
- }
44
- if (!this.state.isActive && wasActive) {
45
- this.clearResumeTimer();
46
- this.config.onTimelockLifted?.(this.getState());
47
- }
48
- }
49
- /**
50
- * Record a 463 error from a failed send
51
- */
52
- record463Error() {
53
- this.state.errorCount++;
54
- if (!this.state.isActive) {
55
- // First 463 before MEX query returns — assume locked for 60s
56
- this.state.isActive = true;
57
- this.state.detectedAt = new Date();
58
- this.state.expiresAt = new Date(Date.now() + 60_000);
59
- this.config.onTimelockDetected?.(this.getState());
60
- this.scheduleResume();
61
- }
62
- }
63
- /**
64
- * Register a JID as a known/existing chat (has tctoken / prior history)
65
- */
66
- registerKnownChat(jid) {
67
- this.knownChats.add(jid);
68
- }
69
- /**
70
- * Register multiple known chats at once (e.g. from chat list on connect)
71
- */
72
- registerKnownChats(jids) {
73
- for (const jid of jids) {
74
- this.knownChats.add(jid);
75
- }
76
- }
77
- /**
78
- * Check if a message to this recipient should be allowed
79
- */
80
- canSend(jid) {
81
- if (!this.state.isActive) {
82
- return { allowed: true };
83
- }
84
- // Check if expiry has passed (with buffer)
85
- if (this.state.expiresAt) {
86
- const expiryWithBuffer = this.state.expiresAt.getTime() + this.config.resumeBufferMs;
87
- if (Date.now() >= expiryWithBuffer) {
88
- this.lift();
89
- return { allowed: true };
90
- }
91
- }
92
- // Groups always flow (tctokens not required for group msgs)
93
- if (jid.endsWith('@g.us') || jid.endsWith('@newsletter')) {
94
- return { allowed: true };
95
- }
96
- // Known chats (have tctoken) can proceed
97
- if (this.knownChats.has(jid)) {
98
- return { allowed: true };
99
- }
100
- // New contact while timelocked — block
101
- const expiresIn = this.state.expiresAt
102
- ? Math.max(0, this.state.expiresAt.getTime() - Date.now())
103
- : 60_000;
104
- return {
105
- allowed: false,
106
- reason: `Reachout timelocked (${this.state.enforcementType || 'unknown'}). `
107
- + `New contacts blocked. Expires in ${Math.ceil(expiresIn / 1000)}s.`,
108
- };
109
- }
110
- /**
111
- * Get current timelock state
112
- */
113
- getState() {
114
- return { ...this.state };
115
- }
116
- /**
117
- * Check if currently timelocked
118
- */
119
- isTimelocked() {
120
- if (!this.state.isActive)
121
- return false;
122
- // Auto-lift if expired
123
- if (this.state.expiresAt) {
124
- const expiryWithBuffer = this.state.expiresAt.getTime() + this.config.resumeBufferMs;
125
- if (Date.now() >= expiryWithBuffer) {
126
- this.lift();
127
- return false;
128
- }
129
- }
130
- return true;
131
- }
132
- /**
133
- * Get the set of known chat JIDs
134
- */
135
- getKnownChats() {
136
- return new Set(this.knownChats);
137
- }
138
- /**
139
- * Manually lift the timelock
140
- */
141
- lift() {
142
- if (this.state.isActive) {
143
- this.state.isActive = false;
144
- this.clearResumeTimer();
145
- this.config.onTimelockLifted?.(this.getState());
146
- }
147
- }
148
- /**
149
- * Reset all state
150
- */
151
- reset() {
152
- this.state = { isActive: false, errorCount: 0 };
153
- this.knownChats.clear();
154
- this.clearResumeTimer();
155
- }
156
- scheduleResume() {
157
- this.clearResumeTimer();
158
- if (this.state.expiresAt) {
159
- const delay = this.state.expiresAt.getTime() - Date.now() + this.config.resumeBufferMs;
160
- if (delay > 0) {
161
- // BUG FIX 4: Increment generation to invalidate any pending timers
162
- this.timerGeneration++;
163
- const currentGeneration = this.timerGeneration;
164
- this.resumeTimer = setTimeout(() => {
165
- // Only lift if this timer is still valid (not superseded by a newer scheduleResume call)
166
- if (currentGeneration === this.timerGeneration) {
167
- this.lift();
168
- }
169
- }, delay);
170
- }
171
- }
172
- }
173
- clearResumeTimer() {
174
- if (this.resumeTimer) {
175
- clearTimeout(this.resumeTimer);
176
- this.resumeTimer = null;
177
- // Increment generation to invalidate any pending timer callbacks
178
- this.timerGeneration++;
179
- }
180
- }
181
- }