diff --git a/src/LiveQueryClient.ts b/src/LiveQueryClient.ts
index 44f1c552f..58a07654e 100644
--- a/src/LiveQueryClient.ts
+++ b/src/LiveQueryClient.ts
@@ -21,6 +21,7 @@ const OP_TYPES = {
SUBSCRIBE: 'subscribe',
UNSUBSCRIBE: 'unsubscribe',
ERROR: 'error',
+ QUERY: 'query',
};
// The event we get back from LiveQuery server
@@ -34,6 +35,7 @@ const OP_EVENTS = {
ENTER: 'enter',
LEAVE: 'leave',
DELETE: 'delete',
+ RESULT: 'result',
};
// The event the LiveQuery client should emit
@@ -53,10 +55,11 @@ const SUBSCRIPTION_EMMITER_TYPES = {
ENTER: 'enter',
LEAVE: 'leave',
DELETE: 'delete',
+ RESULT: 'result',
};
// Exponentially-growing random delay
-const generateInterval = k => {
+const generateInterval = (k: number): number => {
return Math.random() * Math.min(30, Math.pow(2, k) - 1) * 1000;
};
@@ -123,13 +126,15 @@ class LiveQueryClient {
emit: any;
/**
- * @param {object} options
- * @param {string} options.applicationId - applicationId of your Parse app
- * @param {string} options.serverURL - the URL of your LiveQuery server
- * @param {string} options.javascriptKey (optional)
- * @param {string} options.masterKey (optional) Your Parse Master Key. (Node.js only!)
- * @param {string} options.sessionToken (optional)
- * @param {string} options.installationId (optional)
+ * Creates a new LiveQueryClient instance.
+ *
+ * @param options - Configuration options for the LiveQuery client
+ * @param options.applicationId - The applicationId of your Parse app
+ * @param options.serverURL - The URL of your LiveQuery server (must start with 'ws' or 'wss')
+ * @param options.javascriptKey - (Optional) The JavaScript key for your Parse app
+ * @param options.masterKey - (Optional) Your Parse Master Key (Node.js only!)
+ * @param options.sessionToken - (Optional) Session token for authenticated requests
+ * @param options.installationId - (Optional) Installation ID for the client
*/
constructor({
applicationId,
@@ -138,6 +143,13 @@ class LiveQueryClient {
masterKey,
sessionToken,
installationId,
+ }: {
+ applicationId: string;
+ serverURL: string;
+ javascriptKey?: string;
+ masterKey?: string;
+ sessionToken?: string;
+ installationId?: string;
}) {
if (!serverURL || serverURL.indexOf('ws') !== 0) {
throw new Error(
@@ -162,8 +174,8 @@ class LiveQueryClient {
const EventEmitter = CoreManager.getEventEmitter();
this.emitter = new EventEmitter();
- this.on = (eventName, listener) => this.emitter.on(eventName, listener);
- this.emit = (eventName, ...args) => this.emitter.emit(eventName, ...args);
+ this.on = (eventName: string, listener: (...args: unknown[]) => void) => this.emitter.on(eventName, listener);
+ this.emit = (eventName: string, ...args: unknown[]) => this.emitter.emit(eventName, ...args);
// adding listener so process does not crash
// best practice is for developer to register their own listener
this.on('error', () => {});
@@ -212,14 +224,14 @@ class LiveQueryClient {
subscribeRequest.sessionToken = sessionToken;
}
- const subscription = new LiveQuerySubscription(this.requestId, query, sessionToken);
+ const subscription = new LiveQuerySubscription(this.requestId, query, sessionToken, this);
this.subscriptions.set(this.requestId, subscription);
this.requestId += 1;
this.connectPromise
.then(() => {
this.socket.send(JSON.stringify(subscribeRequest));
})
- .catch(error => {
+ .catch((error: Error) => {
subscription.subscribePromise.reject(error);
});
@@ -425,6 +437,18 @@ class LiveQueryClient {
}
break;
}
+ case OP_EVENTS.RESULT: {
+ if (subscription) {
+ const objects = data.results.map((json: Record) => {
+ if (!json.className && subscription.query) {
+ json.className = subscription.query.className;
+ }
+ return ParseObject.fromJSON(json, false);
+ });
+ subscription.emit(SUBSCRIPTION_EMMITER_TYPES.RESULT, objects);
+ }
+ break;
+ }
default: {
// create, update, enter, leave, delete cases
if (!subscription) {
diff --git a/src/LiveQuerySubscription.ts b/src/LiveQuerySubscription.ts
index 79a59d822..0759a5349 100644
--- a/src/LiveQuerySubscription.ts
+++ b/src/LiveQuerySubscription.ts
@@ -92,6 +92,7 @@ class LiveQuerySubscription {
subscribePromise: any;
unsubscribePromise: any;
subscribed: boolean;
+ client: any;
emitter: EventEmitter;
on: EventEmitter['on'];
emit: EventEmitter['emit'];
@@ -99,11 +100,13 @@ class LiveQuerySubscription {
* @param {string | number} id - subscription id
* @param {string} query - query to subscribe to
* @param {string} sessionToken - optional session token
+ * @param {object} client - LiveQueryClient instance
*/
- constructor(id: string | number, query: ParseQuery, sessionToken?: string) {
+ constructor(id: string | number, query: ParseQuery, sessionToken?: string, client?: any) {
this.id = id;
this.query = query;
this.sessionToken = sessionToken;
+ this.client = client;
this.subscribePromise = resolvingPromise();
this.unsubscribePromise = resolvingPromise();
this.subscribed = false;
@@ -130,6 +133,21 @@ class LiveQuerySubscription {
return liveQueryClient.unsubscribe(this);
});
}
+
+ /**
+ * Execute a query on this subscription.
+ * The results will be delivered via the 'result' event.
+ */
+ find() {
+ if (this.client) {
+ this.client.connectPromise.then(() => {
+ this.client.socket.send(JSON.stringify({
+ op: 'query',
+ requestId: this.id,
+ }));
+ });
+ }
+ }
}
export default LiveQuerySubscription;
diff --git a/src/__tests__/LiveQueryClient-test.js b/src/__tests__/LiveQueryClient-test.js
index c8fa0ba0c..ccb192494 100644
--- a/src/__tests__/LiveQueryClient-test.js
+++ b/src/__tests__/LiveQueryClient-test.js
@@ -1,3 +1,4 @@
+jest.dontMock('../LiveQuerySubscription');
jest.dontMock('../LiveQueryClient');
jest.dontMock('../arrayContainsObject');
jest.dontMock('../canBeSerialized');
@@ -23,7 +24,6 @@ jest.dontMock('../UniqueInstanceStateController');
jest.dontMock('../unsavedChildren');
jest.dontMock('../ParseACL');
jest.dontMock('../ParseQuery');
-jest.dontMock('../LiveQuerySubscription');
jest.dontMock('../LocalDatastore');
jest.dontMock('../WebSocketController');
@@ -38,6 +38,7 @@ jest.setMock('../LocalDatastore', mockLocalDatastore);
const CoreManager = require('../CoreManager').default;
const EventEmitter = require('../EventEmitter').default;
const LiveQueryClient = require('../LiveQueryClient').default;
+const LiveQuerySubscription = require('../LiveQuerySubscription').default;
const ParseObject = require('../ParseObject').default;
const ParseQuery = require('../ParseQuery').default;
const WebSocketController = require('../WebSocketController').default;
@@ -1091,4 +1092,136 @@ describe('LiveQueryClient', () => {
const subscription = liveQueryClient.subscribe();
expect(subscription).toBe(undefined);
});
+
+ it('can handle WebSocket result response message', () => {
+ const liveQueryClient = new LiveQueryClient({
+ applicationId: 'applicationId',
+ serverURL: 'ws://test',
+ javascriptKey: 'javascriptKey',
+ masterKey: 'masterKey',
+ sessionToken: 'sessionToken',
+ });
+ // Add mock subscription
+ const subscription = new events.EventEmitter();
+ liveQueryClient.subscriptions.set(1, subscription);
+ const object1 = new ParseObject('Test');
+ object1.set('key', 'value1');
+ const object2 = new ParseObject('Test');
+ object2.set('key', 'value2');
+ const data = {
+ op: 'result',
+ clientId: 1,
+ requestId: 1,
+ results: [object1._toFullJSON(), object2._toFullJSON()],
+ };
+ const event = {
+ data: JSON.stringify(data),
+ };
+ // Register checked in advance
+ let isChecked = false;
+ subscription.on('result', function (objects) {
+ isChecked = true;
+ expect(objects.length).toBe(2);
+ expect(objects[0].get('key')).toEqual('value1');
+ expect(objects[1].get('key')).toEqual('value2');
+ });
+
+ liveQueryClient._handleWebSocketMessage(event);
+
+ expect(isChecked).toBe(true);
+ });
+
+ it('can handle WebSocket result response message with missing className', () => {
+ const liveQueryClient = new LiveQueryClient({
+ applicationId: 'applicationId',
+ serverURL: 'ws://test',
+ javascriptKey: 'javascriptKey',
+ masterKey: 'masterKey',
+ sessionToken: 'sessionToken',
+ });
+ // Add mock subscription with query
+ const subscription = new events.EventEmitter();
+ const query = new ParseQuery('TestClass');
+ subscription.query = query;
+ liveQueryClient.subscriptions.set(1, subscription);
+
+ // Create results without className property
+ const data = {
+ op: 'result',
+ clientId: 1,
+ requestId: 1,
+ results: [
+ { objectId: 'obj1', key: 'value1' },
+ { objectId: 'obj2', key: 'value2' },
+ ],
+ };
+ const event = {
+ data: JSON.stringify(data),
+ };
+
+ // Register checked in advance
+ let isChecked = false;
+ subscription.on('result', function (objects) {
+ isChecked = true;
+ expect(objects.length).toBe(2);
+ expect(objects[0].className).toEqual('TestClass');
+ expect(objects[1].className).toEqual('TestClass');
+ });
+
+ liveQueryClient._handleWebSocketMessage(event);
+
+ expect(isChecked).toBe(true);
+ });
+
+ it('LiveQuerySubscription class has find method', () => {
+ expect(typeof LiveQuerySubscription.prototype.find).toBe('function');
+ });
+
+ it('subscription has find method', () => {
+ const liveQueryClient = new LiveQueryClient({
+ applicationId: 'applicationId',
+ serverURL: 'ws://test',
+ javascriptKey: 'javascriptKey',
+ masterKey: 'masterKey',
+ sessionToken: 'sessionToken',
+ });
+ const query = new ParseQuery('Test');
+ query.equalTo('key', 'value');
+
+ const subscription = liveQueryClient.subscribe(query);
+
+ expect(subscription).toBeInstanceOf(LiveQuerySubscription);
+ expect(typeof subscription.find).toBe('function');
+ });
+
+ it('can send query message via subscription', async () => {
+ const liveQueryClient = new LiveQueryClient({
+ applicationId: 'applicationId',
+ serverURL: 'ws://test',
+ javascriptKey: 'javascriptKey',
+ masterKey: 'masterKey',
+ sessionToken: 'sessionToken',
+ });
+ liveQueryClient.socket = {
+ send: jest.fn(),
+ };
+ const query = new ParseQuery('Test');
+ query.equalTo('key', 'value');
+
+ const subscription = liveQueryClient.subscribe(query);
+ liveQueryClient.connectPromise.resolve();
+ await liveQueryClient.connectPromise;
+
+ subscription.find();
+
+ // Need to wait for the sendMessage promise to resolve
+ await Promise.resolve();
+
+ const messageStr = liveQueryClient.socket.send.mock.calls[1][0];
+ const message = JSON.parse(messageStr);
+ expect(message).toEqual({
+ op: 'query',
+ requestId: 1,
+ });
+ });
});
diff --git a/types/LiveQueryClient.d.ts b/types/LiveQueryClient.d.ts
index 4f41545dc..e4b96d957 100644
--- a/types/LiveQueryClient.d.ts
+++ b/types/LiveQueryClient.d.ts
@@ -65,21 +65,23 @@ declare class LiveQueryClient {
on: any;
emit: any;
/**
- * @param {object} options
- * @param {string} options.applicationId - applicationId of your Parse app
- * @param {string} options.serverURL - the URL of your LiveQuery server
- * @param {string} options.javascriptKey (optional)
- * @param {string} options.masterKey (optional) Your Parse Master Key. (Node.js only!)
- * @param {string} options.sessionToken (optional)
- * @param {string} options.installationId (optional)
+ * Creates a new LiveQueryClient instance.
+ *
+ * @param options - Configuration options for the LiveQuery client
+ * @param options.applicationId - The applicationId of your Parse app
+ * @param options.serverURL - The URL of your LiveQuery server (must start with 'ws' or 'wss')
+ * @param options.javascriptKey - (Optional) The JavaScript key for your Parse app
+ * @param options.masterKey - (Optional) Your Parse Master Key (Node.js only!)
+ * @param options.sessionToken - (Optional) Session token for authenticated requests
+ * @param options.installationId - (Optional) Installation ID for the client
*/
constructor({ applicationId, serverURL, javascriptKey, masterKey, sessionToken, installationId, }: {
- applicationId: any;
- serverURL: any;
- javascriptKey: any;
- masterKey: any;
- sessionToken: any;
- installationId: any;
+ applicationId: string;
+ serverURL: string;
+ javascriptKey?: string;
+ masterKey?: string;
+ sessionToken?: string;
+ installationId?: string;
});
shouldOpen(): any;
/**
diff --git a/types/LiveQuerySubscription.d.ts b/types/LiveQuerySubscription.d.ts
index 7bc4df0e7..e47a78c8d 100644
--- a/types/LiveQuerySubscription.d.ts
+++ b/types/LiveQuerySubscription.d.ts
@@ -89,15 +89,21 @@ declare class LiveQuerySubscription {
subscribePromise: any;
unsubscribePromise: any;
subscribed: boolean;
+ client: any;
emitter: EventEmitter;
on: EventEmitter['on'];
emit: EventEmitter['emit'];
- constructor(id: string | number, query: ParseQuery, sessionToken?: string);
+ constructor(id: string | number, query: ParseQuery, sessionToken?: string, client?: any);
/**
* Close the subscription
*
* @returns {Promise}
*/
unsubscribe(): Promise;
+ /**
+ * Execute a query on this subscription.
+ * The results will be delivered via the 'result' event.
+ */
+ find(): void;
}
export default LiveQuerySubscription;