From e60b354b5a40e7324d04c581d04d34eb67dded5f Mon Sep 17 00:00:00 2001 From: charles jonas Date: Fri, 9 Feb 2024 11:22:46 -0700 Subject: [PATCH] upgrading cometd to 7x --- package.json | 2 +- ts-force-gen/package-lock.json | 39 +- ts-force-gen/package.json | 6 +- ts-force/package-lock.json | 155 ++++++- ts-force/package.json | 6 +- ts-force/src/rest/restObject.ts | 2 +- ts-force/src/streaming/stream.ts | 416 +++++++++--------- ts-force/src/test/streaming/pushTopic.spec.ts | 55 +-- 8 files changed, 396 insertions(+), 285 deletions(-) diff --git a/package.json b/package.json index 5553d65..b6ef74d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-force-project", - "version": "3.4.1", + "version": "3.4.2", "description": "", "scripts": { "postinstall": "cd ts-force && npm install && npm run build && cd ../ts-force-gen && npm install", diff --git a/ts-force-gen/package-lock.json b/ts-force-gen/package-lock.json index e0836ea..e5562ea 100644 --- a/ts-force-gen/package-lock.json +++ b/ts-force-gen/package-lock.json @@ -1,12 +1,12 @@ { "name": "ts-force-gen", - "version": "3.2.0", + "version": "3.4.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ts-force-gen", - "version": "3.2.0", + "version": "3.4.1", "license": "BSD-3-Clause", "dependencies": { "@salesforce/core": "^2.12.3", @@ -36,7 +36,7 @@ "ts-node": "^7.0.1", "tslint": "^5.4.3", "tslint-config-standard": "^6.0.1", - "typescript": "^4.0.3", + "typescript": "^5.3.3", "typescript-json-schema": "^0.43.0" }, "engines": { @@ -4915,16 +4915,16 @@ } }, "node_modules/typescript": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz", - "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/typescript-json-schema": { @@ -5079,6 +5079,19 @@ "node": ">=8" } }, + "node_modules/typescript-json-schema/node_modules/typescript": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.8.tgz", + "integrity": "sha512-oz1765PN+imfz1MlZzSZPtC/tqcwsCyIYA8L47EkRnRW97ztRk83SzMiWLrnChC0vqoYxSU1fcFUDA5gV/ZiPg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/typescript-json-schema/node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -9394,9 +9407,9 @@ "dev": true }, "typescript": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz", - "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true }, "typescript-json-schema": { @@ -9512,6 +9525,12 @@ "ansi-regex": "^5.0.0" } }, + "typescript": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.8.tgz", + "integrity": "sha512-oz1765PN+imfz1MlZzSZPtC/tqcwsCyIYA8L47EkRnRW97ztRk83SzMiWLrnChC0vqoYxSU1fcFUDA5gV/ZiPg==", + "dev": true + }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", diff --git a/ts-force-gen/package.json b/ts-force-gen/package.json index 19fd79e..da7fbf1 100644 --- a/ts-force-gen/package.json +++ b/ts-force-gen/package.json @@ -1,6 +1,6 @@ { "name": "ts-force-gen", - "version": "3.4.1", + "version": "3.4.2", "description": "Code generation for ts-force", "main": "build/index.js", "typings": "build/index.d.ts", @@ -29,7 +29,7 @@ "build": "tsc -p tsconfig.build.json", "watch": "tsc -p tsconfig.build.json -w", "generate-json-schema": "npx typescript-json-schema --required ./tsconfig.json Config > ./ts-force-config.schema.json", - "prepublishOnly": "npm run clean-build && npm run generate-json-schema" + "prepublishOnly": "npm run clean-build" }, "engines": { "node": ">=4.5" @@ -50,7 +50,7 @@ "ts-node": "^7.0.1", "tslint": "^5.4.3", "tslint-config-standard": "^6.0.1", - "typescript": "^4.0.3", + "typescript": "^5.3.3", "typescript-json-schema": "^0.43.0" }, "dependencies": { diff --git a/ts-force/package-lock.json b/ts-force/package-lock.json index ea32238..51b8dee 100644 --- a/ts-force/package-lock.json +++ b/ts-force/package-lock.json @@ -1,17 +1,17 @@ { "name": "ts-force", - "version": "3.4.0", + "version": "3.4.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ts-force", - "version": "3.4.0", + "version": "3.4.2", "license": "BSD-3-Clause", "dependencies": { "@types/cometd": "^4.0.4", "axios": "^1.6.2", - "cometd": "4.0.3", + "cometd": "^7.0.12", "reflect-metadata": "^0.1.10", "tslib": "^1.6.0" }, @@ -21,7 +21,7 @@ "@types/nock": "^8.2.1", "@types/node": "^8.0.4", "chai": "^4.2.0", - "cometd-nodejs-client": "1.0.2", + "cometd-nodejs-client": "1.3.0", "dotenv": "^4.0.0", "mocha": "^10.2.0", "nock": "^9.3.3", @@ -94,6 +94,18 @@ "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", "dev": true }, + "node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -418,15 +430,21 @@ } }, "node_modules/cometd": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/cometd/-/cometd-4.0.3.tgz", - "integrity": "sha512-6oq+ay++yZNVKq4q/FXSVE9KLPGsCJqJ/nwUknsAZyPitsqvQvD1ASS/dE7A0DOu6orItjJtDIs02iRhlJhXMA==" + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/cometd/-/cometd-7.0.12.tgz", + "integrity": "sha512-SRefykEeU/TyP8ZNvOX1D1y6Sh4v7lE3MkfAXQ9h8yUiMCKS8DoWxDsiHqxEgZh5HvLAbohs1fM7g5rVexSg6Q==" }, "node_modules/cometd-nodejs-client": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cometd-nodejs-client/-/cometd-nodejs-client-1.0.2.tgz", - "integrity": "sha512-V49AZtxrEhbHRSSTYdTB0i8SGa5ip3iwbTfboVEku5/H9fMAw4GkFnzzKSBU96jCNkTq46vIuN1V5UhUIeCjMA==", - "dev": true + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/cometd-nodejs-client/-/cometd-nodejs-client-1.3.0.tgz", + "integrity": "sha512-BY2Jn6ZpL1tX3sBXs7MZB+w/lXC9H8jCyOqezl5l4zUjHKPuppmPlyR822nutyjGq5fzCL4oT8dndWsSonVpww==", + "dev": true, + "dependencies": { + "cometd": ">=3.1.2", + "http-proxy-agent": ">=2.1.0", + "https-proxy-agent": ">=2.1.0", + "ws": ">=7.2.0" + } }, "node_modules/commander": { "version": "2.11.0", @@ -629,9 +647,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -814,6 +832,32 @@ "he": "bin/he" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", + "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -2021,6 +2065,27 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -2227,6 +2292,15 @@ "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", "dev": true }, + "agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, "ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -2482,15 +2556,21 @@ } }, "cometd": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/cometd/-/cometd-4.0.3.tgz", - "integrity": "sha512-6oq+ay++yZNVKq4q/FXSVE9KLPGsCJqJ/nwUknsAZyPitsqvQvD1ASS/dE7A0DOu6orItjJtDIs02iRhlJhXMA==" + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/cometd/-/cometd-7.0.12.tgz", + "integrity": "sha512-SRefykEeU/TyP8ZNvOX1D1y6Sh4v7lE3MkfAXQ9h8yUiMCKS8DoWxDsiHqxEgZh5HvLAbohs1fM7g5rVexSg6Q==" }, "cometd-nodejs-client": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cometd-nodejs-client/-/cometd-nodejs-client-1.0.2.tgz", - "integrity": "sha512-V49AZtxrEhbHRSSTYdTB0i8SGa5ip3iwbTfboVEku5/H9fMAw4GkFnzzKSBU96jCNkTq46vIuN1V5UhUIeCjMA==", - "dev": true + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/cometd-nodejs-client/-/cometd-nodejs-client-1.3.0.tgz", + "integrity": "sha512-BY2Jn6ZpL1tX3sBXs7MZB+w/lXC9H8jCyOqezl5l4zUjHKPuppmPlyR822nutyjGq5fzCL4oT8dndWsSonVpww==", + "dev": true, + "requires": { + "cometd": ">=3.1.2", + "http-proxy-agent": ">=2.1.0", + "https-proxy-agent": ">=2.1.0", + "ws": ">=7.2.0" + } }, "commander": { "version": "2.11.0", @@ -2644,9 +2724,9 @@ "dev": true }, "follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" }, "form-data": { "version": "4.0.0", @@ -2763,6 +2843,26 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "http-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", + "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3656,6 +3756,13 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dev": true, + "requires": {} + }, "y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/ts-force/package.json b/ts-force/package.json index 412021a..f4b55b6 100644 --- a/ts-force/package.json +++ b/ts-force/package.json @@ -1,6 +1,6 @@ { "name": "ts-force", - "version": "3.4.1", + "version": "3.4.2", "description": "a typescript client for connecting with salesforce APIs", "main": "build/index.js", "typings": "build/index.d.ts", @@ -39,7 +39,7 @@ "@types/nock": "^8.2.1", "@types/node": "^8.0.4", "chai": "^4.2.0", - "cometd-nodejs-client": "1.0.2", + "cometd-nodejs-client": "1.3.0", "dotenv": "^4.0.0", "mocha": "^10.2.0", "nock": "^9.3.3", @@ -52,7 +52,7 @@ "dependencies": { "@types/cometd": "^4.0.4", "axios": "^1.6.2", - "cometd": "4.0.3", + "cometd": "^7.0.12", "reflect-metadata": "^0.1.10", "tslib": "^1.6.0" } diff --git a/ts-force/src/rest/restObject.ts b/ts-force/src/rest/restObject.ts index e9c8b5f..afc73e3 100644 --- a/ts-force/src/rest/restObject.ts +++ b/ts-force/src/rest/restObject.ts @@ -57,7 +57,7 @@ export abstract class RestObject extends SObject { protected initObject(fields?: Partial>) { if (fields) { if (fields instanceof RestObject) { - this._modified = fields._modified; + this._modified = fields._modified ?? new Set(); } else { this.setModified(Object.keys(fields) as any); } diff --git a/ts-force/src/streaming/stream.ts b/ts-force/src/streaming/stream.ts index fda8e2b..3bf59ce 100644 --- a/ts-force/src/streaming/stream.ts +++ b/ts-force/src/streaming/stream.ts @@ -5,244 +5,240 @@ import { SObject } from '../rest/sObject'; import { Omit } from '../types'; export class Streaming { - - private listener: CometD; - private subscribers: Map; - - /** - * Creates an instance of Streaming class. Used to listen to PushTopic and Platform Events - * @param {Rest} [client] Optional client to use instead of the default connection - * @memberof Streaming - */ - constructor(client?: Rest) { - client = client || new Rest(); - - this.subscribers = new Map(); - this.listener = new CometD(); - this.listener.configure({ - url: `${client.config.instanceUrl}/cometd/${client.config.version.toFixed(1)}/`, - requestHeaders: { Authorization: `OAuth ${client.config.accessToken}` }, - appendMessageTypeToURL: false - }); - } - - - /** - * Method to connect to salesforce. Call before attempting to subscribe to event events or topics - * - * @memberof Streaming - */ - public connect = () => { - return new Promise((resolve, reject) => { - this.listener.handshake((resp) => { - if (resp.successful) { - resolve(); - } else { - reject(resp); - } - }); - }); - } - - /** - * Removes a transport from the cometd connection. - * See: https://docs.cometd.org/current/reference/#_javascript_transports_unregistering - * @param {('websocket' | 'long-polling' | 'callback-polling')} transport - * @memberof Streaming - */ - public unregisterTransport(transport: 'websocket' | 'long-polling' | 'callback-polling') { - this.listener.unregisterTransport(transport); - } - - /** - * General purpose method to subscribe to any uri. - * Use `subscribeToTopic`, `subscribeToTopicMapped` & `subscribeToEvent` for most use cases - * - * @param {string} channel the relative uri to subscribe to. EX: '/topic/abc' - * @param {(message: any) => void} onEvent Callback handler to process received events - * @returns {Promise} A cometd SubscriberHandle. It's recommended to use - * @memberof StreamClient - */ - public _subscribe(channel: string, onEvent: (message: any) => void): Promise { - return new Promise((resolve, reject) => { - if (this.listener.isDisconnected()) { - reject('Streaming client is not connected! Must run connect first!'); - } - let subscriber = this.listener.subscribe( - channel, - onEvent, - (m) => { - if (m.successful) { - this.subscribers.set(channel, subscriber); - resolve(subscriber); - } else { - reject(m); - } - } - ); - }); - } - - /** - * Method to unsubscribe from any subscription. - * @param {string} channel - * @param {('topic' | 'event')} [type] Optional parameter to help build channel - * @returns {Promise} - * @memberof StreamClient - */ - public unsubscribe(channel: string, type?: 'topic' | 'event'): Promise { - channel = type ? `/${type}/${channel}` : channel; - return new Promise((resolve, reject) => { - if (this.subscribers.has(channel)) { - let subscriber = this.subscribers.get(channel); - this.listener.unsubscribe(subscriber, (m) => { - if (m.successful) { - resolve(); - } else { - reject(m); - } - }); - } else { - reject(`No subscriber for ${channel} found`); - } + private listener: CometD; + private subscribers: Map; + + /** + * Creates an instance of Streaming class. Used to listen to PushTopic and Platform Events + * @param {Rest} [client] Optional client to use instead of the default connection + * @memberof Streaming + */ + constructor(client?: Rest) { + client = client || new Rest(); + + this.subscribers = new Map(); + this.listener = new CometD(); + this.listener.websocketEnabled = false; + this.listener.configure({ + url: `${client.config.instanceUrl}/cometd/${client.config.version.toFixed(1)}/`, + requestHeaders: { Authorization: `OAuth ${client.config.accessToken}` }, + appendMessageTypeToURL: false, + }); + } + + /** + * Method to connect to salesforce. Call before attempting to subscribe to event events or topics + * + * @memberof Streaming + */ + public connect = () => { + return new Promise((resolve, reject) => { + this.listener.handshake((resp) => { + if (resp.successful) { + resolve(); + } else { + reject(resp); + } + }); + }); + }; + + /** + * Removes a transport from the cometd connection. + * See: https://docs.cometd.org/current/reference/#_javascript_transports_unregistering + * @param {('websocket' | 'long-polling' | 'callback-polling')} transport + * @memberof Streaming + */ + public unregisterTransport(transport: 'websocket' | 'long-polling' | 'callback-polling') { + this.listener.unregisterTransport(transport); + } + + /** + * General purpose method to subscribe to any uri. + * Use `subscribeToTopic`, `subscribeToTopicMapped` & `subscribeToEvent` for most use cases + * + * @param {string} channel the relative uri to subscribe to. EX: '/topic/abc' + * @param {(message: any) => void} onEvent Callback handler to process received events + * @returns {Promise} A cometd SubscriberHandle. It's recommended to use + * @memberof StreamClient + */ + public _subscribe(channel: string, onEvent: (message: any) => void): Promise { + return new Promise((resolve, reject) => { + if (this.listener.isDisconnected()) { + reject('Streaming client is not connected! Must run connect first!'); + } + let subscriber = this.listener.subscribe(channel, onEvent, (m) => { + if (m.successful) { + this.subscribers.set(channel, subscriber); + resolve(subscriber); + } else { + reject(m); + } + }); + }); + } + + /** + * Method to unsubscribe from any subscription. + * @param {string} channel + * @param {('topic' | 'event')} [type] Optional parameter to help build channel + * @returns {Promise} + * @memberof StreamClient + */ + public unsubscribe(channel: string, type?: 'topic' | 'event'): Promise { + channel = type ? `/${type}/${channel}` : channel; + return new Promise((resolve, reject) => { + if (this.subscribers.has(channel)) { + let subscriber = this.subscribers.get(channel); + this.listener.unsubscribe(subscriber, (m) => { + if (m.successful) { + resolve(); + } else { + reject(m); + } }); - } - - /** - * Method to subscribe to a platform events - * @template T type of the event `payload` - * @param {string} event The name of the PlatformEvent to subscribe to. EG: `My_Event__e` (do not include `/event/`) - * @param {(m: PlatformEvent) => void} onEvent - * @returns {Promise} - * @memberof Streaming - */ - public subscribeToEvent(event: string, onEvent: (m: PlatformEvent) => void): Promise { - return this._subscribe(`/event/${event}`, onEvent); - } - - /** - * Method to subscribe to a push topic - * See `subscribeToTopicMapped` to automatically map your response to a generated SObject class - * @template T The signature of of the event `data.sobject` - * @param {string} topic The name of the PushTopic to subscribe to. EG: `MyTopic` (do not include `/topic/`) - * @param {(m: TopicMessage) => void} onEvent Your event handler - * @returns {Promise} - * @memberof Streaming - */ - public subscribeToTopic(topic: string, onEvent: (m: TopicMessage) => void): Promise { - return this._subscribe(`/topic/${topic}`, onEvent); - } - - /** - * Method to subscribe to a PushTopic and parse event messages directly to a generated SObject type - * - * @template T The SObject type. Implied from `sObjectType` param - * @param {SObjectStatic} sObjectType The static instance of the SObject type to map. EG: `Account` - * @param {string} topic The name of the PushTopic to subscribe to. EG: `MyTopic` (do not include `/topic/`) - * @param {(event: SObjectTopicMessage) => void} onEvent Your event handler. Payload will be parsed to your `sObjectType` - * @returns {Promise} - * @memberof Streaming - * - * Example: - * - * ```typescript - *await stream.subscribeToTopicMapped( - * Account, - * topic.name, - * e => { - * let acc: Account = e.data.sObject; - * console.log(acc.annualRevenue); - * } - * ); - * ``` - */ - public subscribeToTopicMapped(sObjectType: SObjectStatic, topic: string, onEvent: (event: SObjectTopicMessage) => void): Promise { - return this.subscribeToTopic( - topic, - (m: TopicMessage) => { - let mappedEvent: SObjectTopicMessage = { - data: { - event: m.data.event, - sObject: sObjectType.fromSFObject(m.data.sobject as SObject) - }, - channel: m.channel, - clientId: m.clientId - }; - return onEvent(mappedEvent); - } - ); - } - - /** - * Disconnects the streaming client. Will unsubscribe for all active subscriptions - * - * @memberof Streaming - */ - public disconnect = () => { - return new Promise((resolve, reject) => { - this.listener.disconnect(m => { - if (m.successful) { - resolve(); - } else { - reject(m); - } - }); - }); - } - /** - * Returns `true` if the client is currently connected with salesforce - * - * @memberof Streaming - */ - public isConnected = () => { - return !this.listener.isDisconnected(); - } + } else { + reject(`No subscriber for ${channel} found`); + } + }); + } + + /** + * Method to subscribe to a platform events + * @template T type of the event `payload` + * @param {string} event The name of the PlatformEvent to subscribe to. EG: `My_Event__e` (do not include `/event/`) + * @param {(m: PlatformEvent) => void} onEvent + * @returns {Promise} + * @memberof Streaming + */ + public subscribeToEvent(event: string, onEvent: (m: PlatformEvent) => void): Promise { + return this._subscribe(`/event/${event}`, onEvent); + } + + /** + * Method to subscribe to a push topic + * See `subscribeToTopicMapped` to automatically map your response to a generated SObject class + * @template T The signature of of the event `data.sobject` + * @param {string} topic The name of the PushTopic to subscribe to. EG: `MyTopic` (do not include `/topic/`) + * @param {(m: TopicMessage) => void} onEvent Your event handler + * @returns {Promise} + * @memberof Streaming + */ + public subscribeToTopic(topic: string, onEvent: (m: TopicMessage) => void): Promise { + return this._subscribe(`/topic/${topic}`, onEvent); + } + + /** + * Method to subscribe to a PushTopic and parse event messages directly to a generated SObject type + * + * @template T The SObject type. Implied from `sObjectType` param + * @param {SObjectStatic} sObjectType The static instance of the SObject type to map. EG: `Account` + * @param {string} topic The name of the PushTopic to subscribe to. EG: `MyTopic` (do not include `/topic/`) + * @param {(event: SObjectTopicMessage) => void} onEvent Your event handler. Payload will be parsed to your `sObjectType` + * @returns {Promise} + * @memberof Streaming + * + * Example: + * + * ```typescript + *await stream.subscribeToTopicMapped( + * Account, + * topic.name, + * e => { + * let acc: Account = e.data.sObject; + * console.log(acc.annualRevenue); + * } + * ); + * ``` + */ + public subscribeToTopicMapped( + sObjectType: SObjectStatic, + topic: string, + onEvent: (event: SObjectTopicMessage) => void + ): Promise { + return this.subscribeToTopic(topic, (m: TopicMessage) => { + let mappedEvent: SObjectTopicMessage = { + data: { + event: m.data.event, + sObject: sObjectType.fromSFObject(m.data.sobject as SObject), + }, + channel: m.channel, + clientId: m.clientId, + }; + return onEvent(mappedEvent); + }); + } + + /** + * Disconnects the streaming client. Will unsubscribe for all active subscriptions + * + * @memberof Streaming + */ + public disconnect = () => { + return new Promise((resolve, reject) => { + this.listener.disconnect((m) => { + if (m.successful) { + resolve(); + } else { + reject(m); + } + }); + }); + }; + /** + * Returns `true` if the client is currently connected with salesforce + * + * @memberof Streaming + */ + public isConnected = () => { + return !this.listener.isDisconnected(); + }; } /*=== MESSAGE TYPES ===*/ // Push Topic export interface SObjectTopicMessage extends Omit, 'data'> { - data: { - event: Event; - sObject: T; - }; + data: { + event: Event; + sObject: T; + }; } export interface TopicMessage { - clientId: string; - data: Data; - channel: string; + clientId: string; + data: Data; + channel: string; } export interface Event { - createdDate: Date; - replayId: number; - type: TopicEventType; + createdDate: Date; + replayId: number; + type: TopicEventType; } export type TopicEventType = 'created' | 'updated' | 'deleted' | 'undeleted'; export interface TopicSObject { - Id: string; + Id: string; } export interface Data { - event: Event; - sobject: T; + event: Event; + sobject: T; } // PLATFORM EVENTS export interface PlatformEvent { - data: PlatformEventData; - channel: string; + data: PlatformEventData; + channel: string; } export interface PlatformEventInfo { - replayId: number; + replayId: number; } export interface PlatformEventData { - schema: string; - payload: T; - event: PlatformEventInfo; + schema: string; + payload: T; + event: PlatformEventInfo; } diff --git a/ts-force/src/test/streaming/pushTopic.spec.ts b/ts-force/src/test/streaming/pushTopic.spec.ts index 54fd2df..ad53e6c 100644 --- a/ts-force/src/test/streaming/pushTopic.spec.ts +++ b/ts-force/src/test/streaming/pushTopic.spec.ts @@ -6,8 +6,6 @@ import { buildQuery } from '../../qry'; import { Account, PushTopic } from '../assets/sobs'; import { createDefaultClient } from '../helper'; - - const TEST_ACC_NAME = 'testing push topic'; describe('Streaming API', () => { @@ -42,17 +40,15 @@ describe('Streaming API', () => { expect(stream.isConnected()).to.equal(true); // sObject mapping - await stream.subscribeToTopic<{ Id: string, Name: string }>( - topic.name, - e => { - expect(e.data.sobject.Name).to.equal(TEST_ACC_NAME); - stream.unsubscribe(topic.name, 'topic') - .then(() => topic.delete()) - .then(() => stream.disconnect()) - .then(() => resolve()) - .catch(e => reject(e)); - } - ); + await stream.subscribeToTopic<{ Id: string; Name: string }>(topic.name, (e) => { + expect(e.data.sobject.Name).to.equal(TEST_ACC_NAME); + stream + .unsubscribe(topic.name, 'topic') + .then(() => topic.delete()) + .then(() => stream.disconnect()) + .then(() => resolve()) + .catch((e) => reject(e)); + }); let acc = new Account({ name: TEST_ACC_NAME }); await acc.insert(); @@ -75,18 +71,15 @@ describe('Streaming API', () => { expect(stream.isConnected()).to.equal(true); // sObject mapping - await stream.subscribeToTopicMapped( - Account, - topic.name, - e => { - expect(e.data.sObject.name).to.equal(TEST_ACC_NAME); - stream.unsubscribe(topic.name, 'topic') - .then(() => topic.delete()) - .then(() => stream.disconnect()) - .then(() => resolve()) - .catch(e => reject(e)); - } - ); + await stream.subscribeToTopicMapped(Account, topic.name, (e) => { + expect(e.data.sObject.name).to.equal(TEST_ACC_NAME); + stream + .unsubscribe(topic.name, 'topic') + .then(() => topic.delete()) + .then(() => stream.disconnect()) + .then(() => resolve()) + .catch((e) => reject(e)); + }); let acc = new Account({ name: TEST_ACC_NAME }); await acc.insert(); @@ -106,14 +99,10 @@ async function getOrCreateTestTopic(topicName: string) { notifyForOperationCreate: true, description: 'for unit test', apiVersion: DEFAULT_CONFIG.version, - query: buildQuery(Account, f => ( - { - select: [ - ...f.select('id', 'name', 'active') - ] - // where: [{ field: f.select('name'), val: TEST_ACC_NAME }] - } - )) + query: buildQuery(Account, (f) => ({ + select: [...f.select('id', 'name', 'active')], + // where: [{ field: f.select('name'), val: TEST_ACC_NAME }] + })), }); await topic.insert(); }