UNPKG

jsforce

Version:

Salesforce API Library for JavaScript

284 lines (257 loc) 7.85 kB
/** * @file Manages Streaming APIs * @author Shinichi Tomita <shinichi.tomita@gmail.com> */ import { EventEmitter } from 'events'; import { Client, Subscription } from 'faye'; import { registerModule } from '../jsforce'; import Connection from '../connection'; import { Record, Schema } from '../types'; import * as StreamingExtension from './streaming/extension'; /** * */ export type StreamingMessage<R extends Record> = { event: { type: string; createdDate: string; replayId: number; }; sobject: R; }; export type GenericStreamingMessage = { event: { createdDate: string; replayId: number; }; payload: string; }; export type PushEvent = { payload: string; userIds: string[]; }; export type PushEventResult = { fanoutCount: number; userOnlineStatus: { [userId: string]: boolean; }; }; export { Client, Subscription }; /*--------------------------------------------*/ /** * Streaming API topic class */ class Topic<S extends Schema, R extends Record> { _streaming: Streaming<S>; name: string; constructor(streaming: Streaming<S>, name: string) { this._streaming = streaming; this.name = name; } /** * Subscribe listener to topic */ subscribe(listener: (message: StreamingMessage<R>) => void): Subscription { return this._streaming.subscribe(this.name, listener); } /** * Unsubscribe listener from topic */ unsubscribe(subscr: Subscription) { this._streaming.unsubscribe(this.name, subscr); return this; } } /*--------------------------------------------*/ /** * Streaming API Generic Streaming Channel */ class Channel<S extends Schema> { _streaming: Streaming<S>; _id: Promise<string | undefined> | undefined; name: string; constructor(streaming: Streaming<S>, name: string) { this._streaming = streaming; this.name = name; } /** * Subscribe to channel */ subscribe(listener: Function): Subscription { return this._streaming.subscribe(this.name, listener); } unsubscribe(subscr: Subscription) { this._streaming.unsubscribe(this.name, subscr); return this; } push(events: PushEvent): Promise<PushEventResult>; push(events: PushEvent): Promise<PushEventResult[]>; async push(events: PushEvent | PushEvent[]) { const isArray = Array.isArray(events); const pushEvents = Array.isArray(events) ? events : [events]; const conn: Connection = (this._streaming._conn as unknown) as Connection; if (!this._id) { this._id = conn .sobject('StreamingChannel') .findOne({ Name: this.name }, ['Id']) .then((rec) => rec?.Id); } const id = await this._id; if (!id) { throw new Error(`No streaming channel available for name: ${this.name}`); } const channelUrl = `/sobjects/StreamingChannel/${id}/push`; const rets = await conn.requestPost<PushEventResult[]>(channelUrl, { pushEvents, }); return isArray ? rets : rets[0]; } } /*--------------------------------------------*/ /** * Streaming API class */ export class Streaming<S extends Schema> extends EventEmitter { _conn: Connection<S>; _topics: { [name: string]: Topic<S, Record> } = {}; _fayeClients: { [clientType: string]: Client } = {}; /** * */ constructor(conn: Connection<S>) { super(); this._conn = conn; } /* @private */ _createClient(forChannelName?: string | null, extensions?: any[]) { // forChannelName is advisory, for an API workaround. It does not restrict or select the channel. const needsReplayFix = typeof forChannelName === 'string' && forChannelName.startsWith('/u/'); const endpointUrl = [ this._conn.instanceUrl, // special endpoint "/cometd/replay/xx.x" is only available in 36.0. // See https://releasenotes.docs.salesforce.com/en-us/summer16/release-notes/rn_api_streaming_classic_replay.htm 'cometd' + (needsReplayFix && this._conn.version === '36.0' ? '/replay' : ''), this._conn.version, ].join('/'); const fayeClient = new Client(endpointUrl, {}); fayeClient.setHeader('Authorization', 'OAuth ' + this._conn.accessToken); if (Array.isArray(extensions)) { for (const extension of extensions) { fayeClient.addExtension(extension); } } // prevent streaming API server error const dispatcher = (fayeClient as any)._dispatcher; if (dispatcher.getConnectionTypes().indexOf('callback-polling') === -1) { dispatcher.selectTransport('long-polling'); dispatcher._transport.batching = false; } return fayeClient; } /** @private **/ _getFayeClient(channelName: string) { const isGeneric = channelName.startsWith('/u/'); const clientType = isGeneric ? 'generic' : 'pushTopic'; if (!this._fayeClients[clientType]) { this._fayeClients[clientType] = this._createClient(channelName); } return this._fayeClients[clientType]; } /** * Get named topic */ topic<R extends Record = Record>(name: string): Topic<S, R> { this._topics = this._topics || {}; const topic = (this._topics[name] = this._topics[name] || new Topic<S, R>(this, name)); return topic as Topic<S, R>; } /** * Get channel for channel name */ channel(name: string) { return new Channel(this, name); } /** * Subscribe topic/channel */ subscribe(name: string, listener: Function): Subscription { const channelName = name.startsWith('/') ? name : '/topic/' + name; const fayeClient = this._getFayeClient(channelName); return fayeClient.subscribe(channelName, listener); } /** * Unsubscribe topic */ unsubscribe(name: string, subscription: Subscription) { const channelName = name.startsWith('/') ? name : '/topic/' + name; const fayeClient = this._getFayeClient(channelName); fayeClient.unsubscribe(channelName, subscription); return this; } /** * Create a Streaming client, optionally with extensions * * See Faye docs for implementation details: https://faye.jcoglan.com/browser/extensions.html * * Example usage: * * ```javascript * const jsforce = require('jsforce'); * * // Establish a Salesforce connection. (Details elided) * const conn = new jsforce.Connection({ … }); * * const fayeClient = conn.streaming.createClient(); * * const subscription = fayeClient.subscribe(channel, data => { * console.log('topic received data', data); * }); * * subscription.cancel(); * ``` * * Example with extensions, using Replay & Auth Failure extensions in a server-side Node.js app: * * ```javascript * const jsforce = require('jsforce'); * const { StreamingExtension } = require('jsforce/api/streaming'); * * // Establish a Salesforce connection. (Details elided) * const conn = new jsforce.Connection({ … }); * * const channel = "/event/My_Event__e"; * const replayId = -2; // -2 is all retained events * * const exitCallback = () => process.exit(1); * const authFailureExt = new StreamingExtension.AuthFailure(exitCallback); * * const replayExt = new StreamingExtension.Replay(channel, replayId); * * const fayeClient = conn.streaming.createClient([ * authFailureExt, * replayExt * ]); * * const subscription = fayeClient.subscribe(channel, data => { * console.log('topic received data', data); * }); * * subscription.cancel(); * ``` */ createClient(extensions: any[]) { return this._createClient(null, extensions); } } export { StreamingExtension }; /*--------------------------------------------*/ /* * Register hook in connection instantiation for dynamically adding this API module features */ registerModule('streaming', (conn) => new Streaming(conn)); export default Streaming;