@oxidezap/baileyrs 0.2.13 → 0.3.0

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.
@@ -6,7 +6,7 @@ import { GROUP_REQUEST_ACTIONS, GROUP_SETTINGS, JOIN_APPROVAL_MODES, makeGroupMe
6
6
  export const makeCommunityMethods = (ctx, groups = makeGroupMethods(ctx)) => {
7
7
  const communityMetadata = async (jid) => groups.groupMetadata(jid);
8
8
  const communityFetchAllParticipating = async () => {
9
- const bridgeCommunities = await (await ctx.getClient()).communityFetchAllParticipating();
9
+ const bridgeCommunities = await ctx.withClient(client => client.communityFetchAllParticipating());
10
10
  const result = {};
11
11
  for (const [communityJid, metadata] of Object.entries(bridgeCommunities)) {
12
12
  result[communityJid] = bridgeGroupMetadataToBaileys(metadata);
@@ -17,30 +17,30 @@ export const makeCommunityMethods = (ctx, groups = makeGroupMethods(ctx)) => {
17
17
  return {
18
18
  communityMetadata,
19
19
  communityCreate: async (subject, body) => {
20
- const metadata = await (await ctx.getClient()).createCommunity(subject, body || undefined, true, true, true);
20
+ const metadata = await ctx.withClient(client => client.createCommunity(subject, body || undefined, true, true, true));
21
21
  return bridgeGroupMetadataToBaileys(metadata);
22
22
  },
23
23
  communityCreateGroup: async (subject, participants, parentCommunityJid) => {
24
- const metadata = await (await ctx.getClient()).createCommunitySubgroup(subject, participants, parentCommunityJid);
24
+ const metadata = await ctx.withClient(client => client.createCommunitySubgroup(subject, participants, parentCommunityJid));
25
25
  return bridgeGroupMetadataToBaileys(metadata);
26
26
  },
27
27
  communityLeave: async (id) => {
28
- await (await ctx.getClient()).deactivateCommunity(id);
28
+ await ctx.withClient(client => client.deactivateCommunity(id));
29
29
  },
30
30
  communityUpdateSubject: async (jid, subject) => {
31
31
  await groups.groupUpdateSubject(jid, subject);
32
32
  },
33
33
  communityLinkGroup: async (groupJid, parentCommunityJid) => {
34
- await (await ctx.getClient()).linkCommunitySubgroups(parentCommunityJid, [groupJid]);
34
+ await ctx.withClient(client => client.linkCommunitySubgroups(parentCommunityJid, [groupJid]));
35
35
  },
36
36
  communityUnlinkGroup: async (groupJid, parentCommunityJid) => {
37
- await (await ctx.getClient()).unlinkCommunitySubgroups(parentCommunityJid, [groupJid], false);
37
+ await ctx.withClient(client => client.unlinkCommunitySubgroups(parentCommunityJid, [groupJid], false));
38
38
  },
39
39
  communityFetchLinkedGroups: async (jid) => {
40
40
  const metadata = await groups.groupMetadata(jid);
41
41
  const communityJid = metadata.linkedParent || jid;
42
42
  const isCommunity = !metadata.linkedParent;
43
- const subgroups = await (await ctx.getClient()).getCommunitySubgroups(communityJid);
43
+ const subgroups = await ctx.withClient(client => client.getCommunitySubgroups(communityJid));
44
44
  const linkedGroups = subgroups.map(group => ({
45
45
  id: group.id,
46
46
  subject: group.subject,
@@ -61,7 +61,7 @@ export const makeCommunityMethods = (ctx, groups = makeGroupMethods(ctx)) => {
61
61
  },
62
62
  communityParticipantsUpdate: async (jid, participants, action) => {
63
63
  assertArgumentDomain('communityParticipantsUpdate', 'action', action, PARTICIPANT_ACTIONS);
64
- return bridgeParticipantChangesToBaileys(await (await ctx.getClient()).communityParticipantsUpdate(jid, participants, action));
64
+ return bridgeParticipantChangesToBaileys(await ctx.withClient(client => client.communityParticipantsUpdate(jid, participants, action)));
65
65
  },
66
66
  communityUpdateDescription: groups.groupUpdateDescription,
67
67
  communityInviteCode: groups.groupInviteCode,
@@ -1,29 +1,32 @@
1
1
  import { assertArgumentDomain } from '../Utils/argument-domain.js';
2
2
  export const PROFILE_PICTURE_TYPES = ['preview', 'image'];
3
- export const makeContactMethods = (ctx) => ({
4
- onWhatsApp: async (...phoneNumber) => {
5
- const client = await ctx.getClient();
6
- // Single batched usync — the bridge splits PN/LID inputs internally and
7
- // returns lid/pnJid/isBusiness in the same payload, so no secondary IQ.
8
- const results = await client.isOnWhatsApp(phoneNumber);
9
- return results.map(r => {
10
- const out = { exists: r.isRegistered, jid: r.jid, isBusiness: r.isBusiness };
11
- if (r.lid)
12
- out.lid = r.lid;
13
- if (r.pnJid)
14
- out.pnJid = r.pnJid;
15
- if (r.verifiedName)
16
- out.verifiedName = r.verifiedName;
17
- return out;
18
- });
19
- },
20
- profilePictureUrl: async (jid, type = 'preview', timeoutMs) => {
21
- assertArgumentDomain('profilePictureUrl', 'type', type, PROFILE_PICTURE_TYPES);
22
- const result = await (await ctx.getClient()).profilePictureUrl(jid, type, timeoutMs);
23
- return result?.url;
24
- },
25
- fetchUserInfo: async (...jids) => {
26
- return await (await ctx.getClient()).fetchUserInfo(jids);
27
- }
28
- });
3
+ export const makeContactMethods = (ctx) => {
4
+ return {
5
+ onWhatsApp: async (...phoneNumber) => {
6
+ return ctx.withClient(async (client) => {
7
+ // Single batched usync the bridge splits PN/LID inputs internally and
8
+ // returns lid/pnJid/isBusiness in the same payload, so no secondary IQ.
9
+ const results = await client.isOnWhatsApp(phoneNumber);
10
+ return results.map(r => {
11
+ const out = { exists: r.isRegistered, jid: r.jid, isBusiness: r.isBusiness };
12
+ if (r.lid)
13
+ out.lid = r.lid;
14
+ if (r.pnJid)
15
+ out.pnJid = r.pnJid;
16
+ if (r.verifiedName)
17
+ out.verifiedName = r.verifiedName;
18
+ return out;
19
+ });
20
+ });
21
+ },
22
+ profilePictureUrl: async (jid, type = 'preview', timeoutMs) => {
23
+ assertArgumentDomain('profilePictureUrl', 'type', type, PROFILE_PICTURE_TYPES);
24
+ const result = await ctx.withClient(client => client.profilePictureUrl(jid, type, timeoutMs));
25
+ return result?.url;
26
+ },
27
+ fetchUserInfo: async (...jids) => {
28
+ return await ctx.withClient(client => client.fetchUserInfo(jids));
29
+ }
30
+ };
31
+ };
29
32
  //# sourceMappingURL=contacts.js.map
@@ -546,7 +546,7 @@ const DISPATCHERS = {
546
546
  // the same complete lifecycle and ordering as upstream.
547
547
  void (async () => {
548
548
  try {
549
- const bridgeMetadata = await (await ctx.getClient()).getGroupMetadata(evt.groupJid);
549
+ const bridgeMetadata = await ctx.withClient(client => client.getGroupMetadata(evt.groupJid));
550
550
  const metadata = bridgeGroupMetadataToBaileys(bridgeMetadata);
551
551
  ctx.ev.emit('chats.upsert', [
552
552
  { id: metadata.id, name: metadata.subject, conversationTimestamp: metadata.creation }
@@ -20,36 +20,15 @@ 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
- };
44
23
  export const makeGroupMethods = (ctx) => {
45
24
  const groupMetadata = async (jid) => {
46
- const metadata = await (await ctx.getClient()).getGroupMetadata(jid);
25
+ const metadata = await ctx.withClient(client => client.getGroupMetadata(jid));
47
26
  return bridgeGroupMetadataToBaileys(metadata);
48
27
  };
49
28
  const groupSettingUpdate = async (jid, setting) => {
50
29
  const checked = assertArgumentDomain('groupSettingUpdate', 'setting', setting, GROUP_SETTINGS);
51
30
  const mapped = GROUP_SETTING_ALIASES[checked];
52
- await (await ctx.getClient()).groupSettingUpdate(jid, mapped.setting, mapped.value);
31
+ await ctx.withClient(client => client.groupSettingUpdate(jid, mapped.setting, mapped.value));
53
32
  };
54
33
  const groupAcceptInviteV4 = ctx.ev.createBufferedFunction(async (key, inviteMessage
55
34
  // oxlint-disable-next-line typescript/no-explicit-any -- the established public contract returns Promise<any>.
@@ -59,18 +38,7 @@ export const makeGroupMethods = (ctx) => {
59
38
  if (!groupJid || !inviteMessage.inviteCode || !messageKey.remoteJid) {
60
39
  throw new TypeError('groupAcceptInviteV4 requires groupJid, inviteCode and inviter JID');
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
+ const joinedJid = await ctx.withClient(client => client.groupAcceptInviteV4(groupJid, inviteMessage.inviteCode, inviteExpirationNumber(inviteMessage.inviteExpiration), messageKey.remoteJid));
74
42
  if (messageKey.id) {
75
43
  const expiredInvite = proto.Message.GroupInviteMessage.fromObject(inviteMessage);
76
44
  expiredInvite.inviteExpiration = 0;
@@ -101,61 +69,61 @@ export const makeGroupMethods = (ctx) => {
101
69
  return {
102
70
  groupMetadata,
103
71
  groupCreate: async (subject, participants) => {
104
- const metadata = await (await ctx.getClient()).createGroup(subject, participants);
72
+ const metadata = await ctx.withClient(client => client.createGroup(subject, participants));
105
73
  return bridgeGroupMetadataToBaileys(metadata);
106
74
  },
107
75
  groupLeave: async (id) => {
108
- await (await ctx.getClient()).groupLeave(id);
76
+ await ctx.withClient(client => client.groupLeave(id));
109
77
  },
110
78
  groupUpdateSubject: async (jid, subject) => {
111
- await (await ctx.getClient()).groupUpdateSubject(jid, subject);
79
+ await ctx.withClient(client => client.groupUpdateSubject(jid, subject));
112
80
  },
113
81
  groupRequestParticipantsList: async (jid) => {
114
- return bridgeMembershipRequestsToBaileys(await (await ctx.getClient()).groupRequestParticipantsList(jid));
82
+ return bridgeMembershipRequestsToBaileys(await ctx.withClient(client => client.groupRequestParticipantsList(jid)));
115
83
  },
116
84
  groupRequestParticipantsUpdate: async (jid, participants, action) => {
117
85
  assertArgumentDomain('groupRequestParticipantsUpdate', 'action', action, GROUP_REQUEST_ACTIONS);
118
- return bridgeMembershipRequestUpdatesToBaileys(await (await ctx.getClient()).groupRequestParticipantsUpdate(jid, participants, action));
86
+ return bridgeMembershipRequestUpdatesToBaileys(await ctx.withClient(client => client.groupRequestParticipantsUpdate(jid, participants, action)));
119
87
  },
120
88
  groupParticipantsUpdate: async (jid, participants, action) => {
121
89
  assertArgumentDomain('groupParticipantsUpdate', 'action', action, PARTICIPANT_ACTIONS);
122
- return bridgeParticipantChangesToBaileys(await (await ctx.getClient()).groupParticipantsUpdate(jid, participants, action));
90
+ return bridgeParticipantChangesToBaileys(await ctx.withClient(client => client.groupParticipantsUpdate(jid, participants, action)));
123
91
  },
124
92
  groupUpdateDescription: async (jid, description) => {
125
- await (await ctx.getClient()).groupUpdateDescription(jid, description);
93
+ await ctx.withClient(client => client.groupUpdateDescription(jid, description));
126
94
  },
127
95
  groupInviteCode: async (jid) => {
128
- return bridgeInviteLinkToCode(await (await ctx.getClient()).groupInviteCode(jid));
96
+ return bridgeInviteLinkToCode(await ctx.withClient(client => client.groupInviteCode(jid)));
129
97
  },
130
98
  groupRevokeInvite: async (jid) => {
131
- return bridgeInviteLinkToCode(await (await ctx.getClient()).groupRevokeInvite(jid));
99
+ return bridgeInviteLinkToCode(await ctx.withClient(client => client.groupRevokeInvite(jid)));
132
100
  },
133
101
  groupAcceptInvite: async (code) => {
134
- return (await ctx.getClient()).groupAcceptInvite(code);
102
+ return ctx.withClient(client => client.groupAcceptInvite(code));
135
103
  },
136
104
  groupRevokeInviteV4: async (groupJid, invitedJid) => {
137
- return (await ctx.getClient()).groupRevokeInviteV4(groupJid, invitedJid);
105
+ return ctx.withClient(client => client.groupRevokeInviteV4(groupJid, invitedJid));
138
106
  },
139
107
  groupAcceptInviteV4,
140
108
  groupGetInviteInfo: async (code) => {
141
- return bridgeGroupMetadataToBaileys(await (await ctx.getClient()).groupGetInviteInfo(code));
109
+ return bridgeGroupMetadataToBaileys(await ctx.withClient(client => client.groupGetInviteInfo(code)));
142
110
  },
143
111
  groupToggleEphemeral: async (jid, ephemeralExpiration) => {
144
- await (await ctx.getClient()).groupToggleEphemeral(jid, ephemeralExpiration);
112
+ await ctx.withClient(client => client.groupToggleEphemeral(jid, ephemeralExpiration));
145
113
  },
146
114
  groupSettingUpdate,
147
115
  groupMemberAddMode: async (jid, mode) => {
148
116
  assertArgumentDomain('groupMemberAddMode', 'mode', mode, MEMBER_ADD_MODES);
149
- await (await ctx.getClient()).groupMemberAddMode(jid, mode);
117
+ await ctx.withClient(client => client.groupMemberAddMode(jid, mode));
150
118
  },
151
119
  groupJoinApprovalMode: async (jid, mode) => {
152
120
  // Anything but 'on' used to mean off, so a typo turned approvals off
153
121
  // and reported success.
154
122
  assertArgumentDomain('groupJoinApprovalMode', 'mode', mode, JOIN_APPROVAL_MODES);
155
- await (await ctx.getClient()).groupSettingUpdate(jid, 'membership_approval', mode === 'on');
123
+ await ctx.withClient(client => client.groupSettingUpdate(jid, 'membership_approval', mode === 'on'));
156
124
  },
157
125
  groupFetchAllParticipating: async () => {
158
- const bridgeGroups = await (await ctx.getClient()).groupFetchAllParticipating();
126
+ const bridgeGroups = await ctx.withClient(client => client.groupFetchAllParticipating());
159
127
  const result = {};
160
128
  for (const [groupJid, metadata] of Object.entries(bridgeGroups)) {
161
129
  result[groupJid] = bridgeGroupMetadataToBaileys(metadata);
@@ -164,7 +132,7 @@ export const makeGroupMethods = (ctx) => {
164
132
  return result;
165
133
  },
166
134
  updateMemberLabel: async (jid, memberLabel) => {
167
- return (await ctx.getClient()).updateMemberLabel(jid, memberLabel.slice(0, 30));
135
+ return ctx.withClient(client => client.updateMemberLabel(jid, memberLabel.slice(0, 30)));
168
136
  }
169
137
  };
170
138
  };
@@ -1,5 +1,5 @@
1
1
  import { Buffer } from 'node:buffer';
2
- import { type UploadMediaResult } from '@oxidezap/whatsapp-rust-bridge';
2
+ import { type WasmWhatsAppClient, type UploadMediaResult } from '@oxidezap/whatsapp-rust-bridge';
3
3
  import { WebSocketClient } from '../Compatibility/websocket-client.js';
4
4
  import { type MediaType } from '../Defaults/index.js';
5
5
  import type { BinaryNode, AuthenticationCreds, ConnectionState, Contact, ReachoutTimelockState, SignalKeyStoreWithTransaction, UserFacingSocketConfig, WABusinessProfile, WAMessage, WAMessageKey, WAPresence } from '../Types/index.js';
@@ -220,7 +220,7 @@ declare const makeWASocket: (config: UserFacingSocketConfig) => {
220
220
  */
221
221
  [Symbol.asyncDispose](): Promise<void>;
222
222
  user: Contact | undefined;
223
- waClient: import("@oxidezap/whatsapp-rust-bridge").WasmWhatsAppClient | undefined;
223
+ waClient: WasmWhatsAppClient | undefined;
224
224
  isConnected: boolean;
225
225
  isLoggedIn: boolean;
226
226
  authState: {
@@ -44,6 +44,7 @@ import { makeServerQueryMethods } from './server-queries.js';
44
44
  import { makeProfileMethods } from './profile.js';
45
45
  import { mapReachoutTimelock } from './reachout.js';
46
46
  import { makeHttpClient, makeTransport } from './transport.js';
47
+ import { makeWithClient } from './client-operations.js';
47
48
  import { makeUSyncMethods } from './usync.js';
48
49
  let wasmInitialized = false;
49
50
  /**
@@ -216,24 +217,19 @@ const makeWASocket = (config) => {
216
217
  throw firstFlushError;
217
218
  },
218
219
  release: async (client) => {
219
- // `disconnect()` before `free()` is defence in depth, not a fix for a
220
- // reproduced bug on this path.
220
+ // `disconnect()` before `free()` drains exactly one shape: a
221
+ // `disconnect()` still in flight. Since bridge 0.21.1, freeing
222
+ // with ordinary calls pending (`fetchBlocklist()`, `logout()`) is
223
+ // safe — its `Drop` signals shutdown and aborts the background
224
+ // tasks — but freeing mid-`disconnect()` still aborts the process
225
+ // (`async-lock` panicking while panicking). See
226
+ // `__tests__/bridge-free-safety.test.ts`.
221
227
  //
222
- // The hazard is real and reproducible at the bridge: freeing a client
223
- // with any call still pending corrupts the wasm heap — dlmalloc trips
224
- // `assertion failed: psize <= size + max_overhead` and the process
225
- // dies on `RuntimeError: unreachable`, from a microtask no try/catch
226
- // here can reach, since `free()` itself returns normally.
227
- // `logout()`, `disconnect()` and a plain `fetchBlocklist()` all
228
- // reproduce it — see `__tests__/bridge-free-safety.test.ts`.
229
- //
230
- // What keeps teardown off that path is the `ws.close()` above, which
231
- // is itself a `client.disconnect()` (`Compatibility/websocket-client.ts`).
232
- // This is the belt to that braces, and the gap it closes is
233
- // `WebSocketClient.close()`'s early return when `closing`/`closed` is
234
- // already set: that path does NOT await the disconnect it skipped, so
235
- // `void sock.ws.close(); await sock.end()` could otherwise reach
236
- // `free()` with the first disconnect still running.
228
+ // That shape is reachable: `WebSocketClient.close()` early-returns
229
+ // when `closing`/`closed` is already set without awaiting the
230
+ // disconnect it skipped, so `void sock.ws.close(); await sock.end()`
231
+ // could otherwise reach `free()` with the first disconnect still
232
+ // running. Awaiting it here is the belt to `ws.close()`'s braces.
237
233
  let disconnected = true;
238
234
  try {
239
235
  await client.disconnect();
@@ -339,61 +335,42 @@ const makeWASocket = (config) => {
339
335
  setUser: u => {
340
336
  user = u;
341
337
  },
342
- getClient: () => {
343
- // `peek()` keeps returning the client through `closing` so teardown
344
- // can still close the transport with it — but that is teardown's
345
- // client, not everyone's. Handing it to an ordinary call racing
346
- // shutdown, or made from an end handler, starts a bridge operation
347
- // while the client is being disconnected and freed, which is the
348
- // heap-corruption hazard the whole teardown ordering exists to
349
- // avoid. Refuse from the moment `close()` is called.
350
- if (owner.isClosing()) {
351
- return Promise.reject(new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }));
352
- }
353
- // Otherwise gated on `initialized`, not merely on the client
354
- // existing. `adopt()` publishes it several awaits before
355
- // `setDeviceProps`, the account lookups and `run()`, so keying off
356
- // `peek()` alone would hand ordinary calls like `sendMessage()` a
357
- // half-built client whose read loop has not started — and skip the
358
- // `initError` check when startup later fails.
359
- if (initialized) {
360
- const ready = owner.peek();
361
- if (ready)
362
- return Promise.resolve(wrapBridgeClient(ready));
363
- }
364
- return initPromise.then(() => {
365
- // Rechecked after the await: a close landing while startup was
366
- // still running would otherwise be handed the client anyway.
367
- //
368
- // The window between handing a client back and the call reaching
369
- // wasm cannot be closed here — that needs in-flight call
370
- // tracking. What covers it is `release`, which awaits
371
- // `client.disconnect()` before `free()`; the corruption comes
372
- // from freeing with a call pending, and the disconnect drains
373
- // those first (`__tests__/bridge-free-safety.test.ts`).
374
- if (owner.isClosing()) {
375
- throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed });
376
- }
377
- if (initError) {
378
- throw new Boom('Bridge client failed to initialize: ' + initError.message, { statusCode: 500 });
379
- }
380
- const built = owner.peek();
381
- if (!built)
382
- throw new Boom('Client not initialized', { statusCode: 500 });
383
- return wrapBridgeClient(built);
384
- });
385
- },
386
- getClientSync: () => {
387
- // Same rule as `getClient` — see there.
338
+ withClient: makeWithClient(getClient)
339
+ };
340
+ function getClient() {
341
+ // Teardown retains the client through closing to disconnect the transport.
342
+ // Ordinary operations must stop being admitted when close() is called.
343
+ if (owner.isClosing()) {
344
+ return Promise.reject(new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }));
345
+ }
346
+ // Otherwise gated on `initialized`, not merely on the client
347
+ // existing. `adopt()` publishes it several awaits before
348
+ // `setDeviceProps`, the account lookups and `run()`, so keying off
349
+ // `peek()` alone would hand ordinary calls like `sendMessage()` a
350
+ // half-built client whose read loop has not started — and skip the
351
+ // `initError` check when startup later fails.
352
+ if (initialized) {
353
+ const ready = owner.peek();
354
+ if (ready)
355
+ return Promise.resolve(wrapBridgeClient(ready));
356
+ }
357
+ return initPromise.then(() => {
358
+ // Closing may have started while initialization was pending.
359
+ // This gate does not track admitted operations. Bridge 0.21.1 tolerates
360
+ // free() during ordinary calls, but not during disconnect(); release
361
+ // still awaits that drain before freeing the client.
388
362
  if (owner.isClosing()) {
389
363
  throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed });
390
364
  }
365
+ if (initError) {
366
+ throw new Boom('Bridge client failed to initialize: ' + initError.message, { statusCode: 500 });
367
+ }
391
368
  const built = owner.peek();
392
369
  if (!built)
393
370
  throw new Boom('Client not initialized', { statusCode: 500 });
394
371
  return wrapBridgeClient(built);
395
- }
396
- };
372
+ });
373
+ }
397
374
  // The native repository delegates Signal state directly to the core and does
398
375
  // not need the standalone transaction facade. Keep that facade lazy for the
399
376
  // public authState and custom repository contracts that can observe it.
@@ -776,7 +753,7 @@ const makeWASocket = (config) => {
776
753
  });
777
754
  };
778
755
  const fetchReachoutTimelock = async () => {
779
- const payload = await (await ctx.getClient()).fetchReachoutTimelock();
756
+ const payload = await ctx.withClient(client => client.fetchReachoutTimelock());
780
757
  const state = mapReachoutTimelock(payload) ?? { isActive: false };
781
758
  ev.emit('connection.update', { reachoutTimeLock: state });
782
759
  return state;
@@ -784,14 +761,14 @@ const makeWASocket = (config) => {
784
761
  const query = async (node, timeoutMs) => {
785
762
  if (!node.attrs.id)
786
763
  node.attrs.id = generateMessageTag();
787
- const result = (await (await ctx.getClient()).queryNode(node, timeoutMs));
764
+ const result = (await ctx.withClient(client => client.queryNode(node, timeoutMs)));
788
765
  assertNodeErrorFree(result);
789
766
  return result;
790
767
  };
791
768
  const waitForMessage = makeTaggedMessageWaiter(ws, logger, fullConfig.defaultQueryTimeoutMs);
792
769
  const usyncMethods = makeUSyncMethods({
793
770
  queryNode: query,
794
- queryUsync: async (typedQuery) => (await ctx.getClient()).queryUsync(typedQuery)
771
+ queryUsync: async (typedQuery) => ctx.withClient(client => client.queryUsync(typedQuery))
795
772
  });
796
773
  const sock = {
797
774
  ev,
@@ -876,18 +853,18 @@ const makeWASocket = (config) => {
876
853
  notificationMutex,
877
854
  generateMessageTag,
878
855
  sendNode: async (frame) => {
879
- return (await ctx.getClient()).sendNode(frame);
856
+ return ctx.withClient(client => client.sendNode(frame));
880
857
  },
881
858
  assertSessions: async (jids, force) => {
882
- return (await ctx.getClient()).assertSessions(jids, force ?? false);
859
+ return ctx.withClient(client => client.assertSessions(jids, force ?? false));
883
860
  },
884
861
  getUSyncDevices: async (jids, useCache, ignoreZeroDevices) => {
885
- return (await ctx.getClient()).getUSyncDevices(jids, useCache, ignoreZeroDevices);
862
+ return ctx.withClient(client => client.getUSyncDevices(jids, useCache, ignoreZeroDevices));
886
863
  },
887
864
  waitForMessage,
888
865
  query,
889
866
  sendRawMessage: async (data) => {
890
- return (await ctx.getClient()).sendRawMessage(data instanceof Uint8Array ? data : new Uint8Array(data));
867
+ return ctx.withClient(client => client.sendRawMessage(data instanceof Uint8Array ? data : new Uint8Array(data)));
891
868
  },
892
869
  /**
893
870
  * `dsmMessage` is accepted so the signature matches upstream, and
@@ -901,7 +878,7 @@ const makeWASocket = (config) => {
901
878
  throw new Boom('createParticipantNodes: dsmMessage is not supported, the engine encrypts one payload for every recipient and cannot substitute a different one for your own devices', { statusCode: 501 });
902
879
  }
903
880
  const bytes = encodeProtoCompat('Message', message);
904
- return (await ctx.getClient()).createParticipantNodesBytes(jids, bytes, extraAttrs ?? {});
881
+ return ctx.withClient(client => client.createParticipantNodesBytes(jids, bytes, extraAttrs ?? {}));
905
882
  },
906
883
  signalRepository,
907
884
  ...makePreKeyMethods(ctx),
@@ -926,14 +903,15 @@ const makeWASocket = (config) => {
926
903
  // Ahead of the client: an off-union value used to fall through to the
927
904
  // chat-state branch and be reported as a missing jid.
928
905
  assertArgumentDomain('sendPresenceUpdate', 'type', type, WA_PRESENCES);
929
- const c = await ctx.getClient();
930
- if (type === 'available' || type === 'unavailable') {
931
- return c.sendPresence(type);
932
- }
933
- if (!toJid) {
934
- throw new Boom(`sendPresenceUpdate('${type}') requires a target jid`, { statusCode: 400 });
935
- }
936
- return c.sendChatState(toJid, type);
906
+ return ctx.withClient(async (c) => {
907
+ if (type === 'available' || type === 'unavailable') {
908
+ return c.sendPresence(type);
909
+ }
910
+ if (!toJid) {
911
+ throw new Boom(`sendPresenceUpdate('${type}') requires a target jid`, { statusCode: 400 });
912
+ }
913
+ return c.sendChatState(toJid, type);
914
+ });
937
915
  },
938
916
  /**
939
917
  * Plaintext media upload helper, source-compatible with the upstream
@@ -946,15 +924,15 @@ const makeWASocket = (config) => {
946
924
  // `toBridgeMediaType` below still refuses the ones it cannot map.
947
925
  assertArgumentDomain('waUploadToServer', 'mediaType', opts?.mediaType, MEDIA_TYPES);
948
926
  const bytes = data instanceof Uint8Array && !Buffer.isBuffer(data) ? data : new Uint8Array(data);
949
- return (await ctx.getClient()).uploadMedia(bytes, toBridgeMediaType(opts.mediaType));
927
+ return ctx.withClient(client => client.uploadMedia(bytes, toBridgeMediaType(opts.mediaType)));
950
928
  },
951
929
  ...makePrivacyMethods(ctx),
952
930
  updateDefaultDisappearingMode: async (duration) => {
953
- await (await ctx.getClient()).updateDefaultDisappearingMode(duration);
931
+ await ctx.withClient(client => client.updateDefaultDisappearingMode(duration));
954
932
  },
955
933
  rejectCall: async (callId, callFrom) => {
956
934
  const context = activeCallContexts.get(callId);
957
- await (await ctx.getClient()).rejectCall(callId, context?.peer ?? callFrom, context?.callCreator ?? callFrom);
935
+ await ctx.withClient(client => client.rejectCall(callId, context?.peer ?? callFrom, context?.callCreator ?? callFrom));
958
936
  activeCallContexts.delete(callId);
959
937
  },
960
938
  /**
@@ -974,14 +952,14 @@ const makeWASocket = (config) => {
974
952
  /** Upstream Baileys-compatible name. */
975
953
  fetchAccountReachoutTimelock: fetchReachoutTimelock,
976
954
  getBusinessProfile: async (jid) => {
977
- return bridgeBusinessProfileToBaileys(await (await ctx.getClient()).getBusinessProfile(jid));
955
+ return bridgeBusinessProfileToBaileys(await ctx.withClient(client => client.getBusinessProfile(jid)));
978
956
  },
979
957
  fetchMessageHistory: async (count, oldestMsgKey, oldestMsgTimestamp) => {
980
- return (await ctx.getClient()).fetchMessageHistory(count, oldestMsgKey.remoteJid || '', oldestMsgKey.id || '', oldestMsgKey.fromMe || false, typeof oldestMsgTimestamp === 'number' ? oldestMsgTimestamp : oldestMsgTimestamp.toNumber());
958
+ return ctx.withClient(client => client.fetchMessageHistory(count, oldestMsgKey.remoteJid || '', oldestMsgKey.id || '', oldestMsgKey.fromMe || false, typeof oldestMsgTimestamp === 'number' ? oldestMsgTimestamp : oldestMsgTimestamp.toNumber()));
981
959
  },
982
960
  sendStatusMessage: async (message, recipients) => {
983
961
  const bytes = encodeProtoCompat('Message', message);
984
- return (await ctx.getClient()).sendStatusMessageBytes(bytes, recipients);
962
+ return ctx.withClient(client => client.sendStatusMessageBytes(bytes, recipients));
985
963
  },
986
964
  ...makeMessageMethods(ctx),
987
965
  ...groupMethods,
@@ -1002,11 +980,11 @@ const makeWASocket = (config) => {
1002
980
  // the context below is built, and a check past that await reports a
1003
981
  // stack without the caller in it.
1004
982
  assertArgumentDomain('downloadMedia', 'type', type, MEDIA_DOWNLOAD_TYPES);
1005
- return downloadMediaMessage(message, type, options, {
983
+ return ctx.withClient(client => downloadMediaMessage(message, type, options, {
1006
984
  logger,
1007
985
  reuploadRequest: (m) => sock.updateMediaMessage(m),
1008
- waClient: await ctx.getClient()
1009
- });
986
+ waClient: client
987
+ }));
1010
988
  }
1011
989
  };
1012
990
  // Assigning replaces the handler the socket itself reports through, rather
@@ -65,7 +65,7 @@ export const makeInternalMethods = (ctx) => {
65
65
  * that wants the next attempt waits again.
66
66
  */
67
67
  waitForSocketOpen: async () => {
68
- await (await ctx.getClient()).waitForSocket(TRANSPORT_CONNECT_TIMEOUT_MS);
68
+ await ctx.withClient(client => client.waitForSocket(TRANSPORT_CONNECT_TIMEOUT_MS));
69
69
  },
70
70
  /**
71
71
  * Publishes a message onto the event bus, which is this layer's own job.