Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 36 additions & 12 deletions src/LiveQueryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const OP_TYPES = {
SUBSCRIBE: 'subscribe',
UNSUBSCRIBE: 'unsubscribe',
ERROR: 'error',
QUERY: 'query',
};

// The event we get back from LiveQuery server
Expand All @@ -34,6 +35,7 @@ const OP_EVENTS = {
ENTER: 'enter',
LEAVE: 'leave',
DELETE: 'delete',
RESULT: 'result',
};

// The event the LiveQuery client should emit
Expand All @@ -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;
};

Expand Down Expand Up @@ -123,13 +126,15 @@ class LiveQueryClient {
emit: any;

/**
* @param {object} options
* @param {string} options.applicationId - applicationId of your Parse app
* @param {string} options.serverURL - <b>the URL of your LiveQuery server</b>
* @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,
Expand All @@ -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(
Expand All @@ -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', () => {});
Expand Down Expand Up @@ -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);
});

Expand Down Expand Up @@ -425,6 +437,18 @@ class LiveQueryClient {
}
break;
}
case OP_EVENTS.RESULT: {
if (subscription) {
const objects = data.results.map((json: Record<string, unknown>) => {
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) {
Expand Down
20 changes: 19 additions & 1 deletion src/LiveQuerySubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,18 +92,21 @@ class LiveQuerySubscription {
subscribePromise: any;
unsubscribePromise: any;
subscribed: boolean;
client: any;
emitter: EventEmitter;
on: EventEmitter['on'];
emit: EventEmitter['emit'];
/*
* @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;
Expand All @@ -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;
135 changes: 134 additions & 1 deletion src/__tests__/LiveQueryClient-test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
jest.dontMock('../LiveQuerySubscription');
jest.dontMock('../LiveQueryClient');
jest.dontMock('../arrayContainsObject');
jest.dontMock('../canBeSerialized');
Expand All @@ -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');

Expand All @@ -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;
Expand Down Expand Up @@ -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,
});
});
});
8 changes: 7 additions & 1 deletion types/LiveQuerySubscription.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
/**
* Execute a query on this subscription.
* The results will be delivered via the 'result' event.
*/
find(): void;
}
export default LiveQuerySubscription;
Loading