strophe.js
Version:
Strophe.js is an XMPP library for JavaScript
120 lines (110 loc) • 4.19 kB
text/typescript
/**
* XEP-0198 Stream Management
*
* Environment-free helpers.
*
* IMPORTANT: this module must be loadable in a SharedWorker global (it is
* bundled into dist/shared-connection-worker.js), so keep it free of imports
* beyond constants (no ../utils.ts or ../builder.ts, and no *module-level DOM access).
*/
import { NS } from '../constants';
import { SMState } from './types';
/** The 'h' counter is an unsignedInt that wraps back to zero (XEP-0198 §4). */
export const H_WRAP = 2 ** 32;
/** Stanza names that count towards the 'h' counters (XEP-0198 §4). */
const COUNTABLE = ['iq', 'presence', 'message'];
/**
* Escapes invalid xml characters (duplicated from ../utils.ts so the worker
* bundle doesn't pull in utils and its dependencies).
* @param text
*/
export function xmlEscape(text: string): string {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/'/g, ''')
.replace(/"/g, '"');
}
/**
* @returns A fresh, inactive SM state object.
*/
export function freshState(): SMState {
return {
boundJid: null,
enableSent: false,
enabled: false,
hIn: 0,
hOutAcked: 0,
id: null,
location: null,
max: null,
resumeSupported: false,
resumed: false,
sinceLastAck: 0,
unacked: [],
};
}
/**
* Parse an 'h' attribute value into a wrapped unsigned 32-bit counter value.
* @param value - The attribute value (may be undefined).
* @returns The parsed value, or null if missing or invalid.
*/
export function parseH(value: string): number {
if (typeof value !== 'string') return null;
const h = parseInt(value, 10);
return Number.isNaN(h) || h < 0 ? null : h % H_WRAP;
}
/**
* @param name - A top-level element's local tag name.
* @returns true if the element counts towards the SM 'h' counters.
*/
export function isCountableStanza(name: string): boolean {
return COUNTABLE.includes(name);
}
/**
* Remove the `from` attribute from a serialized stanza's root element, so a
* re-sent stanza cannot carry a stale resource after a failed resumption. Only
* the root opening tag is touched; nested `from` attributes (forwarded stanzas,
* MUC addresses) are preserved. The server stamps the authoritative c2s `from`.
* DOM-free string surgery, mirroring stampDelay.
* @param serialized - The serialized stanza.
* @returns The stanza with the root `from` removed (unchanged if none).
*/
export function stripFrom(serialized: string): string {
const end = serialized.indexOf('>');
if (end === -1) {
return serialized;
}
const openTag = serialized.slice(0, end);
const rest = serialized.slice(end);
// Values cannot contain their own delimiter quote (the serializer escapes it),
// so a non-greedy match to the matching quote is safe. The leading \s is
// consumed too, leaving no double space. Only the first (root) from is removed.
const stripped = openTag.replace(/\sfrom=(["'])[\s\S]*?\1/, '');
return stripped === openTag ? serialized : stripped + rest;
}
/**
* Insert a XEP-0203 <delay/> child into a serialized stanza (DOM-free string
* surgery). Used when re-sending salvaged stanzas after a failed resumption,
* so the receiving client can show the original send time.
* @param serialized - The serialized stanza.
* @param name - The stanza's local tag name.
* @param queuedAt - The original queueing time (ms since epoch).
* @returns The stanza with a <delay/> child, or the unmodified input if it
* already carries one (or is malformed).
*/
export function stampDelay(serialized: string, name: string, queuedAt: number): string {
if (serialized.includes(NS.DELAY)) {
return serialized;
}
const delay = `<delay xmlns="${NS.DELAY}" stamp="${new Date(queuedAt).toISOString()}"/>`;
if (serialized.endsWith('/>')) {
return `${serialized.slice(0, -2)}>${delay}</${name}>`;
}
const idx = serialized.lastIndexOf(`</${name}`);
if (idx === -1) {
return serialized;
}
return serialized.slice(0, idx) + delay + serialized.slice(idx);
}