From da1c4454b91328c25dc02b960eb06cc7f60a1302 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Fri, 25 Aug 2023 11:27:22 -0500 Subject: [PATCH 1/2] feat: Allow for custom EventEmitter --- integration/test/ParseLocalDatastoreTest.js | 67 ++++++++++++--------- src/CoreManager.js | 23 +++++++ src/EventEmitter.js | 23 ++++--- src/LiveQueryClient.js | 10 +-- src/LiveQuerySubscription.js | 11 ++-- src/Parse.ts | 11 ++-- src/ParseLiveQuery.js | 58 ++++++++++-------- 7 files changed, 128 insertions(+), 75 deletions(-) diff --git a/integration/test/ParseLocalDatastoreTest.js b/integration/test/ParseLocalDatastoreTest.js index 2c13c2423..91dd1b96c 100644 --- a/integration/test/ParseLocalDatastoreTest.js +++ b/integration/test/ParseLocalDatastoreTest.js @@ -1,36 +1,52 @@ 'use strict'; const assert = require('assert'); -const Parse = require('../../node'); global.localStorage = require('./mockLocalStorage'); +global.WebSocket = require('ws'); const mockRNStorage = require('./mockRNStorage'); -const LocalDatastoreUtils = require('../../lib/node/LocalDatastoreUtils'); +const serverURL = 'http://localhost:1337/parse'; -const { DEFAULT_PIN, PIN_PREFIX, isLocalDatastoreKey } = LocalDatastoreUtils; +function runTest(controller) { + const Parse = require(`../../${controller.name}`); + const LocalDatastoreUtils = require('../../lib/node/LocalDatastoreUtils'); -function LDS_KEY(object) { - return Parse.LocalDatastore.getKeyForObject(object); -} -function LDS_FULL_JSON(object) { - const json = object._toFullJSON(); - if (object._localId) { - json._localId = object._localId; + const { DEFAULT_PIN, PIN_PREFIX, isLocalDatastoreKey } = LocalDatastoreUtils; + + const Item = Parse.Object.extend('Item'); + const TestObject = Parse.Object.extend('TestObject'); + + function LDS_KEY(object) { + return Parse.LocalDatastore.getKeyForObject(object); } - return json; -} -function runTest(controller) { + function LDS_FULL_JSON(object) { + const json = object._toFullJSON(); + if (object._localId) { + json._localId = object._localId; + } + return json; + } + describe(`Parse Object Pinning (${controller.name})`, () => { beforeEach(async () => { const StorageController = require(controller.file); Parse.CoreManager.setAsyncStorage(mockRNStorage); Parse.CoreManager.setLocalDatastoreController(StorageController); - Parse.enableLocalDatastore(); + Parse.CoreManager.setEventEmitter(require('events').EventEmitter); + Parse.User.enableUnsafeCurrentUser(); await Parse.LocalDatastore._clear(); + Parse.initialize('integration'); + Parse.CoreManager.set('SERVER_URL', serverURL); + Parse.CoreManager.set('MASTER_KEY', 'notsosecret'); + const RESTController = Parse.CoreManager.getRESTController(); + RESTController._setXHR(require('xmlhttprequest').XMLHttpRequest); + Parse.enableLocalDatastore(); }); + function getStorageCount(storage) { return Object.keys(storage).reduce((acc, key) => acc + (isLocalDatastoreKey(key) ? 1 : 0), 1); } + it(`${controller.name} can clear localDatastore`, async () => { const obj1 = new TestObject(); const obj2 = new TestObject(); @@ -1060,8 +1076,15 @@ function runTest(controller) { const StorageController = require(controller.file); Parse.CoreManager.setAsyncStorage(mockRNStorage); Parse.CoreManager.setLocalDatastoreController(StorageController); - Parse.enableLocalDatastore(); + Parse.CoreManager.setEventEmitter(require('events').EventEmitter); Parse.LocalDatastore._clear(); + Parse.User.enableUnsafeCurrentUser(); + Parse.initialize('integration'); + Parse.CoreManager.set('SERVER_URL', serverURL); + Parse.CoreManager.set('MASTER_KEY', 'notsosecret'); + const RESTController = Parse.CoreManager.getRESTController(); + RESTController._setXHR(require('xmlhttprequest').XMLHttpRequest); + Parse.enableLocalDatastore(); const numbers = []; for (let i = 0; i < 10; i++) { @@ -2949,20 +2972,10 @@ function runTest(controller) { } describe('Parse LocalDatastore', () => { - beforeEach(() => { - Parse.CoreManager.getInstallationController()._setInstallationIdCache('1234'); - Parse.enableLocalDatastore(); - Parse.User.enableUnsafeCurrentUser(); - }); - const controllers = [ - { name: 'Default', file: '../../lib/node/LocalDatastoreController' }, - { - name: 'React-Native', - file: '../../lib/node/LocalDatastoreController.react-native', - }, + { name: 'node', file: '../../lib/node/LocalDatastoreController' }, + { name: 'react-native', file: '../../lib/react-native/LocalDatastoreController.react-native' }, ]; - for (let i = 0; i < controllers.length; i += 1) { const controller = controllers[i]; runTest(controller); diff --git a/src/CoreManager.js b/src/CoreManager.js index 73228c6d3..5076d8446 100644 --- a/src/CoreManager.js +++ b/src/CoreManager.js @@ -216,6 +216,13 @@ const CoreManager = { config[key] = value; }, + setIfNeeded: function (key: string, value: any): any { + if (!config.hasOwnProperty(key)) { + config[key] = value; + } + return config[key]; + }, + /* Specialized Controller Setters/Getters */ setAnalyticsController(controller: AnalyticsController) { @@ -254,6 +261,14 @@ const CoreManager = { return config['CryptoController']; }, + setEventEmitter(eventEmitter: any) { + config['EventEmitter'] = eventEmitter; + }, + + getEventEmitter(): any { + return config['EventEmitter']; + }, + setFileController(controller: FileController) { requireMethods('FileController', ['saveFile', 'saveBase64'], controller); config['FileController'] = controller; @@ -272,6 +287,14 @@ const CoreManager = { return config['InstallationController']; }, + setLiveQuery(liveQuery: any) { + config['LiveQuery'] = liveQuery; + }, + + getLiveQuery(): any { + return config['LiveQuery']; + }, + setObjectController(controller: ObjectController) { requireMethods('ObjectController', ['save', 'fetch', 'destroy'], controller); config['ObjectController'] = controller; diff --git a/src/EventEmitter.js b/src/EventEmitter.js index f7bdd3172..b2215a46b 100644 --- a/src/EventEmitter.js +++ b/src/EventEmitter.js @@ -2,13 +2,20 @@ * This is a simple wrapper to unify EventEmitter implementations across platforms. */ -if (process.env.PARSE_BUILD === 'react-native') { - let EventEmitter = require('react-native/Libraries/vendor/emitter/EventEmitter'); - if (EventEmitter.default) { - EventEmitter = EventEmitter.default; +let EventEmitter; + +try { + if (process.env.PARSE_BUILD === 'react-native') { + let EventEmitter = require('react-native/Libraries/vendor/emitter/EventEmitter'); + if (EventEmitter.default) { + EventEmitter = EventEmitter.default; + } + EventEmitter.prototype.on = EventEmitter.prototype.addListener; + } else { + EventEmitter = require('events').EventEmitter; } - EventEmitter.prototype.on = EventEmitter.prototype.addListener; - module.exports = EventEmitter; -} else { - module.exports = require('events').EventEmitter; +} catch (_) { + // Event emitter not available } + +module.exports = EventEmitter; diff --git a/src/LiveQueryClient.js b/src/LiveQueryClient.js index 6baf67fb5..790624fbf 100644 --- a/src/LiveQueryClient.js +++ b/src/LiveQueryClient.js @@ -1,7 +1,6 @@ /* global WebSocket */ import CoreManager from './CoreManager'; -import EventEmitter from './EventEmitter'; import ParseObject from './ParseObject'; import LiveQuerySubscription from './LiveQuerySubscription'; import { resolvingPromise } from './promiseUtils'; @@ -63,7 +62,6 @@ const generateInterval = k => { /** * Creates a new LiveQueryClient. - * Extends events.EventEmitter * cloud functions. * * A wrapper of a standard WebSocket client. We add several useful methods to @@ -105,7 +103,7 @@ const generateInterval = k => { * * @alias Parse.LiveQueryClient */ -class LiveQueryClient extends EventEmitter { +class LiveQueryClient { attempts: number; id: number; requestId: number; @@ -138,8 +136,6 @@ class LiveQueryClient extends EventEmitter { sessionToken, installationId, }) { - super(); - if (!serverURL || serverURL.indexOf('ws') !== 0) { throw new Error( 'You need to set a proper Parse LiveQuery server url before using LiveQueryClient' @@ -160,7 +156,11 @@ class LiveQueryClient extends EventEmitter { this.connectPromise = resolvingPromise(); this.subscriptions = new Map(); this.state = CLIENT_STATE.INITIALIZED; + const EventEmitter = CoreManager.getEventEmitter(); + this.emitter = new EventEmitter(); + this.on = this.emitter.on; + this.emit = this.emitter.emit; // adding listener so process does not crash // best practice is for developer to register their own listener this.on('error', () => {}); diff --git a/src/LiveQuerySubscription.js b/src/LiveQuerySubscription.js index 503a07175..c70d234c1 100644 --- a/src/LiveQuerySubscription.js +++ b/src/LiveQuerySubscription.js @@ -1,10 +1,8 @@ -import EventEmitter from './EventEmitter'; import CoreManager from './CoreManager'; import { resolvingPromise } from './promiseUtils'; /** * Creates a new LiveQuery Subscription. - * Extends events.EventEmitter * cloud functions. * *

Response Object - Contains data from the client that made the request @@ -84,24 +82,25 @@ import { resolvingPromise } from './promiseUtils'; * subscription.on('close', () => { * * });

- * - * @alias Parse.LiveQuerySubscription */ -class Subscription extends EventEmitter { +class Subscription { /* * @param {string} id - subscription id * @param {string} query - query to subscribe to * @param {string} sessionToken - optional session token */ constructor(id, query, sessionToken) { - super(); this.id = id; this.query = query; this.sessionToken = sessionToken; this.subscribePromise = resolvingPromise(); this.unsubscribePromise = resolvingPromise(); this.subscribed = false; + const EventEmitter = CoreManager.getEventEmitter(); + this.emitter = new EventEmitter(); + this.on = this.emitter.on; + this.emit = this.emitter.emit; // adding listener so process does not crash // best practice is for developer to register their own listener this.on('error', () => {}); diff --git a/src/Parse.ts b/src/Parse.ts index eee273629..b6876a861 100644 --- a/src/Parse.ts +++ b/src/Parse.ts @@ -11,6 +11,7 @@ import AnonymousUtils from './AnonymousUtils' import * as Cloud from './Cloud'; import CLP from './ParseCLP'; import CoreManager from './CoreManager'; +import EventEmitter from './EventEmitter'; import Config from './ParseConfig' import ParseError from './ParseError' import FacebookUtils from './FacebookUtils' @@ -76,7 +77,7 @@ interface ParseType { Session: typeof Session, Storage: typeof Storage, User: typeof User, - LiveQuery: typeof LiveQuery, + LiveQuery?: typeof LiveQuery, LiveQueryClient: typeof LiveQueryClient, initialize(applicationId: string, javaScriptKey: string): void, @@ -143,13 +144,12 @@ const Parse: ParseType = { Session: Session, Storage: Storage, User: User, - LiveQuery: LiveQuery, LiveQueryClient: LiveQueryClient, + LiveQuery: undefined, IndexedDB: undefined, Hooks: undefined, Parse: undefined, - /** * Call this method first to set up your authentication tokens for Parse. * @@ -179,6 +179,10 @@ const Parse: ParseType = { CoreManager.set('JAVASCRIPT_KEY', javaScriptKey); CoreManager.set('MASTER_KEY', masterKey); CoreManager.set('USE_MASTER_KEY', false); + CoreManager.setIfNeeded('EventEmitter', EventEmitter); + + Parse.LiveQuery = new LiveQuery(); + CoreManager.setIfNeeded('LiveQuery', Parse.LiveQuery); }, /** @@ -422,7 +426,6 @@ const Parse: ParseType = { isEncryptedUserEnabled () { return this.encryptedUser; }, - }; if (process.env.PARSE_BUILD === 'browser') { diff --git a/src/ParseLiveQuery.js b/src/ParseLiveQuery.js index 2a0aa7e69..eea9e59e2 100644 --- a/src/ParseLiveQuery.js +++ b/src/ParseLiveQuery.js @@ -1,12 +1,10 @@ /** * @flow */ - -import EventEmitter from './EventEmitter'; import LiveQueryClient from './LiveQueryClient'; import CoreManager from './CoreManager'; -function getLiveQueryClient(): LiveQueryClient { +function getLiveQueryClient(): Promise { return CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); } @@ -37,31 +35,39 @@ function getLiveQueryClient(): LiveQueryClient { * @class Parse.LiveQuery * @static */ -const LiveQuery = new EventEmitter(); +class LiveQuery { + constructor() { + const EventEmitter = CoreManager.getEventEmitter(); + this.emitter = new EventEmitter(); + this.on = this.emitter.on; + this.emit = this.emitter.emit; -/** - * After open is called, the LiveQuery will try to send a connect request - * to the LiveQuery server. - */ -LiveQuery.open = async () => { - const liveQueryClient = await getLiveQueryClient(); - liveQueryClient.open(); -}; + // adding listener so process does not crash + // best practice is for developer to register their own listener + this.on('error', () => {}); + } -/** - * When you're done using LiveQuery, you can call Parse.LiveQuery.close(). - * This function will close the WebSocket connection to the LiveQuery server, - * cancel the auto reconnect, and unsubscribe all subscriptions based on it. - * If you call query.subscribe() after this, we'll create a new WebSocket - * connection to the LiveQuery server. - */ -LiveQuery.close = async () => { - const liveQueryClient = await getLiveQueryClient(); - liveQueryClient.close(); -}; + /** + * After open is called, the LiveQuery will try to send a connect request + * to the LiveQuery server. + */ + async open(): void { + const liveQueryClient = await getLiveQueryClient(); + liveQueryClient.open(); + } -// Register a default onError callback to make sure we do not crash on error -LiveQuery.on('error', () => {}); + /** + * When you're done using LiveQuery, you can call Parse.LiveQuery.close(). + * This function will close the WebSocket connection to the LiveQuery server, + * cancel the auto reconnect, and unsubscribe all subscriptions based on it. + * If you call query.subscribe() after this, we'll create a new WebSocket + * connection to the LiveQuery server. + */ + async close(): void { + const liveQueryClient = await getLiveQueryClient(); + liveQueryClient.close(); + } +} export default LiveQuery; @@ -110,6 +116,8 @@ const DefaultLiveQueryController = { sessionToken, installationId, }); + const LiveQuery = CoreManager.getLiveQuery(); + defaultLiveQueryClient.on('error', error => { LiveQuery.emit('error', error); }); From 22029814d942c14de1478c9b33d5c80873ec53c6 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Fri, 25 Aug 2023 12:01:13 -0500 Subject: [PATCH 2/2] fix unit tests --- src/EventEmitter.js | 5 ++--- src/__tests__/LiveQueryClient-test.js | 2 ++ src/__tests__/ParseLiveQuery-test.js | 6 +++++- src/__tests__/ParseQuery-test.js | 3 +++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/EventEmitter.js b/src/EventEmitter.js index b2215a46b..1f1bcbd00 100644 --- a/src/EventEmitter.js +++ b/src/EventEmitter.js @@ -6,7 +6,7 @@ let EventEmitter; try { if (process.env.PARSE_BUILD === 'react-native') { - let EventEmitter = require('react-native/Libraries/vendor/emitter/EventEmitter'); + EventEmitter = require('react-native/Libraries/vendor/emitter/EventEmitter'); if (EventEmitter.default) { EventEmitter = EventEmitter.default; } @@ -15,7 +15,6 @@ try { EventEmitter = require('events').EventEmitter; } } catch (_) { - // Event emitter not available + // EventEmitter unavailable } - module.exports = EventEmitter; diff --git a/src/__tests__/LiveQueryClient-test.js b/src/__tests__/LiveQueryClient-test.js index 5e396f20a..4d7222db7 100644 --- a/src/__tests__/LiveQueryClient-test.js +++ b/src/__tests__/LiveQueryClient-test.js @@ -35,6 +35,7 @@ const mockLocalDatastore = { jest.setMock('../LocalDatastore', mockLocalDatastore); const CoreManager = require('../CoreManager'); +const EventEmitter = require('../EventEmitter'); const LiveQueryClient = require('../LiveQueryClient').default; const ParseObject = require('../ParseObject').default; const ParseQuery = require('../ParseQuery').default; @@ -46,6 +47,7 @@ CoreManager.setLocalDatastore(mockLocalDatastore); describe('LiveQueryClient', () => { beforeEach(() => { mockLocalDatastore.isEnabled = false; + CoreManager.setEventEmitter(EventEmitter); }); it('serverURL required', () => { diff --git a/src/__tests__/ParseLiveQuery-test.js b/src/__tests__/ParseLiveQuery-test.js index 63178f028..ae105f1ef 100644 --- a/src/__tests__/ParseLiveQuery-test.js +++ b/src/__tests__/ParseLiveQuery-test.js @@ -9,18 +9,22 @@ jest.dontMock('../EventEmitter'); jest.dontMock('../promiseUtils'); // Forces the loading -const LiveQuery = require('../ParseLiveQuery').default; +const ParseLiveQuery = require('../ParseLiveQuery').default; const CoreManager = require('../CoreManager'); +const EventEmitter = require('../EventEmitter'); const ParseQuery = require('../ParseQuery').default; const LiveQuerySubscription = require('../LiveQuerySubscription').default; const mockLiveQueryClient = { open: jest.fn(), close: jest.fn(), }; +CoreManager.setEventEmitter(EventEmitter); +const LiveQuery = new ParseLiveQuery() describe('ParseLiveQuery', () => { beforeEach(() => { const controller = CoreManager.getLiveQueryController(); + CoreManager.setLiveQuery(LiveQuery); controller._clearCachedDefaultClient(); CoreManager.set('InstallationController', { currentInstallationId() { diff --git a/src/__tests__/ParseQuery-test.js b/src/__tests__/ParseQuery-test.js index 4499b27fa..457edd1fe 100644 --- a/src/__tests__/ParseQuery-test.js +++ b/src/__tests__/ParseQuery-test.js @@ -1,6 +1,7 @@ jest.dontMock('../CoreManager'); jest.dontMock('../encode'); jest.dontMock('../decode'); +jest.dontMock('../EventEmitter'); jest.dontMock('../ParseError'); jest.dontMock('../ParseGeoPoint'); jest.dontMock('../ParseQuery'); @@ -40,6 +41,7 @@ const mockLocalDatastore = { jest.setMock('../LocalDatastore', mockLocalDatastore); let CoreManager = require('../CoreManager'); +const EventEmitter = require('../EventEmitter'); const ParseError = require('../ParseError').default; const ParseGeoPoint = require('../ParseGeoPoint').default; let ParseObject = require('../ParseObject'); @@ -52,6 +54,7 @@ const MockRESTController = { }; const QueryController = CoreManager.getQueryController(); +CoreManager.setEventEmitter(EventEmitter); import { DEFAULT_PIN } from '../LocalDatastoreUtils';