diff --git a/lib/StripeEmitter.js b/lib/StripeEmitter.js new file mode 100644 index 0000000000..03e5ec95dd --- /dev/null +++ b/lib/StripeEmitter.js @@ -0,0 +1,44 @@ +"use strict"; +/** + * @private + * (For internal use in stripe-node.) + * Wrapper around the Event Web API. + */ +class _StripeEvent extends Event { + constructor(eventName, data) { + super(eventName); + this.data = data; + } +} +/** Minimal EventEmitter wrapper around EventTarget. */ +class StripeEmitter { + constructor() { + this.eventTarget = new EventTarget(); + this.listenerMapping = new Map(); + } + on(eventName, listener) { + const listenerWrapper = (event) => { + listener(event.data); + }; + this.listenerMapping.set(listener, listenerWrapper); + return this.eventTarget.addEventListener(eventName, listenerWrapper); + } + removeListener(eventName, listener) { + const listenerWrapper = this.listenerMapping.get(listener); + this.listenerMapping.delete(listener); + return this.eventTarget.removeEventListener(eventName, listenerWrapper); + } + once(eventName, listener) { + const listenerWrapper = (event) => { + listener(event.data); + }; + this.listenerMapping.set(listener, listenerWrapper); + return this.eventTarget.addEventListener(eventName, listenerWrapper, { + once: true, + }); + } + emit(eventName, data) { + return this.eventTarget.dispatchEvent(new _StripeEvent(eventName, data)); + } +} +module.exports = StripeEmitter; diff --git a/lib/StripeResource.js b/lib/StripeResource.js index 2b94b382dc..8039608d33 100644 --- a/lib/StripeResource.js +++ b/lib/StripeResource.js @@ -133,6 +133,7 @@ StripeResource.prototype = { }, _makeRequest(requestArgs, spec, overrideData) { return new Promise((resolve, reject) => { + var _a; let opts; try { opts = this._getRequestOpts(requestArgs, spec, overrideData); @@ -158,7 +159,7 @@ StripeResource.prototype = { utils.stringifyRequestData(opts.queryData), ].join(''); const { headers, settings } = opts; - this._stripe._requestSender._request(opts.requestMethod, opts.host, path, opts.bodyData, opts.auth, { headers, settings, streaming: opts.streaming }, requestCallback, this.requestDataProcessor); + this._stripe._requestSender._request(opts.requestMethod, opts.host, path, opts.bodyData, opts.auth, { headers, settings, streaming: opts.streaming }, requestCallback, (_a = this.requestDataProcessor) === null || _a === void 0 ? void 0 : _a.bind(this)); }); }, }; diff --git a/lib/multipart.js b/lib/multipart.js index a2ac1044ca..51ce8b2f4c 100644 --- a/lib/multipart.js +++ b/lib/multipart.js @@ -1,9 +1,5 @@ "use strict"; const utils = require("./utils"); -const _Error = require("./Error"); -const { StripeError } = _Error; -class StreamProcessingError extends StripeError { -} // Method for formatting HTTP body for the multipart/form-data specification // Mostly taken from Fermata.js // https://github.com/natevw/fermata/blob/5d9732a33d776ce925013a265935facd1626cc88/fermata.js#L315-L343 @@ -44,38 +40,19 @@ const multipartDataGenerator = (method, data, headers) => { push(`--${segno}--`); return buffer; }; -const streamProcessor = (method, data, headers, callback) => { - const bufferArray = []; - data.file.data - .on('data', (line) => { - bufferArray.push(line); - }) - .once('end', () => { - // @ts-ignore - const bufferData = Object.assign({}, data); - bufferData.file.data = utils.concat(bufferArray); - const buffer = multipartDataGenerator(method, bufferData, headers); - callback(null, buffer); - }) - .on('error', (err) => { - callback(new StreamProcessingError({ - message: 'An error occurred while attempting to process the file for upload.', - detail: err, - }), null); - }); -}; -const multipartRequestDataProcessor = (method, data, headers, callback) => { +function multipartRequestDataProcessor(method, data, headers, callback) { data = data || {}; if (method !== 'POST') { return callback(null, utils.stringifyRequestData(data)); } - const isStream = utils.checkForStream(data); - if (isStream) { - return streamProcessor(method, data, headers, callback); - } - const buffer = multipartDataGenerator(method, data, headers); - return callback(null, buffer); -}; + this._stripe._platformFunctions + .tryBufferData(data) + .then((bufferedData) => { + const buffer = multipartDataGenerator(method, bufferedData, headers); + return callback(null, buffer); + }) + .catch((err) => callback(err, null)); +} module.exports = { multipartRequestDataProcessor: multipartRequestDataProcessor, }; diff --git a/lib/platform/NodePlatformFunctions.js b/lib/platform/NodePlatformFunctions.js index 57205290dc..af07b0d171 100644 --- a/lib/platform/NodePlatformFunctions.js +++ b/lib/platform/NodePlatformFunctions.js @@ -1,10 +1,16 @@ "use strict"; const crypto = require("crypto"); -const DefaultPlatformFunctions = require("./DefaultPlatformFunctions"); +const EventEmitter = require("events"); +const _Error = require("../Error"); +const StripeError = _Error.StripeError; +const utils = require("../utils"); +const PlatformFunctions = require("./PlatformFunctions"); +class StreamProcessingError extends StripeError { +} /** - * Specializes DefaultPlatformFunctions using APIs available in Node.js. + * Specializes WebPlatformFunctions using APIs available in Node.js. */ -class NodePlatformFunctions extends DefaultPlatformFunctions { +class NodePlatformFunctions extends PlatformFunctions { constructor() { super(); this._exec = require('child_process').exec; @@ -68,5 +74,33 @@ class NodePlatformFunctions extends DefaultPlatformFunctions { } return super.secureCompare(a, b); } + createEmitter() { + return new EventEmitter(); + } + /** @override */ + tryBufferData(data) { + if (!(data.file.data instanceof EventEmitter)) { + return Promise.resolve(data); + } + const bufferArray = []; + return new Promise((resolve, reject) => { + data.file.data + .on('data', (line) => { + bufferArray.push(line); + }) + .once('end', () => { + // @ts-ignore + const bufferData = Object.assign({}, data); + bufferData.file.data = utils.concat(bufferArray); + resolve(bufferData); + }) + .on('error', (err) => { + reject(new StreamProcessingError({ + message: 'An error occurred while attempting to process the file for upload.', + detail: err, + })); + }); + }); + } } module.exports = NodePlatformFunctions; diff --git a/lib/platform/DefaultPlatformFunctions.js b/lib/platform/PlatformFunctions.js similarity index 67% rename from lib/platform/DefaultPlatformFunctions.js rename to lib/platform/PlatformFunctions.js index 30484e9162..524c83248a 100644 --- a/lib/platform/DefaultPlatformFunctions.js +++ b/lib/platform/PlatformFunctions.js @@ -3,12 +3,12 @@ * Interface encapsulating various utility functions whose * implementations depend on the platform / JS runtime. */ -class DefaultPlatformFunctions { +class PlatformFunctions { /** * Gets uname with Node's built-in `exec` function, if available. */ getUname() { - return Promise.resolve(null); + throw new Error('getUname not implemented.'); } /** * Generates a v4 UUID. See https://stackoverflow.com/a/2117523 @@ -35,5 +35,18 @@ class DefaultPlatformFunctions { } return result === 0; } + /** + * Creates an event emitter. + */ + createEmitter() { + throw new Error('createEmitter not implemented.'); + } + /** + * Checks if the request data is a stream. If so, read the entire stream + * to a buffer and return the buffer. + */ + tryBufferData(data) { + throw new Error('tryBufferData not implemented.'); + } } -module.exports = DefaultPlatformFunctions; +module.exports = PlatformFunctions; diff --git a/lib/platform/WebPlatformFunctions.js b/lib/platform/WebPlatformFunctions.js new file mode 100644 index 0000000000..384a4bc734 --- /dev/null +++ b/lib/platform/WebPlatformFunctions.js @@ -0,0 +1,24 @@ +"use strict"; +const StripeEmitter = require("../StripeEmitter"); +const PlatformFunctions = require("./PlatformFunctions"); +/** + * Specializes WebPlatformFunctions using APIs available in Web workers. + */ +class WebPlatformFunctions extends PlatformFunctions { + /** @override */ + getUname() { + return Promise.resolve(null); + } + /** @override */ + createEmitter() { + return new StripeEmitter(); + } + /** @override */ + tryBufferData(data) { + if (data.file.data instanceof ReadableStream) { + throw new Error('Uploading a file as a stream is not supported in non-Node environments. Please open or upvote an issue at github.com/stripe/stripe-node if you use this, detailing your use-case.'); + } + return Promise.resolve(data); + } +} +module.exports = WebPlatformFunctions; diff --git a/lib/stripe.common.js b/lib/stripe.common.js index 81cf084b3c..b52b5e730f 100644 --- a/lib/stripe.common.js +++ b/lib/stripe.common.js @@ -35,17 +35,16 @@ const HttpClient = require("./net/HttpClient"); Stripe.HttpClient = HttpClient.HttpClient; Stripe.HttpClientResponse = HttpClient.HttpClientResponse; const CryptoProvider = require("./crypto/CryptoProvider"); -const EventEmitter = require("events"); Stripe.CryptoProvider = CryptoProvider; -const DefaultPlatformFunctions = require("./platform/DefaultPlatformFunctions"); -Stripe._platformFunctions = new DefaultPlatformFunctions(); +// @ts-ignore +Stripe._platformFunctions = null; function Stripe(key, config = {}) { if (!(this instanceof Stripe)) { return new Stripe(key, config); } const props = this._getPropsFromConfig(config); Object.defineProperty(this, '_emitter', { - value: new EventEmitter(), + value: Stripe._platformFunctions.createEmitter(), enumerable: false, configurable: false, writable: false, diff --git a/lib/stripe.worker.js b/lib/stripe.worker.js index c8d4e1c124..c7913da6ba 100644 --- a/lib/stripe.worker.js +++ b/lib/stripe.worker.js @@ -1,6 +1,9 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const Stripe = require("./stripe.common"); +const WebPlatformFunctions = require("./platform/WebPlatformFunctions"); +Stripe._platformFunctions = new WebPlatformFunctions(); +Stripe.webhooks._platformFunctions = Stripe._platformFunctions; Stripe.createHttpClient = Stripe.createFetchHttpClient; Stripe.webhooks._createCryptoProvider = Stripe.createSubtleCryptoProvider; module.exports = Stripe; diff --git a/lib/utils.js b/lib/utils.js index a92eb3970b..3c4f23f7e7 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,5 +1,4 @@ "use strict"; -const EventEmitter = require('events').EventEmitter; const qs = require('qs'); const OPTIONS_KEYS = [ 'apiKey', @@ -191,16 +190,6 @@ const utils = { .map((text) => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase()) .join('-'); }, - /** - * Determine if file data is a derivative of EventEmitter class. - * https://nodejs.org/api/events.html#events_events - */ - checkForStream: (obj) => { - if (obj.file && obj.file.data) { - return obj.file.data instanceof EventEmitter; - } - return false; - }, callbackifyPromiseWithTimeout: (promise, callback) => { if (callback) { // Ensure callback is called outside of promise stack. diff --git a/src/Error.ts b/src/Error.ts index 6ecf9159fa..c1d13eb4d6 100644 --- a/src/Error.ts +++ b/src/Error.ts @@ -14,7 +14,7 @@ class StripeError extends Error { readonly code?: string; readonly doc_url?: string; readonly param?: string; - readonly detail?: string; + readonly detail?: string | Error; readonly statusCode?: number; readonly charge?: string; readonly decline_code?: string; diff --git a/src/StripeEmitter.ts b/src/StripeEmitter.ts new file mode 100644 index 0000000000..bc5ee84223 --- /dev/null +++ b/src/StripeEmitter.ts @@ -0,0 +1,56 @@ +/** + * @private + * (For internal use in stripe-node.) + * Wrapper around the Event Web API. + */ +class _StripeEvent extends Event { + data?: RequestEvent | ResponseEvent; + constructor(eventName: string, data: any) { + super(eventName); + this.data = data; + } +} + +type Listener = (...args: any[]) => any; +type ListenerWrapper = (event: _StripeEvent) => void; + +/** Minimal EventEmitter wrapper around EventTarget. */ +class StripeEmitter { + eventTarget: EventTarget; + listenerMapping: Map; + + constructor() { + this.eventTarget = new EventTarget(); + this.listenerMapping = new Map(); + } + + on(eventName: string, listener: Listener): void { + const listenerWrapper: ListenerWrapper = (event: _StripeEvent): void => { + listener(event.data); + }; + this.listenerMapping.set(listener, listenerWrapper); + return this.eventTarget.addEventListener(eventName, listenerWrapper); + } + + removeListener(eventName: string, listener: Listener): void { + const listenerWrapper = this.listenerMapping.get(listener); + this.listenerMapping.delete(listener); + return this.eventTarget.removeEventListener(eventName, listenerWrapper!); + } + + once(eventName: string, listener: Listener): void { + const listenerWrapper: ListenerWrapper = (event: _StripeEvent): void => { + listener(event.data); + }; + this.listenerMapping.set(listener, listenerWrapper); + return this.eventTarget.addEventListener(eventName, listenerWrapper, { + once: true, + }); + } + + emit(eventName: string, data: RequestEvent | ResponseEvent): boolean { + return this.eventTarget.dispatchEvent(new _StripeEvent(eventName, data)); + } +} + +export = StripeEmitter; diff --git a/src/StripeResource.ts b/src/StripeResource.ts index da2bac6374..ed657b736d 100644 --- a/src/StripeResource.ts +++ b/src/StripeResource.ts @@ -222,7 +222,7 @@ StripeResource.prototype = { opts.auth, {headers, settings, streaming: opts.streaming}, requestCallback, - this.requestDataProcessor + this.requestDataProcessor?.bind(this) ); }); }, diff --git a/src/Types.d.ts b/src/Types.d.ts index b36a034650..e730c0bc26 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -31,7 +31,7 @@ type RequestCallback = ( response?: any ) => RequestCallbackReturn; type RequestCallbackReturn = any; -type RequestData = Record; +type RequestData = Record; type RequestEvent = { api_version?: string; account?: string; @@ -159,7 +159,7 @@ type StripeObject = { _requestSender: RequestSender; _getPropsFromConfig: (config: Record) => UserProvidedConfig; _clientId?: string; - _platformFunctions: import('./platform/DefaultPlatformFunctions'); + _platformFunctions: import('./platform/PlatformFunctions'); }; type RequestSender = { _request( @@ -170,7 +170,7 @@ type RequestSender = { auth: string | null, options: RequestOptions, callback: RequestCallback, - requestDataProcessor: RequestDataProcessor | null + requestDataProcessor: RequestDataProcessor | undefined ): void; }; type StripeRawError = { @@ -183,7 +183,7 @@ type StripeRawError = { doc_url?: string; decline_code?: string; param?: string; - detail?: string; + detail?: string | Error; charge?: string; payment_method_type?: string; payment_intent?: any; diff --git a/src/Webhooks.ts b/src/Webhooks.ts index dae6e7243d..2f20765c13 100644 --- a/src/Webhooks.ts +++ b/src/Webhooks.ts @@ -1,6 +1,5 @@ -import utils = require('./utils'); import _Error = require('./Error'); -import DefaultPlatformFunctions = require('./platform/DefaultPlatformFunctions'); +import PlatformFunctions = require('./platform/PlatformFunctions'); import CryptoProvider = require('./crypto/CryptoProvider'); const {StripeError, StripeSignatureVerificationError} = _Error; @@ -60,7 +59,7 @@ type WebhookObject = { ) => Promise; generateTestHeaderString: (opts: WebhookTestHeaderOptions) => string; _createCryptoProvider: () => CryptoProvider | null; - _platformFunctions: DefaultPlatformFunctions | null; + _platformFunctions: PlatformFunctions | null; }; const Webhook: WebhookObject = { diff --git a/src/multipart.ts b/src/multipart.ts index e261c0c297..29abbbb972 100644 --- a/src/multipart.ts +++ b/src/multipart.ts @@ -1,8 +1,6 @@ import utils = require('./utils'); import _Error = require('./Error'); -const {StripeError} = _Error; -class StreamProcessingError extends StripeError {} type MultipartCallbackReturn = any; type MultipartCallback = ( error: Error | null, @@ -70,56 +68,27 @@ const multipartDataGenerator = ( return buffer; }; -const streamProcessor = ( +function multipartRequestDataProcessor( + this: StripeResourceObject, method: string, - data: StreamingFile, + data: RequestData, headers: RequestHeaders, callback: MultipartCallback -): void => { - const bufferArray: Array = []; - data.file.data - .on('data', (line: Uint8Array) => { - bufferArray.push(line); - }) - .once('end', () => { - // @ts-ignore - const bufferData: BufferedFile = Object.assign({}, data); - bufferData.file.data = utils.concat(bufferArray); - const buffer = multipartDataGenerator(method, bufferData, headers); - callback(null, buffer); - }) - .on('error', (err) => { - callback( - new StreamProcessingError({ - message: - 'An error occurred while attempting to process the file for upload.', - detail: err, - }), - null - ); - }); -}; - -const multipartRequestDataProcessor = ( - method: string, - data: MultipartRequestData, - headers: RequestHeaders, - callback: MultipartCallback -): MultipartCallbackReturn => { +): MultipartCallbackReturn { data = data || {}; if (method !== 'POST') { return callback(null, utils.stringifyRequestData(data)); } - const isStream = utils.checkForStream(data); - if (isStream) { - return streamProcessor(method, data as StreamingFile, headers, callback); - } - - const buffer = multipartDataGenerator(method, data, headers); - return callback(null, buffer); -}; + this._stripe._platformFunctions + .tryBufferData(data) + .then((bufferedData: MultipartRequestData) => { + const buffer = multipartDataGenerator(method, bufferedData, headers); + return callback(null, buffer); + }) + .catch((err: Error) => callback(err, null)); +} export = { multipartRequestDataProcessor: multipartRequestDataProcessor, diff --git a/src/platform/NodePlatformFunctions.ts b/src/platform/NodePlatformFunctions.ts index f261a57aea..64ed7f9ad3 100644 --- a/src/platform/NodePlatformFunctions.ts +++ b/src/platform/NodePlatformFunctions.ts @@ -1,12 +1,16 @@ -import {rejects} from 'assert'; import crypto = require('crypto'); +import EventEmitter = require('events'); +import _Error = require('../Error'); +const StripeError = _Error.StripeError; +import utils = require('../utils'); +import PlatformFunctions = require('./PlatformFunctions'); -import DefaultPlatformFunctions = require('./DefaultPlatformFunctions'); +class StreamProcessingError extends StripeError {} /** - * Specializes DefaultPlatformFunctions using APIs available in Node.js. + * Specializes WebPlatformFunctions using APIs available in Node.js. */ -class NodePlatformFunctions extends DefaultPlatformFunctions { +class NodePlatformFunctions extends PlatformFunctions { /** For mocking in tests */ _exec: any; _UNAME_CACHE: Promise | null; @@ -15,7 +19,7 @@ class NodePlatformFunctions extends DefaultPlatformFunctions { super(); this._exec = require('child_process').exec; - this._UNAME_CACHE = null as Promise | null; + this._UNAME_CACHE = null; } /** @override */ @@ -80,6 +84,41 @@ class NodePlatformFunctions extends DefaultPlatformFunctions { return super.secureCompare(a, b); } + + createEmitter(): EventEmitter { + return new EventEmitter(); + } + + /** @override */ + tryBufferData( + data: MultipartRequestData + ): Promise { + if (!(data.file.data instanceof EventEmitter)) { + return Promise.resolve(data); + } + const bufferArray: Array = []; + return new Promise((resolve, reject) => { + data.file.data + .on('data', (line: Uint8Array) => { + bufferArray.push(line); + }) + .once('end', () => { + // @ts-ignore + const bufferData: BufferedFile = Object.assign({}, data); + bufferData.file.data = utils.concat(bufferArray); + resolve(bufferData); + }) + .on('error', (err: Error) => { + reject( + new StreamProcessingError({ + message: + 'An error occurred while attempting to process the file for upload.', + detail: err, + }) + ); + }); + }); + } } export = NodePlatformFunctions; diff --git a/src/platform/DefaultPlatformFunctions.ts b/src/platform/PlatformFunctions.ts similarity index 60% rename from src/platform/DefaultPlatformFunctions.ts rename to src/platform/PlatformFunctions.ts index 21d957ea05..04c84ecaf3 100644 --- a/src/platform/DefaultPlatformFunctions.ts +++ b/src/platform/PlatformFunctions.ts @@ -1,13 +1,16 @@ +import EventEmitter = require('events'); +import StripeEmitter = require('../StripeEmitter'); + /** * Interface encapsulating various utility functions whose * implementations depend on the platform / JS runtime. */ -class DefaultPlatformFunctions { +class PlatformFunctions { /** * Gets uname with Node's built-in `exec` function, if available. */ getUname(): Promise { - return Promise.resolve(null); + throw new Error('getUname not implemented.'); } /** @@ -29,15 +32,30 @@ class DefaultPlatformFunctions { if (a.length !== b.length) { return false; } - const len = a.length; let result = 0; - for (let i = 0; i < len; ++i) { result |= a.charCodeAt(i) ^ b.charCodeAt(i); } return result === 0; } + + /** + * Creates an event emitter. + */ + createEmitter(): StripeEmitter | EventEmitter { + throw new Error('createEmitter not implemented.'); + } + + /** + * Checks if the request data is a stream. If so, read the entire stream + * to a buffer and return the buffer. + */ + tryBufferData( + data: MultipartRequestData + ): Promise { + throw new Error('tryBufferData not implemented.'); + } } -export = DefaultPlatformFunctions; +export = PlatformFunctions; diff --git a/src/platform/WebPlatformFunctions.ts b/src/platform/WebPlatformFunctions.ts new file mode 100644 index 0000000000..ac99dcb981 --- /dev/null +++ b/src/platform/WebPlatformFunctions.ts @@ -0,0 +1,31 @@ +import StripeEmitter = require('../StripeEmitter'); +import PlatformFunctions = require('./PlatformFunctions'); + +/** + * Specializes WebPlatformFunctions using APIs available in Web workers. + */ +class WebPlatformFunctions extends PlatformFunctions { + /** @override */ + getUname(): Promise { + return Promise.resolve(null); + } + + /** @override */ + createEmitter(): StripeEmitter { + return new StripeEmitter(); + } + + /** @override */ + tryBufferData( + data: MultipartRequestData + ): Promise { + if (data.file.data instanceof ReadableStream) { + throw new Error( + 'Uploading a file as a stream is not supported in non-Node environments. Please open or upvote an issue at github.com/stripe/stripe-node if you use this, detailing your use-case.' + ); + } + return Promise.resolve(data); + } +} + +export = WebPlatformFunctions; diff --git a/src/stripe.common.ts b/src/stripe.common.ts index 1e9466baec..66b4b7ff89 100644 --- a/src/stripe.common.ts +++ b/src/stripe.common.ts @@ -53,11 +53,10 @@ Stripe.HttpClient = HttpClient.HttpClient; Stripe.HttpClientResponse = HttpClient.HttpClientResponse; import CryptoProvider = require('./crypto/CryptoProvider'); -import EventEmitter = require('events'); Stripe.CryptoProvider = CryptoProvider; -import DefaultPlatformFunctions = require('./platform/DefaultPlatformFunctions'); -Stripe._platformFunctions = new DefaultPlatformFunctions(); +// @ts-ignore +Stripe._platformFunctions = null; function Stripe( this: StripeObject, @@ -71,7 +70,7 @@ function Stripe( const props = this._getPropsFromConfig(config); Object.defineProperty(this, '_emitter', { - value: new EventEmitter(), + value: Stripe._platformFunctions.createEmitter(), enumerable: false, configurable: false, writable: false, diff --git a/src/stripe.worker.ts b/src/stripe.worker.ts index c2d965f761..58ac5bf2d6 100644 --- a/src/stripe.worker.ts +++ b/src/stripe.worker.ts @@ -1,4 +1,8 @@ import Stripe = require('./stripe.common'); +import WebPlatformFunctions = require('./platform/WebPlatformFunctions'); + +Stripe._platformFunctions = new WebPlatformFunctions(); +Stripe.webhooks._platformFunctions = Stripe._platformFunctions; Stripe.createHttpClient = Stripe.createFetchHttpClient; Stripe.webhooks._createCryptoProvider = Stripe.createSubtleCryptoProvider; diff --git a/src/utils.ts b/src/utils.ts index f219bdf470..3c3583ffdf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,3 @@ -const EventEmitter = require('events').EventEmitter; const qs = require('qs'); const OPTIONS_KEYS = [ @@ -246,17 +245,6 @@ const utils = { .join('-'); }, - /** - * Determine if file data is a derivative of EventEmitter class. - * https://nodejs.org/api/events.html#events_events - */ - checkForStream: (obj: {file?: {data: unknown}}): boolean => { - if (obj.file && obj.file.data) { - return obj.file.data instanceof EventEmitter; - } - return false; - }, - callbackifyPromiseWithTimeout: ( promise: Promise, callback: ((error: unknown, result: T | null) => void) | null diff --git a/test/PlatformFunctions.spec.ts b/test/PlatformFunctions.spec.ts index e6ccd0c81a..523133b066 100644 --- a/test/PlatformFunctions.spec.ts +++ b/test/PlatformFunctions.spec.ts @@ -2,20 +2,35 @@ require('../testUtils'); -import {expect} from 'chai'; -import DefaultPlatformFunctions = require('../lib/platform/DefaultPlatformFunctions'); +import fs = require('fs'); +import path = require('path'); import NodePlatformFunctions = require('../lib/platform/NodePlatformFunctions'); +import PlatformFunctions = require('../lib/platform/PlatformFunctions'); + +import {expect} from 'chai'; + +let platforms: Record; -const platforms = { - default: new DefaultPlatformFunctions(), - node: new NodePlatformFunctions(), -}; +if (process.versions.node < '15') { + console.log( + `Skipping WebPlatformFunctions tests. Cannot load WebPlatformFunctions because 'Event' is not available in the global scope for ${process.version}.` + ); + platforms = { + Node: new NodePlatformFunctions(), + }; +} else { + const WebPlatformFunctions = require('../lib/platform/WebPlatformFunctions'); + platforms = { + Web: new WebPlatformFunctions(), + Node: new NodePlatformFunctions(), + }; +} for (const platform in platforms) { const platformFunctions = platforms[platform]; - const isNodeEnvironment = platform === 'node'; + const isNodeEnvironment = platform === 'Node'; - describe(`${platform} platform functions`, () => { + describe(`${platform}PlatformFunctions`, () => { describe('uuid', () => { describe('should use crypto.randomUUID if it exists', () => { const crypto = require('crypto'); @@ -181,5 +196,158 @@ for (const platform in platforms) { expect(await platformFunctions.getUname()).to.be.null; }); }); + + describe('createEmitter', () => { + let emitter; + beforeEach(() => { + emitter = platformFunctions.createEmitter(); + }); + + it('should emit a `foo` event with data to listeners', (done) => { + function onFoo(data): void { + emitter.removeListener('foo', onFoo); + + expect(data.bar).to.equal('bar'); + expect(data.baz).to.equal('baz'); + + done(); + } + + emitter.on('foo', onFoo); + emitter.emit('foo', {bar: 'bar', baz: 'baz'}); + }); + + it('listeners registered via `on` keep firing until removed', (done) => { + let calls = 0; + + function onFoo(): void { + calls += 1; + } + + emitter.on('foo', onFoo); + emitter.emit('foo'); + expect(calls).to.equal(1); + emitter.emit('foo'); + expect(calls).to.equal(2); + emitter.removeListener('foo', onFoo); + + done(); + }); + + it('listeners registered via `once` only fire once', (done) => { + let calls = 0; + + function onFoo(): void { + calls += 1; + } + + emitter.once('foo', onFoo); + emitter.emit('foo'); + expect(calls).to.equal(1); + emitter.emit('foo'); + expect(calls).to.equal(1); + + done(); + }); + + it('listeners registered multiple times are for each time it was registered', (done) => { + let calls = 0; + + function onFoo(): void { + calls += 1; + } + + emitter.on('foo', onFoo); + emitter.once('foo', onFoo); + + emitter.emit('foo'); + expect(calls).to.equal(2); + + done(); + }); + + it('should not emit a `foo` event to removed listeners', (done) => { + function onFoo(): void { + done(new Error('How did you get here?')); + } + + emitter.once('response', onFoo); + emitter.removeListener('response', onFoo); + emitter.emit('foo'); + + done(); + }); + }); + + describe('tryBufferData', () => { + if (!isNodeEnvironment) { + // WebPlatformFunctions + it('should throw an error in web environments if streaming data is provided', () => { + if (process.versions.node < '18') { + console.log( + `'ReadableStream' is not available in the global scope for ${process.version}, skipping test.` + ); + return; + } + + const f = new ReadableStream(); + const data = { + file: { + data: f, + name: 'test.pdf', + type: 'application/octet-stream', + }, + }; + + expect(() => { + platformFunctions.tryBufferData(data); + }).to.throw(); + }); + } else { + // NodePlatformFunctions + it('should return data unmodified if not a file stream', () => { + const testFilename = path.join( + __dirname, + 'resources/data/minimal.pdf' + ); + const buf = fs.readFileSync(testFilename); + + // Create Uint8Array from Buffer + const f = new Uint8Array(buf.buffer, buf.byteOffset, buf.length); + + const data = { + file: { + data: f, + name: 'minimal.pdf', + type: 'application/octet-stream', + }, + }; + + expect( + platformFunctions.tryBufferData(data) + ).to.eventually.deep.equal(data); + }); + + it('should load file streams into buffer', () => { + const testFilename = path.join( + __dirname, + 'resources/data/minimal.pdf' + ); + const f = fs.createReadStream(testFilename); + + const data = { + file: { + data: f, + name: 'minimal.pdf', + type: 'application/octet-stream', + }, + }; + + return expect( + platformFunctions.tryBufferData(data).then((d) => d.file.data) + ).to.eventually.have.nested.property('length', 739); + }); + } + }); }); } diff --git a/test/flows.spec.js b/test/flows.spec.ts similarity index 98% rename from test/flows.spec.js rename to test/flows.spec.ts index b0666a6c54..a1a10ea6c3 100644 --- a/test/flows.spec.js +++ b/test/flows.spec.ts @@ -326,7 +326,7 @@ describe('Flows', function() { .slice(2); const lowerBoundStartTime = Date.now(); - function onRequest(request) { + function onRequest(request): void { expect(request.api_version).to.equal('latest'); expect(request.idempotency_key).to.equal(idempotencyKey); expect(request.account).to.equal(connectedAccountId); @@ -365,7 +365,7 @@ describe('Flows', function() { .toString(36) .slice(2); - function onResponse(response) { + function onResponse(response): void { // On the off chance we're picking up a response from a differentrequest // then just ignore this and wait for the right one: if (response.idempotency_key !== idempotencyKey) { @@ -412,7 +412,7 @@ describe('Flows', function() { }); it('should not emit a `response` event to removed listeners on response', (done) => { - function onResponse(response) { + function onResponse(response): void { done(new Error('How did you get here?')); } @@ -489,7 +489,7 @@ describe('Flows', function() { it('Surfaces stream errors correctly', (done) => { const mockedStream = new stream.Readable(); - mockedStream._read = () => {}; + mockedStream._read = (): void => {}; const fakeError = new Error('I am a fake error');