@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.
@@ -1,12 +1,16 @@
1
1
  /**
2
- * `encodeProto`, with the two inputs the bridge codec stopped accepting put back.
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
- * This is where that is absorbed, so the strict contract stays true of the
9
- * bridge and the tolerant one stays true of this library.
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 two inputs the bridge codec stopped accepting put back.
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
- * This is where that is absorbed, so the strict contract stays true of the
11
- * bridge and the tolerant one stays true of this library.
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 — and
26
- // it has to keep propagating rather than be retried into a second throw.
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
53
53
  /**
54
54
  * Puts back what the codec used to do with an input it now refuses.
55
55
  *
56
- * Two cases, both measured against upstream Baileys, which still encodes both:
56
+ * Three cases, all measured against upstream Baileys, which still encodes them:
57
57
  *
58
58
  * - An empty string where the schema declares a 64-bit integer. Every one of the
59
59
  * 134 send-path failures this addresses carried exactly `''`; a string that is
@@ -62,6 +62,13 @@ 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 two inputs the bridge codec stopped accepting back to what it
258
- * used to write, and returns `value` itself when there was nothing to coerce.
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 must keep propagating.
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) => field[1] === PROTO_FIELD_KIND.message ? repairMessage(field[2], item, seen) : repairScalar(field[1], 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 two inputs it used to accept silently, and upstream
461
- // Baileys still encodes both. Repairing on failure rather than checking
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.
@@ -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:
@@ -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
- if (!inviteMessage.groupJid || !inviteMessage.inviteCode || !messageKey.remoteJid) {
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
- const joinedJid = await (await ctx.getClient()).groupAcceptInviteV4(inviteMessage.groupJid, inviteMessage.inviteCode, inviteExpirationNumber(inviteMessage.inviteExpiration), messageKey.remoteJid);
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;
@@ -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
- terminalClose.reportAfter(() => owner.close(error).finally(() => initPromise), publish);
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()` spawns the connect/handshake/read/reconnect loop as a
531
- // background task and returns `void` it deliberately is not `async`,
532
- // so that it does not hold a wasm-bindgen borrow on `self` that would
533
- // block `disconnect()`.
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 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.
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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oxidezap/baileyrs",
3
3
  "type": "module",
4
- "version": "0.2.12",
4
+ "version": "0.2.13",
5
5
  "description": "A Rust-powered WhatsApp Web library for JavaScript, with a Baileys-compatible API",
6
6
  "keywords": [
7
7
  "whatsapp",