fix: prevent duplicate ready events on SPA re-injection (#201653)
* fix: prevent duplicate ready events on SPA re-injection 1. Add _injectInProgress concurrency guard 2. Replace polling loops with waitForFunction 3. Deduplicate Backbone listeners via tuple array with cleanup 4. Atomic hasSynced check after listener registration 5. isMainFrame guard and storeAvailable SPA skip in framenavigated co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add robustness fixes from fork PR #41 - try/finally around inject() to always reset _injectInProgress - Atomic hasSynced check inside listener registration evaluate() - Fix framenavigated: capture isLogout before async, skip re-inject only when not logout AND store available - try/catch around obj.off() in listener cleanup - Null guard in QR ref change handler - Reset qrRetries on LOADING_SCREEN event - Add exposeFunctionIfAbsent in requestPairingCode() co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: remove QR listener on auth success extract named onRefChange handler so it can be removed via socket.on('change:hasSynced') once authentication succeeds, matching PR #41's cleanup behavior. co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: remove exposeFunctionIfAbsent from requestPairingCode * fix: run refreshQR in browser context via pupPage.evaluate the onAuthAppStateChangedEvent callback is registered with exposefunctionifabsent, so it runs in Node.js context, not the browser. calling window.require() there throws ReferenceError: window is not defined, which means the QR code was never refreshed on UNPAIRED_IDLE state. * fix: remove unnecessary try/catch around Backbone .off() backbone's .off() never throws - verified on a live WhatsApp Web instance with non-existent listeners, fake events, and double-off. the try/catch was defensive but unnecessary. * fix: remove unnecessary try/catch blocks in framenavigated handler - storeAvailable: evaluate() always succeeds after framenavigated because Puppeteer waits for the execution context automatically (verified via CDP event order and Puppeteer source) - inject(): matches upstream behavior - let errors surface instead of silently swallowing them * fix: use AbortController to cancel inject on page navigation when WhatsApp Web does location.reload() during inject (e.g. after viewer context auth), puppeteer's waitForFunction hangs for 30s on a dead execution context. After the timeout, no recovery is possible. fix: - Replace _injectInProgress flag with AbortController pattern: new inject() calls automatically cancel any previous in-progress inject - Pass signal to waitForFunction for immediate cancellation - Move framenavigated handler registration before first inject() in initialize(), so navigation during startup triggers re-inject - Abort inject on destroy() to prevent spurious errors * chore: ignore *.d.ts in eslint upstream added TypeScript declaration changes that ESLint cannot parse. * chore: revert index.d.ts and .eslintignore changes * fix: reset _framenavigatedRegistered flag on destroy * fix: revert unintended setCurrentIndexHtml refactor from lint auto-fix * fix: replace async listener with waitForResponse for index cache the response listener used an async handler on EventEmitter. mitt ignores returned Promises, so when res.text() rejected on CDP disconnect the promise was orphaned - causing unhandledRejection. replace with page.waitForResponse() which returns a proper Promise chain. Use .catch((_) => _) following the existing convention in this codebase (see waitForNavigation on line 386). * Revert "fix: replace async listener with waitForResponse for index cache" This reverts commit 8cfd915ee5331d8b828c74ca8306dc72517bb0f1. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4df15e58b9
commit
1780711a1c
1 changed files with 347 additions and 288 deletions
657
src/Client.js
657
src/Client.js
|
|
@ -112,320 +112,355 @@ class Client extends EventEmitter {
|
||||||
* Private function
|
* Private function
|
||||||
*/
|
*/
|
||||||
async inject() {
|
async inject() {
|
||||||
if (
|
// Cancel any previous inject still running
|
||||||
this.options.authTimeoutMs === undefined ||
|
if (this._injectAbort) this._injectAbort.abort();
|
||||||
this.options.authTimeoutMs == 0
|
const abort = new AbortController();
|
||||||
) {
|
this._injectAbort = abort;
|
||||||
this.options.authTimeoutMs = 30000;
|
|
||||||
}
|
try {
|
||||||
let start = Date.now();
|
const authTimeout = this.options.authTimeoutMs || 30000;
|
||||||
let timeout = this.options.authTimeoutMs;
|
await this.pupPage
|
||||||
let res = false;
|
.waitForFunction('window.Debug?.VERSION != undefined', {
|
||||||
while (start > Date.now() - timeout) {
|
timeout: authTimeout,
|
||||||
res = await this.pupPage.evaluate(
|
signal: abort.signal,
|
||||||
'window.Debug?.VERSION != undefined',
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (abort.signal.aborted) throw err;
|
||||||
|
throw 'auth timeout';
|
||||||
|
});
|
||||||
|
if (abort.signal.aborted) return;
|
||||||
|
await this.setDeviceName(
|
||||||
|
this.options.deviceName,
|
||||||
|
this.options.browserName,
|
||||||
);
|
);
|
||||||
if (res) {
|
const pairWithPhoneNumber = this.options.pairWithPhoneNumber;
|
||||||
break;
|
const version = await this.getWWebVersion();
|
||||||
}
|
|
||||||
await new Promise((r) => setTimeout(r, 200));
|
|
||||||
}
|
|
||||||
if (!res) {
|
|
||||||
throw 'auth timeout';
|
|
||||||
}
|
|
||||||
await this.setDeviceName(
|
|
||||||
this.options.deviceName,
|
|
||||||
this.options.browserName,
|
|
||||||
);
|
|
||||||
const pairWithPhoneNumber = this.options.pairWithPhoneNumber;
|
|
||||||
const version = await this.getWWebVersion();
|
|
||||||
|
|
||||||
const needAuthentication = await this.pupPage.evaluate(async () => {
|
const needAuthHandle = await this.pupPage.waitForFunction(
|
||||||
let state = window.require('WAWebSocketModel').Socket.state;
|
() => {
|
||||||
|
const state =
|
||||||
if (
|
window.require?.('WAWebSocketModel')?.Socket?.state;
|
||||||
state === 'OPENING' ||
|
|
||||||
state === 'UNLAUNCHED' ||
|
|
||||||
state === 'PAIRING'
|
|
||||||
) {
|
|
||||||
// wait till state changes
|
|
||||||
await new Promise((r) => {
|
|
||||||
window
|
|
||||||
.require('WAWebSocketModel')
|
|
||||||
.Socket.on(
|
|
||||||
'change:state',
|
|
||||||
function waitTillInit(_AppState, state) {
|
|
||||||
if (
|
|
||||||
state !== 'OPENING' &&
|
|
||||||
state !== 'UNLAUNCHED' &&
|
|
||||||
state !== 'PAIRING'
|
|
||||||
) {
|
|
||||||
window
|
|
||||||
.require('WAWebSocketModel')
|
|
||||||
.Socket.off(
|
|
||||||
'change:state',
|
|
||||||
waitTillInit,
|
|
||||||
);
|
|
||||||
r();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
state = window.require('WAWebSocketModel').Socket.state;
|
|
||||||
return state == 'UNPAIRED' || state == 'UNPAIRED_IDLE';
|
|
||||||
});
|
|
||||||
|
|
||||||
if (needAuthentication) {
|
|
||||||
const { failed, failureEventPayload, restart } =
|
|
||||||
await this.authStrategy.onAuthenticationNeeded();
|
|
||||||
|
|
||||||
if (failed) {
|
|
||||||
/**
|
|
||||||
* Emitted when there has been an error while trying to restore an existing session
|
|
||||||
* @event Client#auth_failure
|
|
||||||
* @param {string} message
|
|
||||||
*/
|
|
||||||
this.emit(Events.AUTHENTICATION_FAILURE, failureEventPayload);
|
|
||||||
await this.destroy();
|
|
||||||
if (restart) {
|
|
||||||
// session restore failed so try again but without session to force new authentication
|
|
||||||
return this.initialize();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register qr/code events
|
|
||||||
if (pairWithPhoneNumber.phoneNumber) {
|
|
||||||
await exposeFunctionIfAbsent(
|
|
||||||
this.pupPage,
|
|
||||||
'onCodeReceivedEvent',
|
|
||||||
async (code) => {
|
|
||||||
/**
|
|
||||||
* Emitted when a pairing code is received
|
|
||||||
* @event Client#code
|
|
||||||
* @param {string} code Code
|
|
||||||
* @returns {string} Code that was just received
|
|
||||||
*/
|
|
||||||
this.emit(Events.CODE_RECEIVED, code);
|
|
||||||
return code;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
this.requestPairingCode(
|
|
||||||
pairWithPhoneNumber.phoneNumber,
|
|
||||||
pairWithPhoneNumber.showNotification,
|
|
||||||
pairWithPhoneNumber.intervalMs,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let qrRetries = 0;
|
|
||||||
await exposeFunctionIfAbsent(
|
|
||||||
this.pupPage,
|
|
||||||
'onQRChangedEvent',
|
|
||||||
async (qr) => {
|
|
||||||
/**
|
|
||||||
* Emitted when a QR code is received
|
|
||||||
* @event Client#qr
|
|
||||||
* @param {string} qr QR Code
|
|
||||||
*/
|
|
||||||
this.emit(Events.QR_RECEIVED, qr);
|
|
||||||
if (this.options.qrMaxRetries > 0) {
|
|
||||||
qrRetries++;
|
|
||||||
if (qrRetries > this.options.qrMaxRetries) {
|
|
||||||
this.emit(
|
|
||||||
Events.DISCONNECTED,
|
|
||||||
'Max qrcode retries reached',
|
|
||||||
);
|
|
||||||
await this.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.pupPage.evaluate(async () => {
|
|
||||||
const registrationInfo = await window
|
|
||||||
.require('WAWebSignalStoreApi')
|
|
||||||
.waSignalStore.getRegistrationInfo();
|
|
||||||
const noiseKeyPair = await window
|
|
||||||
.require('WAWebUserPrefsInfoStore')
|
|
||||||
.waNoiseInfo.get();
|
|
||||||
const staticKeyB64 = window
|
|
||||||
.require('WABase64')
|
|
||||||
.encodeB64(noiseKeyPair.staticKeyPair.pubKey);
|
|
||||||
const identityKeyB64 = window
|
|
||||||
.require('WABase64')
|
|
||||||
.encodeB64(registrationInfo.identityKeyPair.pubKey);
|
|
||||||
const platform = window.require(
|
|
||||||
'WAWebCompanionRegClientUtils',
|
|
||||||
).DEVICE_PLATFORM;
|
|
||||||
const getQR = (ref) =>
|
|
||||||
ref +
|
|
||||||
',' +
|
|
||||||
staticKeyB64 +
|
|
||||||
',' +
|
|
||||||
identityKeyB64 +
|
|
||||||
',' +
|
|
||||||
window
|
|
||||||
.require('WAWebUserPrefsMultiDevice')
|
|
||||||
.getADVSecretKey() +
|
|
||||||
',' +
|
|
||||||
platform;
|
|
||||||
window.onQRChangedEvent(
|
|
||||||
getQR(window.require('WAWebConnModel').Conn.ref),
|
|
||||||
); // initial qr
|
|
||||||
window
|
|
||||||
.require('WAWebConnModel')
|
|
||||||
.Conn.on('change:ref', (_, ref) => {
|
|
||||||
window.onQRChangedEvent(getQR(ref));
|
|
||||||
}); // future QR changes
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await exposeFunctionIfAbsent(
|
|
||||||
this.pupPage,
|
|
||||||
'onAuthAppStateChangedEvent',
|
|
||||||
async (state) => {
|
|
||||||
if (
|
|
||||||
state == 'UNPAIRED_IDLE' &&
|
|
||||||
!pairWithPhoneNumber.phoneNumber
|
|
||||||
) {
|
|
||||||
// refresh qr code
|
|
||||||
window.require('WAWebCmd').Cmd.refreshQR();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await exposeFunctionIfAbsent(
|
|
||||||
this.pupPage,
|
|
||||||
'onAppStateHasSyncedEvent',
|
|
||||||
async () => {
|
|
||||||
const authEventPayload =
|
|
||||||
await this.authStrategy.getAuthEventPayload();
|
|
||||||
/**
|
|
||||||
* Emitted when authentication is successful
|
|
||||||
* @event Client#authenticated
|
|
||||||
*/
|
|
||||||
this.emit(Events.AUTHENTICATED, authEventPayload);
|
|
||||||
|
|
||||||
const injected = await this.pupPage.evaluate(async () => {
|
|
||||||
return typeof window.WWebJS !== 'undefined';
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!injected) {
|
|
||||||
if (
|
if (
|
||||||
this.options.webVersionCache.type === 'local' &&
|
!state ||
|
||||||
this.currentIndexHtml
|
state === 'OPENING' ||
|
||||||
|
state === 'UNLAUNCHED' ||
|
||||||
|
state === 'PAIRING'
|
||||||
) {
|
) {
|
||||||
const { type: webCacheType, ...webCacheOptions } =
|
return false;
|
||||||
this.options.webVersionCache;
|
|
||||||
const webCache = WebCacheFactory.createWebCache(
|
|
||||||
webCacheType,
|
|
||||||
webCacheOptions,
|
|
||||||
);
|
|
||||||
|
|
||||||
await webCache.persist(this.currentIndexHtml, version);
|
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
need: state === 'UNPAIRED' || state === 'UNPAIRED_IDLE',
|
||||||
|
state,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ timeout: authTimeout },
|
||||||
|
);
|
||||||
|
const needAuthentication = await needAuthHandle.jsonValue();
|
||||||
|
|
||||||
// Load util functions (serializers, helper functions)
|
if (needAuthentication.need) {
|
||||||
await this.pupPage.evaluate(LoadUtils);
|
const { failed, failureEventPayload, restart } =
|
||||||
|
await this.authStrategy.onAuthenticationNeeded();
|
||||||
let start = Date.now();
|
|
||||||
let res = false;
|
|
||||||
while (start > Date.now() - 30000) {
|
|
||||||
// Check window.WWebJS Injection
|
|
||||||
res = await this.pupPage.evaluate(
|
|
||||||
'window.WWebJS != undefined',
|
|
||||||
);
|
|
||||||
if (res) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await new Promise((r) => setTimeout(r, 200));
|
|
||||||
}
|
|
||||||
if (!res) {
|
|
||||||
throw 'ready timeout';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (failed) {
|
||||||
/**
|
/**
|
||||||
* Current connection information
|
* Emitted when there has been an error while trying to restore an existing session
|
||||||
* @type {ClientInfo}
|
* @event Client#auth_failure
|
||||||
|
* @param {string} message
|
||||||
*/
|
*/
|
||||||
this.info = new ClientInfo(
|
this.emit(
|
||||||
this,
|
Events.AUTHENTICATION_FAILURE,
|
||||||
await this.pupPage.evaluate(() => {
|
failureEventPayload,
|
||||||
return {
|
);
|
||||||
...window
|
await this.destroy();
|
||||||
.require('WAWebConnModel')
|
if (restart) {
|
||||||
.Conn.serialize(),
|
// session restore failed so try again but without session to force new authentication
|
||||||
wid:
|
return this.initialize();
|
||||||
window
|
}
|
||||||
.require('WAWebUserPrefsMeUser')
|
return;
|
||||||
.getMaybeMePnUser() ||
|
}
|
||||||
window
|
|
||||||
.require('WAWebUserPrefsMeUser')
|
// Register qr/code events
|
||||||
.getMaybeMeLidUser(),
|
if (pairWithPhoneNumber.phoneNumber) {
|
||||||
};
|
this.requestPairingCode(
|
||||||
}),
|
pairWithPhoneNumber.phoneNumber,
|
||||||
|
pairWithPhoneNumber.showNotification,
|
||||||
|
pairWithPhoneNumber.intervalMs,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let qrRetries = 0;
|
||||||
|
|
||||||
|
this.on(Events.LOADING_SCREEN, () => {
|
||||||
|
qrRetries = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
await exposeFunctionIfAbsent(
|
||||||
|
this.pupPage,
|
||||||
|
'onQRChangedEvent',
|
||||||
|
async (qr) => {
|
||||||
|
/**
|
||||||
|
* Emitted when a QR code is received
|
||||||
|
* @event Client#qr
|
||||||
|
* @param {string} qr QR Code
|
||||||
|
*/
|
||||||
|
this.emit(Events.QR_RECEIVED, qr);
|
||||||
|
if (this.options.qrMaxRetries > 0) {
|
||||||
|
qrRetries++;
|
||||||
|
if (qrRetries > this.options.qrMaxRetries) {
|
||||||
|
this.emit(
|
||||||
|
Events.DISCONNECTED,
|
||||||
|
'Max qrcode retries reached',
|
||||||
|
);
|
||||||
|
await this.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
this.interface = new InterfaceController(this);
|
await this.pupPage.evaluate(async () => {
|
||||||
|
const registrationInfo = await window
|
||||||
|
.require('WAWebSignalStoreApi')
|
||||||
|
.waSignalStore.getRegistrationInfo();
|
||||||
|
const noiseKeyPair = await window
|
||||||
|
.require('WAWebUserPrefsInfoStore')
|
||||||
|
.waNoiseInfo.get();
|
||||||
|
const staticKeyB64 = window
|
||||||
|
.require('WABase64')
|
||||||
|
.encodeB64(noiseKeyPair.staticKeyPair.pubKey);
|
||||||
|
const identityKeyB64 = window
|
||||||
|
.require('WABase64')
|
||||||
|
.encodeB64(registrationInfo.identityKeyPair.pubKey);
|
||||||
|
const advSecretKey = await window
|
||||||
|
.require('WAWebUserPrefsMultiDevice')
|
||||||
|
.getADVSecretKey();
|
||||||
|
const platform = window.require(
|
||||||
|
'WAWebCompanionRegClientUtils',
|
||||||
|
).DEVICE_PLATFORM;
|
||||||
|
const getQR = (ref) =>
|
||||||
|
ref +
|
||||||
|
',' +
|
||||||
|
staticKeyB64 +
|
||||||
|
',' +
|
||||||
|
identityKeyB64 +
|
||||||
|
',' +
|
||||||
|
advSecretKey +
|
||||||
|
',' +
|
||||||
|
platform;
|
||||||
|
|
||||||
await this.attachEventListeners();
|
const onRefChange = (_, ref) => {
|
||||||
|
if (ref == null) return;
|
||||||
|
window.onQRChangedEvent(getQR(ref));
|
||||||
|
};
|
||||||
|
|
||||||
|
window.onQRChangedEvent(
|
||||||
|
getQR(window.require('WAWebConnModel').Conn.ref),
|
||||||
|
); // initial qr
|
||||||
|
window
|
||||||
|
.require('WAWebConnModel')
|
||||||
|
.Conn.on('change:ref', onRefChange); // future QR changes
|
||||||
|
|
||||||
|
// Remove QR listener once authentication succeeds
|
||||||
|
window
|
||||||
|
.require('WAWebSocketModel')
|
||||||
|
.Socket.on('change:hasSynced', () => {
|
||||||
|
window
|
||||||
|
.require('WAWebConnModel')
|
||||||
|
.Conn.off('change:ref', onRefChange);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
/**
|
}
|
||||||
* Emitted when the client has initialized and is ready to receive messages.
|
|
||||||
* @event Client#ready
|
await exposeFunctionIfAbsent(
|
||||||
*/
|
this.pupPage,
|
||||||
this.emit(Events.READY);
|
'onAuthAppStateChangedEvent',
|
||||||
this.authStrategy.afterAuthReady();
|
async (state) => {
|
||||||
},
|
if (
|
||||||
);
|
state == 'UNPAIRED_IDLE' &&
|
||||||
let lastPercent = null;
|
!pairWithPhoneNumber.phoneNumber
|
||||||
await exposeFunctionIfAbsent(
|
) {
|
||||||
this.pupPage,
|
// refresh qr code
|
||||||
'onOfflineProgressUpdateEvent',
|
await this.pupPage.evaluate(() => {
|
||||||
async (percent) => {
|
window.require('WAWebCmd').Cmd.refreshQR();
|
||||||
if (lastPercent !== percent) {
|
});
|
||||||
lastPercent = percent;
|
}
|
||||||
this.emit(Events.LOADING_SCREEN, percent, 'WhatsApp'); // Message is hardcoded as "WhatsApp" for now
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await exposeFunctionIfAbsent(
|
||||||
|
this.pupPage,
|
||||||
|
'onAppStateHasSyncedEvent',
|
||||||
|
async () => {
|
||||||
|
const authEventPayload =
|
||||||
|
await this.authStrategy.getAuthEventPayload();
|
||||||
|
/**
|
||||||
|
* Emitted when authentication is successful
|
||||||
|
* @event Client#authenticated
|
||||||
|
*/
|
||||||
|
this.emit(Events.AUTHENTICATED, authEventPayload);
|
||||||
|
|
||||||
|
const injected = await this.pupPage.evaluate(async () => {
|
||||||
|
return typeof window.WWebJS !== 'undefined';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!injected) {
|
||||||
|
if (
|
||||||
|
this.options.webVersionCache.type === 'local' &&
|
||||||
|
this.currentIndexHtml
|
||||||
|
) {
|
||||||
|
const { type: webCacheType, ...webCacheOptions } =
|
||||||
|
this.options.webVersionCache;
|
||||||
|
const webCache = WebCacheFactory.createWebCache(
|
||||||
|
webCacheType,
|
||||||
|
webCacheOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
await webCache.persist(
|
||||||
|
this.currentIndexHtml,
|
||||||
|
version,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load util functions (serializers, helper functions)
|
||||||
|
await this.pupPage.evaluate(LoadUtils);
|
||||||
|
|
||||||
|
await this.pupPage
|
||||||
|
.waitForFunction(
|
||||||
|
'typeof window.WWebJS !== "undefined"',
|
||||||
|
{ timeout: 30000 },
|
||||||
|
)
|
||||||
|
.catch(() => {
|
||||||
|
throw 'ready timeout';
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current connection information
|
||||||
|
* @type {ClientInfo}
|
||||||
|
*/
|
||||||
|
this.info = new ClientInfo(
|
||||||
|
this,
|
||||||
|
await this.pupPage.evaluate(() => {
|
||||||
|
return {
|
||||||
|
...window
|
||||||
|
.require('WAWebConnModel')
|
||||||
|
.Conn.serialize(),
|
||||||
|
wid:
|
||||||
|
window
|
||||||
|
.require('WAWebUserPrefsMeUser')
|
||||||
|
.getMaybeMePnUser() ||
|
||||||
|
window
|
||||||
|
.require('WAWebUserPrefsMeUser')
|
||||||
|
.getMaybeMeLidUser(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.interface = new InterfaceController(this);
|
||||||
|
|
||||||
|
await this.attachEventListeners();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Emitted when the client has initialized and is ready to receive messages.
|
||||||
|
* @event Client#ready
|
||||||
|
*/
|
||||||
|
this.emit(Events.READY);
|
||||||
|
this.authStrategy.afterAuthReady();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let lastPercent = null;
|
||||||
|
await exposeFunctionIfAbsent(
|
||||||
|
this.pupPage,
|
||||||
|
'onOfflineProgressUpdateEvent',
|
||||||
|
async (percent) => {
|
||||||
|
if (lastPercent !== percent) {
|
||||||
|
lastPercent = percent;
|
||||||
|
this.emit(Events.LOADING_SCREEN, percent, 'WhatsApp'); // Message is hardcoded as "WhatsApp" for now
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await exposeFunctionIfAbsent(
|
||||||
|
this.pupPage,
|
||||||
|
'onLogoutEvent',
|
||||||
|
async () => {
|
||||||
|
this.lastLoggedOut = true;
|
||||||
|
await this.pupPage
|
||||||
|
.waitForNavigation({ waitUntil: 'load', timeout: 5000 })
|
||||||
|
.catch((_) => _);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await this.pupPage.evaluate(() => {
|
||||||
|
const Socket = window.require('WAWebSocketModel').Socket;
|
||||||
|
const Cmd = window.require('WAWebCmd').Cmd;
|
||||||
|
|
||||||
|
const listeners = [
|
||||||
|
[
|
||||||
|
Socket,
|
||||||
|
'change:state',
|
||||||
|
(_AppState, state) => {
|
||||||
|
window.onAuthAppStateChangedEvent(state);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Socket,
|
||||||
|
'change:hasSynced',
|
||||||
|
() => {
|
||||||
|
window.onAppStateHasSyncedEvent();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Cmd,
|
||||||
|
'offline_progress_update_from_bridge',
|
||||||
|
() => {
|
||||||
|
window.onOfflineProgressUpdateEvent(
|
||||||
|
window
|
||||||
|
.require('WAWebOfflineHandler')
|
||||||
|
.OfflineMessageHandler.getOfflineDeliveryProgress(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Cmd,
|
||||||
|
'logout',
|
||||||
|
async () => {
|
||||||
|
await window.onLogoutEvent();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Cmd,
|
||||||
|
'logout_from_bridge',
|
||||||
|
async () => {
|
||||||
|
await window.onLogoutEvent();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Clean up old listeners to prevent accumulation on re-inject
|
||||||
|
if (window._wwjsListeners) {
|
||||||
|
for (const [obj, event, handler] of window._wwjsListeners) {
|
||||||
|
obj.off(event, handler);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
);
|
for (const [obj, event, handler] of listeners) {
|
||||||
await exposeFunctionIfAbsent(
|
obj.on(event, handler);
|
||||||
this.pupPage,
|
}
|
||||||
'onLogoutEvent',
|
window._wwjsListeners = listeners;
|
||||||
async () => {
|
|
||||||
this.lastLoggedOut = true;
|
// Atomic hasSynced check in the same synchronous block as listener registration.
|
||||||
await this.pupPage
|
// If hasSynced is already true, Backbone won't fire change:hasSynced (no transition).
|
||||||
.waitForNavigation({ waitUntil: 'load', timeout: 5000 })
|
// If hasSynced is false, the listener above will catch the future transition.
|
||||||
.catch((_) => _);
|
const storeInjected = typeof window.WWebJS !== 'undefined';
|
||||||
},
|
if (Socket.hasSynced === true && !storeInjected) {
|
||||||
);
|
|
||||||
await this.pupPage.evaluate(() => {
|
|
||||||
window
|
|
||||||
.require('WAWebSocketModel')
|
|
||||||
.Socket.on('change:state', (_AppState, state) => {
|
|
||||||
window.onAuthAppStateChangedEvent(state);
|
|
||||||
});
|
|
||||||
window
|
|
||||||
.require('WAWebSocketModel')
|
|
||||||
.Socket.on('change:hasSynced', () => {
|
|
||||||
window.onAppStateHasSyncedEvent();
|
window.onAppStateHasSyncedEvent();
|
||||||
});
|
}
|
||||||
const Cmd = window.require('WAWebCmd').Cmd;
|
|
||||||
Cmd.on('offline_progress_update_from_bridge', () => {
|
|
||||||
window.onOfflineProgressUpdateEvent(
|
|
||||||
window
|
|
||||||
.require('WAWebOfflineHandler')
|
|
||||||
.OfflineMessageHandler.getOfflineDeliveryProgress(),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
Cmd.on('logout', async () => {
|
} catch (err) {
|
||||||
await window.onLogoutEvent();
|
if (abort.signal.aborted) return; // superseded by newer inject
|
||||||
});
|
throw err;
|
||||||
Cmd.on('logout_from_bridge', async () => {
|
} finally {
|
||||||
await window.onLogoutEvent();
|
if (this._injectAbort === abort) {
|
||||||
});
|
this._injectAbort = null;
|
||||||
});
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -495,16 +530,37 @@ class Client extends EventEmitter {
|
||||||
referer: 'https://whatsapp.com/',
|
referer: 'https://whatsapp.com/',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Register framenavigated BEFORE inject so that if navigation
|
||||||
|
// interrupts inject, the handler triggers a fresh inject.
|
||||||
|
this._registerFramenavigatedHandler();
|
||||||
|
|
||||||
await this.inject();
|
await this.inject();
|
||||||
|
}
|
||||||
|
|
||||||
|
_registerFramenavigatedHandler() {
|
||||||
|
if (this._framenavigatedRegistered) return;
|
||||||
|
this._framenavigatedRegistered = true;
|
||||||
|
|
||||||
this.pupPage.on('framenavigated', async (frame) => {
|
this.pupPage.on('framenavigated', async (frame) => {
|
||||||
if (frame.url().includes('post_logout=1') || this.lastLoggedOut) {
|
if (frame.parentFrame() !== null) return;
|
||||||
|
|
||||||
|
const isLogout =
|
||||||
|
frame.url().includes('post_logout=1') || this.lastLoggedOut;
|
||||||
|
|
||||||
|
if (isLogout) {
|
||||||
this.emit(Events.DISCONNECTED, 'LOGOUT');
|
this.emit(Events.DISCONNECTED, 'LOGOUT');
|
||||||
await this.authStrategy.logout();
|
await this.authStrategy.logout();
|
||||||
await this.authStrategy.beforeBrowserInitialized();
|
await this.authStrategy.beforeBrowserInitialized();
|
||||||
await this.authStrategy.afterBrowserInitialized();
|
await this.authStrategy.afterBrowserInitialized();
|
||||||
this.lastLoggedOut = false;
|
this.lastLoggedOut = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const storeAvailable = await this.pupPage.evaluate(() => {
|
||||||
|
return typeof window.WWebJS !== 'undefined';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isLogout && storeAvailable) return;
|
||||||
|
|
||||||
await this.inject();
|
await this.inject();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1276,6 +1332,9 @@ class Client extends EventEmitter {
|
||||||
* Closes the client
|
* Closes the client
|
||||||
*/
|
*/
|
||||||
async destroy() {
|
async destroy() {
|
||||||
|
if (this._injectAbort) this._injectAbort.abort();
|
||||||
|
this._framenavigatedRegistered = false;
|
||||||
|
|
||||||
const browser = this.pupBrowser;
|
const browser = this.pupBrowser;
|
||||||
const isConnected = browser?.isConnected?.();
|
const isConnected = browser?.isConnected?.();
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue