@oxidezap/baileyrs 0.2.12 → 0.2.13
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/lib/Compatibility/encode-proto.d.ts +7 -3
- package/lib/Compatibility/encode-proto.js +10 -5
- package/lib/Compatibility/proto-runtime.js +54 -7
- package/lib/Socket/events.js +1 -36
- package/lib/Socket/groups.js +35 -2
- package/lib/Socket/index.js +95 -17
- package/lib/Socket/terminal-close.d.ts +2 -36
- package/lib/Socket/terminal-close.js +29 -5
- package/package.json +1 -1
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `encodeProto`, with the
|
|
2
|
+
* `encodeProto`, with the three inputs the bridge codec refuses put back.
|
|
3
3
|
*
|
|
4
4
|
* From 0.8.0 the codec refuses an empty string where the schema declares a
|
|
5
5
|
* 64-bit integer, and an unpaired surrogate in a text field. Both were written
|
|
6
6
|
* before — as `0` and as U+FFFD — and upstream Baileys still encodes both, so a
|
|
7
7
|
* message that used to reach the server would now throw in the caller's face.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* The codec also refuses enum names where the schema declares an enum
|
|
9
|
+
* (issue #109: `"NONE"` where an int32 goes on the wire), which upstream's
|
|
10
|
+
* `fromObject` resolves and its direct `encode` coerces — that repair lives in
|
|
11
|
+
* `repairProtoMessage`, next to the other two. This is where all three are
|
|
12
|
+
* absorbed, so the strict contract stays true of the bridge and the tolerant
|
|
13
|
+
* one stays true of this library.
|
|
10
14
|
*
|
|
11
15
|
* Repair on failure rather than check on write: the ordinary encode is exactly
|
|
12
16
|
* the call it was before, with no scan of any field, and the repair runs only
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { encodeProto } from '@oxidezap/whatsapp-rust-bridge';
|
|
2
2
|
import { repairProtoMessage } from './proto-runtime.js';
|
|
3
3
|
/**
|
|
4
|
-
* `encodeProto`, with the
|
|
4
|
+
* `encodeProto`, with the three inputs the bridge codec refuses put back.
|
|
5
5
|
*
|
|
6
6
|
* From 0.8.0 the codec refuses an empty string where the schema declares a
|
|
7
7
|
* 64-bit integer, and an unpaired surrogate in a text field. Both were written
|
|
8
8
|
* before — as `0` and as U+FFFD — and upstream Baileys still encodes both, so a
|
|
9
9
|
* message that used to reach the server would now throw in the caller's face.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* The codec also refuses enum names where the schema declares an enum
|
|
11
|
+
* (issue #109: `"NONE"` where an int32 goes on the wire), which upstream's
|
|
12
|
+
* `fromObject` resolves and its direct `encode` coerces — that repair lives in
|
|
13
|
+
* `repairProtoMessage`, next to the other two. This is where all three are
|
|
14
|
+
* absorbed, so the strict contract stays true of the bridge and the tolerant
|
|
15
|
+
* one stays true of this library.
|
|
12
16
|
*
|
|
13
17
|
* Repair on failure rather than check on write: the ordinary encode is exactly
|
|
14
18
|
* the call it was before, with no scan of any field, and the repair runs only
|
|
@@ -22,8 +26,9 @@ export const encodeProtoCompat = (path, message) => {
|
|
|
22
26
|
catch (error) {
|
|
23
27
|
const repaired = repairProtoMessage(path, message);
|
|
24
28
|
// Reference equality: nothing was coerced, so the failure is something this
|
|
25
|
-
// does not explain — an unmodelled type, a number no int64 can hold
|
|
26
|
-
// it has to keep propagating rather than be
|
|
29
|
+
// does not explain — an unmodelled type, a number no int64 can hold, an
|
|
30
|
+
// unknown enum name — and it has to keep propagating rather than be
|
|
31
|
+
// retried into a second throw.
|
|
27
32
|
if (repaired === message)
|
|
28
33
|
throw error;
|
|
29
34
|
return encodeProto(path, repaired);
|
|
@@ -53,7 +53,7 @@ const UNPAIRED_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<|(?<![\uD800-\uDBF
|
|
|
62
62
|
* - An unpaired surrogate in a text field, replaced with U+FFFD. That is the
|
|
63
63
|
* substitution `TextEncoder` used to make, so the bytes are unchanged from what
|
|
64
64
|
* this library sent before.
|
|
65
|
+
* - An enum name where the schema declares an enum (issue #109: `"NONE"` where
|
|
66
|
+
* an int32 goes on the wire). Upstream's `fromObject` resolves names to numbers
|
|
67
|
+
* and its direct `encode` coerces any string with `| 0`, so a caller passing a
|
|
68
|
+
* name never sees a throw. The bridge codec accepts only numbers (numeric
|
|
69
|
+
* strings aside) and throws `invalid int32: "NONE"`. An unknown name is left to
|
|
70
|
+
* throw rather than silenced to `0`: upstream's direct encode would write `0`
|
|
71
|
+
* for it, but that puts a value on the wire nobody sent.
|
|
65
72
|
*
|
|
66
73
|
* Returns `item` itself when it has nothing to do, so the caller can tell a
|
|
67
74
|
* repair from a failure it does not understand.
|
|
@@ -77,6 +84,36 @@ const repairScalar = (kind, item) => {
|
|
|
77
84
|
const replaced = item.replace(UNPAIRED_SURROGATE, '\uFFFD');
|
|
78
85
|
return replaced === item ? item : replaced;
|
|
79
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* Enum name to wire number, built per enum on the first repair that needs it.
|
|
89
|
+
*
|
|
90
|
+
* Only the repair path reads this, and only for a field that actually holds a
|
|
91
|
+
* string — a message the codec accepts never reaches here, and a numeric enum
|
|
92
|
+
* never touches a map. Each table is built once from the generated entries and
|
|
93
|
+
* then reused; an importer that never sends a refused value allocates nothing.
|
|
94
|
+
* A `Map` (not a plain object) so names like `__proto__` are keys, not hazards.
|
|
95
|
+
*/
|
|
96
|
+
let enumTablesById;
|
|
97
|
+
const enumValueFor = (enumId, name) => {
|
|
98
|
+
if (enumId < 0 || enumId >= PROTO_ENUM_SCHEMAS.length)
|
|
99
|
+
return undefined;
|
|
100
|
+
enumTablesById ?? (enumTablesById = []);
|
|
101
|
+
let byName = enumTablesById[enumId];
|
|
102
|
+
if (byName === undefined) {
|
|
103
|
+
const entries = PROTO_ENUM_SCHEMAS[enumId]?.[1];
|
|
104
|
+
if (!entries)
|
|
105
|
+
return undefined;
|
|
106
|
+
byName = new Map();
|
|
107
|
+
for (let index = 0; index < entries.length; index += 2) {
|
|
108
|
+
const entryName = entries[index];
|
|
109
|
+
const entryValue = entries[index + 1];
|
|
110
|
+
if (typeof entryName === 'string' && typeof entryValue === 'number')
|
|
111
|
+
byName.set(entryName, entryValue);
|
|
112
|
+
}
|
|
113
|
+
enumTablesById[enumId] = byName;
|
|
114
|
+
}
|
|
115
|
+
return byName.get(name);
|
|
116
|
+
};
|
|
80
117
|
const longFromWords = (low, high, unsigned) => LongRuntime.fromBits(low, high, unsigned);
|
|
81
118
|
/**
|
|
82
119
|
* Split a decoded 64-bit value into the low/high words `longFromWords` takes.
|
|
@@ -254,12 +291,13 @@ const schemaIdFor = (path) => {
|
|
|
254
291
|
return schemaIdsByPath.get(path);
|
|
255
292
|
};
|
|
256
293
|
/**
|
|
257
|
-
* Coerces the
|
|
258
|
-
*
|
|
294
|
+
* Coerces the three inputs the bridge codec refuses back to what upstream
|
|
295
|
+
* Baileys writes, and returns `value` itself when there was nothing to coerce.
|
|
259
296
|
*
|
|
260
297
|
* Reference equality is the signal: the caller only reaches here after an
|
|
261
298
|
* encode threw, and an unchanged result means the failure was something else
|
|
262
|
-
* — a genuinely invalid number, a missing codec — which
|
|
299
|
+
* — a genuinely invalid number, an unknown enum name, a missing codec — which
|
|
300
|
+
* must keep propagating.
|
|
263
301
|
*
|
|
264
302
|
* Copy-on-write throughout, like `projectForEncode`: a branch with nothing to
|
|
265
303
|
* fix is shared, not rebuilt.
|
|
@@ -286,7 +324,16 @@ const repairMessage = (schemaId, value, ancestors) => {
|
|
|
286
324
|
const current = value[field[0]];
|
|
287
325
|
if (current === null || current === undefined)
|
|
288
326
|
continue;
|
|
289
|
-
const repair = (item) =>
|
|
327
|
+
const repair = (item) => {
|
|
328
|
+
if (field[1] === PROTO_FIELD_KIND.message)
|
|
329
|
+
return repairMessage(field[2], item, seen);
|
|
330
|
+
// `?? item`, not `|| item`: 0 is a valid wire value (e.g. `"NONE"`).
|
|
331
|
+
// Unknown names stay strings, so the retry still throws and the
|
|
332
|
+
// original failure keeps propagating instead of becoming a silent 0.
|
|
333
|
+
if (field[1] === PROTO_FIELD_KIND.enum && typeof item === 'string')
|
|
334
|
+
return enumValueFor(field[2], item) ?? item;
|
|
335
|
+
return repairScalar(field[1], item);
|
|
336
|
+
};
|
|
290
337
|
let converted = current;
|
|
291
338
|
if (field[3] & PROTO_FIELD_FLAG.repeated) {
|
|
292
339
|
if (Array.isArray(current)) {
|
|
@@ -457,8 +504,8 @@ class ProtoCompatibilityRuntime {
|
|
|
457
504
|
throw new Error(`protobuf codec unavailable for ${path}`);
|
|
458
505
|
const projected = this.projectForEncode(schemaId, message);
|
|
459
506
|
const encoded = sourceCodec.encode(projected);
|
|
460
|
-
// The bridge refuses
|
|
461
|
-
// Baileys still encodes
|
|
507
|
+
// The bridge refuses three inputs it used to accept silently, and upstream
|
|
508
|
+
// Baileys still encodes all three. Repairing on failure rather than checking
|
|
462
509
|
// every field on the way in is what keeps the ordinary encode free: a
|
|
463
510
|
// message the codec accepts never reaches the repair, and one that does
|
|
464
511
|
// not was already going to throw.
|
package/lib/Socket/events.js
CHANGED
|
@@ -22,7 +22,7 @@ import { buildGroupCreateStubMessage, buildGroupJoinRequestEvents, buildGroupNot
|
|
|
22
22
|
import { emitMessageUpsert } from '../Compatibility/message-upsert.js';
|
|
23
23
|
import { extractMessageCappingPayload } from './message-capping.js';
|
|
24
24
|
import { mapReachoutTimelock } from './reachout.js';
|
|
25
|
-
import { isReconnectableConnectFailure } from './terminal-close.js';
|
|
25
|
+
import { isReconnectableConnectFailure, mapConnectFailureToDisconnect } from './terminal-close.js';
|
|
26
26
|
const CANONICAL_MESSAGE_EVENT = 'message';
|
|
27
27
|
const MESSAGE_UPSERT_APPEND = 'append';
|
|
28
28
|
const MESSAGE_UPSERT_NOTIFY = 'notify';
|
|
@@ -191,41 +191,6 @@ const emitRetrying = (ctx) => ctx.ev.emit('connection.update', {
|
|
|
191
191
|
* dispatcher for why `badSession` was the wrong home.
|
|
192
192
|
*/
|
|
193
193
|
const CLIENT_OUTDATED_STATUS = 405;
|
|
194
|
-
/**
|
|
195
|
-
* Map bridge `ConnectFailureReason` wire codes (per the bridge's
|
|
196
|
-
* `.d.ts` annotation) onto upstream Baileys' `DisconnectReason`.
|
|
197
|
-
* Unknown codes fall through to `connectionClosed` so existing
|
|
198
|
-
* reconnect heuristics keep working.
|
|
199
|
-
*
|
|
200
|
-
* Several cases here are belt-and-braces: the engine dispatches its own event
|
|
201
|
-
* for `is_logged_out()` reasons (401/403/406) and for 405, so those never
|
|
202
|
-
* reach `connectFailure` in practice. Kept because they cost nothing and the
|
|
203
|
-
* engine's routing is not ours to depend on.
|
|
204
|
-
*/
|
|
205
|
-
const mapConnectFailureToDisconnect = (reason) => {
|
|
206
|
-
switch (reason) {
|
|
207
|
-
case 401: // LoggedOut
|
|
208
|
-
case 403: // MainDeviceGone
|
|
209
|
-
case 406: // UnknownLogout
|
|
210
|
-
return DisconnectReason.loggedOut;
|
|
211
|
-
case 402: // TempBanned
|
|
212
|
-
return DisconnectReason.forbidden;
|
|
213
|
-
case 405: // ClientOutdated
|
|
214
|
-
return CLIENT_OUTDATED_STATUS;
|
|
215
|
-
case 411: // MultideviceMismatch (legacy alias)
|
|
216
|
-
return DisconnectReason.multideviceMismatch;
|
|
217
|
-
case 503: // ServiceUnavailable
|
|
218
|
-
case 501: // Experimental
|
|
219
|
-
return DisconnectReason.unavailableService;
|
|
220
|
-
case 408: // Timed out
|
|
221
|
-
return DisconnectReason.timedOut;
|
|
222
|
-
case 515: // RestartRequired
|
|
223
|
-
return DisconnectReason.restartRequired;
|
|
224
|
-
// 400, 409, 413, 414, 415, 418, 500, undefined → generic close
|
|
225
|
-
default:
|
|
226
|
-
return DisconnectReason.connectionClosed;
|
|
227
|
-
}
|
|
228
|
-
};
|
|
229
194
|
const describeTempBan = (code) => {
|
|
230
195
|
switch (code) {
|
|
231
196
|
case 101:
|
package/lib/Socket/groups.js
CHANGED
|
@@ -20,6 +20,27 @@ export const JOIN_APPROVAL_MODES = ['on', 'off'];
|
|
|
20
20
|
// exact as a double, and that shape carries no methods. The helper reads both
|
|
21
21
|
// forms, and reconstructs the high word instead of dropping it.
|
|
22
22
|
const inviteExpirationNumber = (value) => toNumber(value);
|
|
23
|
+
// The core's V4 join parser only accepts a `<group>`, `<community>` or
|
|
24
|
+
// `<membership_approval_request>` child, but the server also answers a
|
|
25
|
+
// successful join with a bare `<iq type="result">` (WA Web's own
|
|
26
|
+
// `AcceptGroupAddResponseSuccess` variant requires no child at all — only the
|
|
27
|
+
// result envelope whose `from` echoes the request's `to`). The core reports
|
|
28
|
+
// that shape as an `IqError::ParseError`, which the bridge surfaces as
|
|
29
|
+
// `kind: 'internal'`. Error stanzas never reach the parser (they become
|
|
30
|
+
// `kind: 'server'` one layer below), so this substring can only mean the join
|
|
31
|
+
// was accepted and the JID carrier is missing — never a rejection.
|
|
32
|
+
const BARE_JOIN_SUCCESS_FRAGMENT = 'expected <group>, <community>, or <membership_approval_request> in join response';
|
|
33
|
+
// A bare `<iq type="result">` join success, as described above. Anything else
|
|
34
|
+
// (server rejections, timeouts, transport loss, protocol violations) must keep
|
|
35
|
+
// propagating.
|
|
36
|
+
const isBareJoinSuccess = (error) => {
|
|
37
|
+
if (!(error instanceof Error) || error.name !== 'WhatsAppError')
|
|
38
|
+
return false;
|
|
39
|
+
const kind = error.kind;
|
|
40
|
+
if (kind !== 'internal')
|
|
41
|
+
return false;
|
|
42
|
+
return typeof error.message === 'string' && error.message.includes(BARE_JOIN_SUCCESS_FRAGMENT);
|
|
43
|
+
};
|
|
23
44
|
export const makeGroupMethods = (ctx) => {
|
|
24
45
|
const groupMetadata = async (jid) => {
|
|
25
46
|
const metadata = await (await ctx.getClient()).getGroupMetadata(jid);
|
|
@@ -34,10 +55,22 @@ export const makeGroupMethods = (ctx) => {
|
|
|
34
55
|
// oxlint-disable-next-line typescript/no-explicit-any -- the established public contract returns Promise<any>.
|
|
35
56
|
) => {
|
|
36
57
|
const messageKey = typeof key === 'string' ? { remoteJid: key } : key;
|
|
37
|
-
|
|
58
|
+
const groupJid = inviteMessage.groupJid;
|
|
59
|
+
if (!groupJid || !inviteMessage.inviteCode || !messageKey.remoteJid) {
|
|
38
60
|
throw new TypeError('groupAcceptInviteV4 requires groupJid, inviteCode and inviter JID');
|
|
39
61
|
}
|
|
40
|
-
|
|
62
|
+
let joinedJid;
|
|
63
|
+
try {
|
|
64
|
+
joinedJid = await (await ctx.getClient()).groupAcceptInviteV4(groupJid, inviteMessage.inviteCode, inviteExpirationNumber(inviteMessage.inviteExpiration), messageKey.remoteJid);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// The join was accepted but the response carried no JID node.
|
|
68
|
+
// Baileys returns the envelope's `from` here, which echoes the
|
|
69
|
+
// request's `to` — the group JID we already hold.
|
|
70
|
+
if (!isBareJoinSuccess(error))
|
|
71
|
+
throw error;
|
|
72
|
+
joinedJid = groupJid;
|
|
73
|
+
}
|
|
41
74
|
if (messageKey.id) {
|
|
42
75
|
const expiredInvite = proto.Message.GroupInviteMessage.fromObject(inviteMessage);
|
|
43
76
|
expiredInvite.inviteExpiration = 0;
|
package/lib/Socket/index.js
CHANGED
|
@@ -31,6 +31,7 @@ import { makeBridgeClientOwner } from './bridge-client-owner.js';
|
|
|
31
31
|
import { warnUnsupportedConfig } from './unsupported-config.js';
|
|
32
32
|
import { wrapBridgeClient } from './bridge-error-boundary.js';
|
|
33
33
|
import { makeTerminalCloseReporter } from './terminal-close-reporter.js';
|
|
34
|
+
import { mapConnectFailureToDisconnect } from './terminal-close.js';
|
|
34
35
|
import { makeEventHandlers } from './events.js';
|
|
35
36
|
import { makeGroupMethods } from './groups.js';
|
|
36
37
|
import { makeInternalMethods, makeUnexpectedErrorReporter } from './internals.js';
|
|
@@ -71,6 +72,29 @@ const browserToPlatformType = (browser) => {
|
|
|
71
72
|
return 'CHROME';
|
|
72
73
|
}
|
|
73
74
|
};
|
|
75
|
+
const COMPLETION_FAILURE_CODES = new Map([
|
|
76
|
+
['Generic', 400],
|
|
77
|
+
['LoggedOut', 401],
|
|
78
|
+
['TempBanned', 402],
|
|
79
|
+
['AccountLocked', 403],
|
|
80
|
+
['UnknownLogout', 406],
|
|
81
|
+
['ClientOutdated', 405],
|
|
82
|
+
['BadUserAgent', 409],
|
|
83
|
+
['CatExpired', 413],
|
|
84
|
+
['CatInvalid', 414],
|
|
85
|
+
['NotFound', 415],
|
|
86
|
+
['ClientUnknown', 418],
|
|
87
|
+
['InternalServerError', 500],
|
|
88
|
+
['Experimental', 501],
|
|
89
|
+
['ServiceUnavailable', 503]
|
|
90
|
+
]);
|
|
91
|
+
const completionFailureCode = (reason) => {
|
|
92
|
+
const named = COMPLETION_FAILURE_CODES.get(reason);
|
|
93
|
+
if (named !== undefined)
|
|
94
|
+
return named;
|
|
95
|
+
const unknown = /^Unknown\((-?\d+)\)$/.exec(reason)?.[1];
|
|
96
|
+
return unknown === undefined ? undefined : Number(unknown);
|
|
97
|
+
};
|
|
74
98
|
/** Build the ws EventEmitter with auto-enable raw node forwarding */
|
|
75
99
|
const makeWASocket = (config) => {
|
|
76
100
|
const fullConfig = { ...DEFAULT_CONNECTION_CONFIG, ...config };
|
|
@@ -261,6 +285,37 @@ const makeWASocket = (config) => {
|
|
|
261
285
|
let autoReconnectEnabled = true;
|
|
262
286
|
/** Owns reporting the terminal close: once, after teardown, never not at all. */
|
|
263
287
|
const terminalClose = makeTerminalCloseReporter({ logger });
|
|
288
|
+
const runCompletionError = (completion) => {
|
|
289
|
+
let statusCode = DisconnectReason.connectionClosed;
|
|
290
|
+
let message = 'Connection closed';
|
|
291
|
+
if (completion.reason === 'unknown') {
|
|
292
|
+
message = `Connection run ended: ${completion.detail}`;
|
|
293
|
+
}
|
|
294
|
+
else if (completion.reason === 'stopped') {
|
|
295
|
+
message = 'Connection run stopped';
|
|
296
|
+
}
|
|
297
|
+
else if (completion.reason === 'already-running') {
|
|
298
|
+
message = 'Connection run was already running';
|
|
299
|
+
}
|
|
300
|
+
else if (completion.reason === 'auto-reconnect-disabled') {
|
|
301
|
+
const protocol = completion.protocolError;
|
|
302
|
+
if (protocol?.kind === 'conflict') {
|
|
303
|
+
statusCode = DisconnectReason.connectionReplaced;
|
|
304
|
+
message = 'Connection replaced';
|
|
305
|
+
}
|
|
306
|
+
else if (protocol?.kind === 'stream-error') {
|
|
307
|
+
statusCode = mapConnectFailureToDisconnect(protocol.code);
|
|
308
|
+
}
|
|
309
|
+
else if (protocol?.kind === 'connect-failure') {
|
|
310
|
+
statusCode = mapConnectFailureToDisconnect(completionFailureCode(protocol.reason));
|
|
311
|
+
}
|
|
312
|
+
else if (completion.connection?.kind === 'server-close') {
|
|
313
|
+
message = completion.connection.reason;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return new Boom(message, { statusCode, data: { runCompletion: completion } });
|
|
317
|
+
};
|
|
318
|
+
const reportTerminalClose = (error, publish) => terminalClose.reportAfter(() => owner.close(error).finally(() => initPromise), publish);
|
|
264
319
|
/**
|
|
265
320
|
* Held in a reporter rather than captured, because `sock.onUnexpectedError`
|
|
266
321
|
* is an assignable property: a consumer that replaces it has to be the one
|
|
@@ -401,7 +456,7 @@ const makeWASocket = (config) => {
|
|
|
401
456
|
// is still owned. This waits for the real teardown, and cannot
|
|
402
457
|
// deadlock because `reportAfter` runs it detached; nothing in the
|
|
403
458
|
// teardown is waiting on this.
|
|
404
|
-
|
|
459
|
+
reportTerminalClose(error, publish);
|
|
405
460
|
},
|
|
406
461
|
isAutoReconnectEnabled: () => autoReconnectEnabled,
|
|
407
462
|
// Timers the dispatcher armed outlive the events that armed them, and
|
|
@@ -527,23 +582,46 @@ const makeWASocket = (config) => {
|
|
|
527
582
|
// starting the read loop now would run against a handle about to go.
|
|
528
583
|
if (owner.isClosing())
|
|
529
584
|
return;
|
|
530
|
-
// `run()`
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
// Consequence: the loop's exit is not observable from here. The engine
|
|
536
|
-
// clears `enable_auto_reconnect` and breaks out on every terminal
|
|
537
|
-
// disconnect (conflict/401/409/516, and any `<failure>` whose reason is
|
|
538
|
-
// not 500/503), and when it does, the `WasmWhatsAppClient` is dead
|
|
539
|
-
// weight that only `sock.end()` can free — nothing else can, because
|
|
540
|
-
// the bridge holds the JS event callbacks as wasm-bindgen externrefs,
|
|
541
|
-
// those close over `ctx`, and `ctx` closes over `client`, so the cycle
|
|
542
|
-
// crosses the JS/wasm boundary and no `FinalizationRegistry` fires.
|
|
543
|
-
// Freeing that automatically needs the bridge to expose loop completion
|
|
544
|
-
// (a terminal callback or an awaitable handle); until it does, the
|
|
545
|
-
// consumer has to call `sock.end()` on a terminal close.
|
|
585
|
+
// `run()` deliberately returns immediately so callers can use the
|
|
586
|
+
// client while supervision owns its background task. Registering the
|
|
587
|
+
// completion observer after it is started is safe: bridge 0.21.0 admits
|
|
588
|
+
// late observers against the stored result for this run generation.
|
|
546
589
|
created.run();
|
|
590
|
+
const observedClient = created;
|
|
591
|
+
void created
|
|
592
|
+
.waitForRunCompletion()
|
|
593
|
+
.then(completion => {
|
|
594
|
+
// The owner identity is the socket generation fence. A completion
|
|
595
|
+
// from a client that teardown already released must never close a
|
|
596
|
+
// later socket using the same auth state.
|
|
597
|
+
if (owner.isClosing() || owner.peek() !== observedClient)
|
|
598
|
+
return;
|
|
599
|
+
const error = runCompletionError(completion);
|
|
600
|
+
reportTerminalClose(error, () => ev.emit('connection.update', {
|
|
601
|
+
connection: 'close',
|
|
602
|
+
lastDisconnect: { error, date: new Date() }
|
|
603
|
+
}));
|
|
604
|
+
}, error => {
|
|
605
|
+
if (owner.isClosing() || owner.peek() !== observedClient)
|
|
606
|
+
return;
|
|
607
|
+
const closeError = new Boom('Connection run ended without a completion result', {
|
|
608
|
+
statusCode: DisconnectReason.connectionClosed
|
|
609
|
+
});
|
|
610
|
+
reportTerminalClose(closeError, () => ev.emit('connection.update', {
|
|
611
|
+
connection: 'close',
|
|
612
|
+
lastDisconnect: { error: closeError, date: new Date() }
|
|
613
|
+
}));
|
|
614
|
+
try {
|
|
615
|
+
logger.error({ err: error }, 'bridge run completion observation failed');
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
// A consumer logger cannot prevent the terminal cleanup above.
|
|
619
|
+
}
|
|
620
|
+
})
|
|
621
|
+
.catch(() => {
|
|
622
|
+
// The reporter contains teardown and publish failures; this final guard
|
|
623
|
+
// also contains a consumer logger that throws from an observation path.
|
|
624
|
+
});
|
|
547
625
|
initialized = true;
|
|
548
626
|
};
|
|
549
627
|
/**
|
|
@@ -1,39 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Which bridge disconnects end the socket for good.
|
|
3
|
-
*
|
|
4
|
-
* This mirrors one decision that lives in the Rust engine, so keep it honest:
|
|
5
|
-
* `whatsapp-rust` clears `enable_auto_reconnect` and breaks out of its run loop
|
|
6
|
-
* on exactly these, and retries everything else on the WA Web Fibonacci backoff
|
|
7
|
-
* (`client/lifecycle.rs`).
|
|
8
|
-
*
|
|
9
|
-
* The socket layer needs the answer because `WasmWhatsAppClient.run()` returns
|
|
10
|
-
* `void` — it spawns the loop as a background task, so the loop's exit is not
|
|
11
|
-
* observable from JS (`whatsapp_rust_bridge.d.ts`, `run(): void`). Until the
|
|
12
|
-
* bridge exposes loop completion, this table is how the socket knows a client
|
|
13
|
-
* has become dead weight that only `free()` can reclaim.
|
|
14
|
-
*
|
|
15
|
-
* The upstream Baileys contract this buys us: `connection.update { close }`
|
|
16
|
-
* means "this socket is finished, build a new one" — which is what every
|
|
17
|
-
* consumer written against upstream already assumes, because upstream has no
|
|
18
|
-
* auto-reconnect at all. Transient drops therefore never surface as `close`;
|
|
19
|
-
* they surface as `connecting`.
|
|
20
|
-
*
|
|
21
|
-
* Events deliberately absent from the terminal set, and why:
|
|
22
|
-
* - `disconnected` — the engine only dispatches `Event::Disconnected` for an
|
|
23
|
-
* *unexpected* loop exit, and every terminal path sets `expected_disconnect`
|
|
24
|
-
* first, which suppresses it (`client/lifecycle.rs`). So this one is the
|
|
25
|
-
* "engine is retrying" signal — with one exception the dispatcher handles:
|
|
26
|
-
* under `setAutoReconnect(false)` the run loop dispatches `Disconnected`
|
|
27
|
-
* and only *then* tests the flag and breaks out, which makes the very same
|
|
28
|
-
* event terminal. Absence from this list means "not terminal on its own",
|
|
29
|
-
* not "never terminal".
|
|
30
|
-
* - `streamError` — reaches JS only from the engine's catch-all `<stream:error>`
|
|
31
|
-
* branch (unknown code, `<ack/>`, `<xml-not-well-formed>`); the coded ones
|
|
32
|
-
* (401/409/515/516/429/503) dispatch their own events instead. Every case
|
|
33
|
-
* that gets here keeps the connection or recycles it deliberately.
|
|
34
|
-
* - `pairError` — pairing failed, but the engine keeps its loop and re-emits a
|
|
35
|
-
* QR; nothing about the client is dead.
|
|
36
|
-
*/
|
|
37
1
|
/** True when the engine will keep retrying after this `<failure>`. */
|
|
38
2
|
export declare const isReconnectableConnectFailure: (reason: number | undefined) => boolean;
|
|
3
|
+
/** Map a typed bridge connect-failure code to Baileys' close status. */
|
|
4
|
+
export declare const mapConnectFailureToDisconnect: (reason: number | undefined) => number;
|
|
39
5
|
//# sourceMappingURL=terminal-close.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DisconnectReason } from '../Types/index.js';
|
|
1
2
|
/**
|
|
2
3
|
* Which bridge disconnects end the socket for good.
|
|
3
4
|
*
|
|
@@ -6,11 +7,10 @@
|
|
|
6
7
|
* on exactly these, and retries everything else on the WA Web Fibonacci backoff
|
|
7
8
|
* (`client/lifecycle.rs`).
|
|
8
9
|
*
|
|
9
|
-
* The socket layer needs the answer
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* has become dead weight that only `free()` can reclaim.
|
|
10
|
+
* The socket layer needs the answer for bridge events that classify a terminal
|
|
11
|
+
* close before the supervised run completion is observed. Bridge 0.21.0 also
|
|
12
|
+
* exposes `waitForRunCompletion()` for exits with no terminal event; that
|
|
13
|
+
* observer uses the same terminal-close reporter and owner as this table.
|
|
14
14
|
*
|
|
15
15
|
* The upstream Baileys contract this buys us: `connection.update { close }`
|
|
16
16
|
* means "this socket is finished, build a new one" — which is what every
|
|
@@ -48,4 +48,28 @@ const RECONNECTABLE_CONNECT_FAILURE_REASONS = new Set([
|
|
|
48
48
|
]);
|
|
49
49
|
/** True when the engine will keep retrying after this `<failure>`. */
|
|
50
50
|
export const isReconnectableConnectFailure = (reason) => reason !== undefined && RECONNECTABLE_CONNECT_FAILURE_REASONS.has(reason);
|
|
51
|
+
/** Map a typed bridge connect-failure code to Baileys' close status. */
|
|
52
|
+
export const mapConnectFailureToDisconnect = (reason) => {
|
|
53
|
+
switch (reason) {
|
|
54
|
+
case 401:
|
|
55
|
+
case 403:
|
|
56
|
+
case 406:
|
|
57
|
+
return DisconnectReason.loggedOut;
|
|
58
|
+
case 402:
|
|
59
|
+
return DisconnectReason.forbidden;
|
|
60
|
+
case 405:
|
|
61
|
+
return 405;
|
|
62
|
+
case 411:
|
|
63
|
+
return DisconnectReason.multideviceMismatch;
|
|
64
|
+
case 503:
|
|
65
|
+
case 501:
|
|
66
|
+
return DisconnectReason.unavailableService;
|
|
67
|
+
case 408:
|
|
68
|
+
return DisconnectReason.timedOut;
|
|
69
|
+
case 515:
|
|
70
|
+
return DisconnectReason.restartRequired;
|
|
71
|
+
default:
|
|
72
|
+
return DisconnectReason.connectionClosed;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
51
75
|
//# sourceMappingURL=terminal-close.js.map
|