UNPKG

@fnlb-project/stanza

Version:

Modern XMPP in the browser, with a JSON API

186 lines (185 loc) 5.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = require("events"); const MAX_SEQ = Math.pow(2, 32); const mod = (v, n) => ((v % n) + n) % n; class StreamManagement extends events_1.EventEmitter { constructor() { super(); this.allowResume = true; this.lastAck = 0; this.handled = 0; this.unacked = []; this.inboundStarted = false; this.outboundStarted = false; this.id = undefined; this.jid = undefined; this.allowResume = true; this.started = false; this.cacheHandler = () => undefined; this._reset(); } get started() { return this.outboundStarted && this.inboundStarted; } set started(value) { if (!value) { this.outboundStarted = false; this.inboundStarted = false; } } get resumable() { return this.started && this.allowResume; } load(opts) { var _a; this.id = opts.id; this.allowResume = (_a = opts.allowResume) !== null && _a !== void 0 ? _a : true; this.handled = opts.handled; this.lastAck = opts.lastAck; this.unacked = opts.unacked; this.emit('prebound', opts.jid); } cache(handler) { this.cacheHandler = handler; } async bind(jid) { this.jid = jid; await this._cache(); } async enable() { this.emit('send', { allowResumption: this.allowResume, type: 'enable' }); } async resume() { this.emit('send', { handled: this.handled, previousSession: this.id, type: 'resume' }); } async enabled(resp) { this.id = resp.id; this.allowResume = resp.resume || false; this.handled = 0; this.inboundStarted = true; await this._cache(); } async resumed(resp) { this.id = resp.previousSession; this.inboundStarted = true; await this.process(resp, true); await this._cache(); } async failed(resp) { // Resumption might fail, but the server can still tell us how far // the old session progressed. await this.process(resp); // We alert that any remaining unacked stanzas failed to send. It has // been too long for auto-retrying these to be the right thing to do. for (const [kind, stanza] of this.unacked) { this.emit('failed', { kind, stanza }); } this._reset(); await this._cache(); } ack() { this.emit('send', { handled: this.handled, type: 'ack' }); } request() { this.emit('send', { type: 'request' }); } async process(ack, resend = false) { if (ack.handled === undefined) { return; } const numAcked = mod(ack.handled - this.lastAck, MAX_SEQ); for (let i = 0; i < numAcked && this.unacked.length > 0; i++) { const [kind, stanza] = this.unacked.shift(); this.emit('acked', { kind, stanza }); } this.lastAck = ack.handled; if (resend) { const resendUnacked = this.unacked; this.unacked = []; if (resendUnacked.length) { this.emit('begin-resend'); for (const [kind, stanza] of resendUnacked) { this.emit('resend', { kind, stanza }); } this.emit('end-resend'); } } await this._cache(); } async track(kind, stanza) { const isStanzaEnable = stanza.type === 'enable'; if (kind === 'sm' && (isStanzaEnable || stanza.type === 'resume')) { if (isStanzaEnable) { this.handled = 0; } this.outboundStarted = true; await this._cache(); return false; } if (!this.outboundStarted) { return false; } if (kind !== 'message' && kind !== 'presence' && kind !== 'iq') { return false; } this.unacked.push([kind, stanza]); await this._cache(); return true; } async handle() { if (this.inboundStarted) { this.handled = mod(this.handled + 1, MAX_SEQ); await this._cache(); } } async hibernate() { if (!this.resumable) { return this.shutdown(); } for (const [kind, stanza] of this.unacked) { this.emit('hibernated', { kind, stanza }); } } async shutdown() { return this.failed({ type: 'failed' }); } async _cache() { try { await this.cacheHandler({ allowResume: this.allowResume, handled: this.handled, id: this.id, jid: this.jid, lastAck: this.lastAck, unacked: this.unacked }); } catch (err) { // TODO: Is there a good way to handle this? // istanbul ignore next console.error('Failed to cache stream state', err); } } _reset() { this.id = ''; this.inboundStarted = false; this.outboundStarted = false; this.lastAck = 0; this.handled = 0; this.unacked = []; } } exports.default = StreamManagement;