@oxidezap/baileyrs 0.2.6 → 0.2.8

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/README.md CHANGED
@@ -295,6 +295,14 @@ What to do instead:
295
295
  tens of minutes rather than seconds, and have it alert a human instead of
296
296
  killing the process. A shorter one fires on a backoff that was about to
297
297
  succeed.
298
+ - Know what your own calls do meanwhile. A call issued while the engine is
299
+ reconnecting no longer fails fast: it parks and goes out when the connection
300
+ lands, which on the ladder above can be tens of minutes. That is the point,
301
+ since the alternative was an error indistinguishable from a finished session,
302
+ but it means a call you issue during `connecting` is a call you are committing
303
+ to. Racing it against your own deadline bounds your waiting and not the work:
304
+ the call still goes out when the reconnect lands, so a retry after the
305
+ deadline sends it twice unless you pinned `messageId`.
298
306
  - Fix the cause on your side: send less. The engine restores the connection,
299
307
  but it does not pace your traffic, and the traffic is what earned the `429`.
300
308
  Queueing, throttling and deferral are yours to decide, the same as upstream.
@@ -366,6 +374,29 @@ baileyrs auto-wraps, including `useLegacyMultiFileAuthState`. It does not apply
366
374
  to `useMultiFileAuthState`, whose `keys` is a projection over the engine's own
367
375
  store; the `bridge-` namespaces never surface through it.
368
376
 
377
+ The move back down is not symmetric either. From 0.2.8 on, the core buffers a
378
+ skipped-message key as its seed alone, where an earlier release wrote the
379
+ derived cipher/mac/iv triple beside it. Forward is fine — a record written
380
+ before this carries both, and the newer engine reads it unchanged. Backward is
381
+ not: an older engine cannot parse a record whose skipped keys carry no triple,
382
+ and it fails on **the whole record**, not on the one key.
383
+
384
+ Where that bites depends on which bytes the engine is handed, and a wrapped
385
+ store is not automatically on the safe side of it. It holds the Baileys
386
+ projection, which has only ever held seeds — but it also mirrors the core's
387
+ exact bytes under `bridge-native-session`, and a read prefers that mirror for as
388
+ long as its fingerprint still matches the projection. A downgrade can therefore
389
+ be handed the newer record with a perfectly readable projection sitting right
390
+ beside it.
391
+
392
+ Dropping a mirror row makes the next read rebuild it from the projection, which
393
+ is the format both versions agree on — but only for a session that has one. A
394
+ session that turned native-only lives in that row alone, so dropping *that* one
395
+ loses the session outright, which is the case the warning above is about. A
396
+ session with no skipped keys buffered at the time is unaffected either way: the
397
+ difference only exists while a chain is holding keys for messages that arrived
398
+ out of order.
399
+
369
400
  ## Disclaimer
370
401
 
371
402
  This project is not affiliated with, endorsed by, or in any way officially connected to
@@ -35,7 +35,17 @@ const emitCBEvents = (ctx, node) => {
35
35
  const id = l1.id;
36
36
  if (id)
37
37
  ws.emit(`${DEF_TAG_PREFIX}${id}`, node);
38
- for (const [key, val] of Object.entries(l1)) {
38
+ // `for..in` over the attrs rather than `Object.entries`: this runs for every
39
+ // stanza the socket sees and the pair array it built was thrown away
40
+ // immediately. `Object.entries` was own-only by construction though, and
41
+ // `BinaryNode` is public, so the walk is guarded rather than trusting every
42
+ // caller to hand over an attrs map with a bare prototype. An inherited
43
+ // enumerable would otherwise fire CB events for an attr the stanza does not
44
+ // carry; the check costs nothing next to the emits below.
45
+ for (const key in l1) {
46
+ if (!Object.hasOwn(l1, key))
47
+ continue;
48
+ const val = l1[key];
39
49
  if (l2)
40
50
  ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${val},${l2}`, node);
41
51
  ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${val}`, node);
@@ -612,13 +612,54 @@ export const generateWAMessage = async (jid, content, options) => {
612
612
  : options;
613
613
  return generateWAMessageFromContent(jid, await generateWAMessageContent(content, contentOptions), options);
614
614
  };
615
+ /**
616
+ * The content keys that carry almost every message on a live socket, settled by
617
+ * identity before the structural predicate runs. Every name here already
618
+ * satisfies that predicate, so this only shortcuts the substring scan; it never
619
+ * changes which key wins.
620
+ */
621
+ const isCommonContentKey = (key) => {
622
+ switch (key) {
623
+ case 'conversation':
624
+ case 'extendedTextMessage':
625
+ case 'imageMessage':
626
+ case 'videoMessage':
627
+ case 'audioMessage':
628
+ case 'stickerMessage':
629
+ case 'documentMessage':
630
+ case 'reactionMessage':
631
+ case 'protocolMessage':
632
+ case 'pollCreationMessage':
633
+ return true;
634
+ default:
635
+ return false;
636
+ }
637
+ };
615
638
  /** Get the key to access the true type of content */
616
639
  export const getContentType = (content) => {
617
- if (content) {
618
- const keys = Object.keys(content);
619
- const key = keys.find(k => (k === 'conversation' || k.includes('Message')) && k !== 'senderKeyDistributionMessage');
620
- return key;
640
+ if (!content) {
641
+ return undefined;
621
642
  }
643
+ // The answer stays positional (the first own key naming content wins), but
644
+ // the scan is an indexed loop with an early return instead of `Array.find`,
645
+ // so no closure is allocated and the common keys never reach `includes`.
646
+ //
647
+ // Deliberately still `Object.keys` and not `for..in`: a decoded
648
+ // `WAProto.Message` inherits a default for every field of the schema, so
649
+ // `for..in` would enumerate ~100 prototype keys per call (and would have to
650
+ // filter them back out with `hasOwn` to stay positional) where `Object.keys`
651
+ // yields only the handful that were actually set.
652
+ const keys = Object.keys(content);
653
+ for (let i = 0; i < keys.length; i++) {
654
+ const key = keys[i];
655
+ if (isCommonContentKey(key)) {
656
+ return key;
657
+ }
658
+ if (key.includes('Message') && key !== 'senderKeyDistributionMessage') {
659
+ return key;
660
+ }
661
+ }
662
+ return undefined;
622
663
  };
623
664
  const getFutureProofMessage = (message) => message?.ephemeralMessage ||
624
665
  message?.viewOnceMessage ||
@@ -57,8 +57,52 @@ export const jidDecode = (jid) => {
57
57
  device: device ? +device : undefined
58
58
  };
59
59
  };
60
+ const CHAR_DEVICE_SEP = 58; // ':'
61
+ const CHAR_AGENT_SEP = 95; // '_'
62
+ /**
63
+ * Index at which the user component ends, given the index of the '@'.
64
+ *
65
+ * `jidDecode` derives the user by splitting the pre-`@` half on ':' and then on
66
+ * '_', so the user always runs to whichever of ':' or '_' comes first, and to
67
+ * the '@' when neither is there. Locating that boundary answers both callers
68
+ * below without the intermediate arrays and result object `jidDecode` allocates.
69
+ */
70
+ const userEndBefore = (jid, sepIdx) => {
71
+ for (let i = 0; i < sepIdx; i++) {
72
+ const code = jid.charCodeAt(i);
73
+ if (code === CHAR_DEVICE_SEP || code === CHAR_AGENT_SEP) {
74
+ return i;
75
+ }
76
+ }
77
+ return sepIdx;
78
+ };
79
+ /** As above, or -1 when the JID has no server part: the case `jidDecode` reports as `undefined`. */
80
+ const userEnd = (jid) => {
81
+ const sepIdx = jid.indexOf('@');
82
+ return sepIdx < 0 ? -1 : userEndBefore(jid, sepIdx);
83
+ };
60
84
  /** Compare the user component of two JIDs, matching upstream Baileys. */
61
- export const areJidsSameUser = (jid1, jid2) => jidDecode(jid1)?.user === jidDecode(jid2)?.user;
85
+ export const areJidsSameUser = (jid1, jid2) => {
86
+ // Same answer as comparing `jidDecode(...)?.user`, compared in place: this
87
+ // runs once per participant check on the message pipeline, and decoding both
88
+ // sides allocated four arrays and two objects only to throw them away.
89
+ const end = typeof jid1 === 'string' ? userEnd(jid1) : -1;
90
+ if (end !== (typeof jid2 === 'string' ? userEnd(jid2) : -1)) {
91
+ return false;
92
+ }
93
+ // Neither side decodes, so upstream compares `undefined === undefined`.
94
+ if (end < 0) {
95
+ return true;
96
+ }
97
+ const left = jid1;
98
+ const right = jid2;
99
+ for (let i = 0; i < end; i++) {
100
+ if (left.charCodeAt(i) !== right.charCodeAt(i)) {
101
+ return false;
102
+ }
103
+ }
104
+ return true;
105
+ };
62
106
  export const isJidMetaAI = (jid) => jid?.endsWith('@bot');
63
107
  export const isPnUser = (jid) => jid?.endsWith('@s.whatsapp.net');
64
108
  export const isLidUser = (jid) => jid?.endsWith('@lid');
@@ -71,12 +115,25 @@ export const isHostedPnUser = (jid) => jid?.endsWith('@hosted');
71
115
  const botRegexp = /^1313555\d{4}$|^131655500\d{2}$/;
72
116
  export const isJidBot = (jid) => jid && botRegexp.test(jid.split('@')[0]) && jid.endsWith('@c.us');
73
117
  export const jidNormalizedUser = (jid) => {
74
- const result = jidDecode(jid);
75
- if (!result) {
118
+ if (typeof jid !== 'string') {
119
+ return '';
120
+ }
121
+ const sepIdx = jid.indexOf('@');
122
+ if (sepIdx < 0) {
76
123
  return '';
77
124
  }
78
- const { user, server } = result;
79
- return jidEncode(user, server === 'c.us' ? 's.whatsapp.net' : server);
125
+ // Everything between the user and the '@' is exactly what normalization
126
+ // drops, so the boundary is all this needs. `jidDecode` would split the same
127
+ // half twice and box the pieces in an object only for them to be re-joined.
128
+ const end = userEndBefore(jid, sepIdx);
129
+ // Fast path for the shape that dominates the pipeline: nothing to strip and a
130
+ // server `jidEncode` re-emits verbatim, so the JID already is its own normal
131
+ // form and no new string has to be built at all.
132
+ if (end === sepIdx && !jid.endsWith('@c.us')) {
133
+ return jid;
134
+ }
135
+ const server = jid.slice(sepIdx + 1);
136
+ return jidEncode(jid.slice(0, end), server === 'c.us' ? 's.whatsapp.net' : server);
80
137
  };
81
138
  export const transferDevice = (fromJid, toJid) => {
82
139
  const deviceId = jidDecode(fromJid)?.device || 0;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oxidezap/baileyrs",
3
3
  "type": "module",
4
- "version": "0.2.6",
4
+ "version": "0.2.8",
5
5
  "description": "A Rust-powered WhatsApp Web library for JavaScript, with a Baileys-compatible API",
6
6
  "keywords": [
7
7
  "whatsapp",
@@ -82,7 +82,7 @@
82
82
  },
83
83
  "dependencies": {
84
84
  "@hapi/boom": "^9.1.4",
85
- "@oxidezap/whatsapp-rust-bridge": "0.16.0",
85
+ "@oxidezap/whatsapp-rust-bridge": "0.18.0",
86
86
  "long": "^5.3.2",
87
87
  "pino": "^10.3.1",
88
88
  "protobufjs": "^7.6.5"