Add LiveQuery
This commit is contained in:
104
src/LiveQuery/Client.js
Normal file
104
src/LiveQuery/Client.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import PLog from './PLog';
|
||||
import Parse from 'parse/node';
|
||||
|
||||
import type { FlattenedObjectData } from './Subscription';
|
||||
export type Message = { [attr: string]: any };
|
||||
|
||||
let dafaultFields = ['className', 'objectId', 'updatedAt', 'createdAt', 'ACL'];
|
||||
|
||||
class Client {
|
||||
id: number;
|
||||
parseWebSocket: any;
|
||||
userId: string;
|
||||
roles: Array<string>;
|
||||
subscriptionInfos: Object;
|
||||
pushConnect: Function;
|
||||
pushSubscribe: Function;
|
||||
pushUnsubscribe: Function;
|
||||
pushCreate: Function;
|
||||
pushEnter: Function;
|
||||
pushUpdate: Function;
|
||||
pushDelete: Function;
|
||||
pushLeave: Function;
|
||||
|
||||
constructor(id: number, parseWebSocket: any) {
|
||||
this.id = id;
|
||||
this.parseWebSocket = parseWebSocket;
|
||||
this.roles = [];
|
||||
this.subscriptionInfos = new Map();
|
||||
this.pushConnect = this._pushEvent('connected');
|
||||
this.pushSubscribe = this._pushEvent('subscribed');
|
||||
this.pushUnsubscribe = this._pushEvent('unsubscribed');
|
||||
this.pushCreate = this._pushEvent('create');
|
||||
this.pushEnter = this._pushEvent('enter');
|
||||
this.pushUpdate = this._pushEvent('update');
|
||||
this.pushDelete = this._pushEvent('delete');
|
||||
this.pushLeave = this._pushEvent('leave');
|
||||
}
|
||||
|
||||
static pushResponse(parseWebSocket: any, message: Message): void {
|
||||
PLog.verbose('Push Response : %j', message);
|
||||
parseWebSocket.send(message);
|
||||
}
|
||||
|
||||
static pushError(parseWebSocket: any, code: number, error: string, reconnect: boolean = true): void {
|
||||
Client.pushResponse(parseWebSocket, JSON.stringify({
|
||||
'op': 'error',
|
||||
'error': error,
|
||||
'code': code,
|
||||
'reconnect': reconnect
|
||||
}));
|
||||
}
|
||||
|
||||
addSubscriptionInfo(requestId: number, subscriptionInfo: any): void {
|
||||
this.subscriptionInfos.set(requestId, subscriptionInfo);
|
||||
}
|
||||
|
||||
getSubscriptionInfo(requestId: numner): any {
|
||||
return this.subscriptionInfos.get(requestId);
|
||||
}
|
||||
|
||||
deleteSubscriptionInfo(requestId: number): void {
|
||||
return this.subscriptionInfos.delete(requestId);
|
||||
}
|
||||
|
||||
_pushEvent(type: string): Function {
|
||||
return function(subscriptionId: number, parseObjectJSON: any): void {
|
||||
let response: Message = {
|
||||
'op' : type,
|
||||
'clientId' : this.id
|
||||
};
|
||||
if (typeof subscriptionId !== 'undefined') {
|
||||
response['requestId'] = subscriptionId;
|
||||
}
|
||||
if (typeof parseObjectJSON !== 'undefined') {
|
||||
let fields;
|
||||
if (this.subscriptionInfos.has(subscriptionId)) {
|
||||
fields = this.subscriptionInfos.get(subscriptionId).fields;
|
||||
}
|
||||
response['object'] = this._toJSONWithFields(parseObjectJSON, fields);
|
||||
}
|
||||
Client.pushResponse(this.parseWebSocket, JSON.stringify(response));
|
||||
}
|
||||
}
|
||||
|
||||
_toJSONWithFields(parseObjectJSON: any, fields: any): FlattenedObjectData {
|
||||
if (!fields) {
|
||||
return parseObjectJSON;
|
||||
}
|
||||
let limitedParseObject = {};
|
||||
for (let field of dafaultFields) {
|
||||
limitedParseObject[field] = parseObjectJSON[field];
|
||||
}
|
||||
for (let field of fields) {
|
||||
if (field in parseObjectJSON) {
|
||||
limitedParseObject[field] = parseObjectJSON[field];
|
||||
}
|
||||
}
|
||||
return limitedParseObject;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
Client
|
||||
}
|
||||
59
src/LiveQuery/EventEmitterPubSub.js
Normal file
59
src/LiveQuery/EventEmitterPubSub.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import events from 'events';
|
||||
|
||||
let emitter = new events.EventEmitter();
|
||||
|
||||
class Publisher {
|
||||
emitter: any;
|
||||
|
||||
constructor(emitter: any) {
|
||||
this.emitter = emitter;
|
||||
}
|
||||
|
||||
publish(channel: string, message: string): void {
|
||||
this.emitter.emit(channel, message);
|
||||
}
|
||||
}
|
||||
|
||||
class Subscriber extends events.EventEmitter {
|
||||
emitter: any;
|
||||
subscriptions: any;
|
||||
|
||||
constructor(emitter: any) {
|
||||
super();
|
||||
this.emitter = emitter;
|
||||
this.subscriptions = new Map();
|
||||
}
|
||||
|
||||
subscribe(channel: string): void {
|
||||
let handler = (message) => {
|
||||
this.emit('message', channel, message);
|
||||
}
|
||||
this.subscriptions.set(channel, handler);
|
||||
this.emitter.on(channel, handler);
|
||||
}
|
||||
|
||||
unsubscribe(channel: string): void {
|
||||
if (!this.subscriptions.has(channel)) {
|
||||
return;
|
||||
}
|
||||
this.emitter.removeListener(channel, this.subscriptions.get(channel));
|
||||
this.subscriptions.delete(channel);
|
||||
}
|
||||
}
|
||||
|
||||
function createPublisher(): any {
|
||||
return new Publisher(emitter);
|
||||
}
|
||||
|
||||
function createSubscriber(): any {
|
||||
return new Subscriber(emitter);
|
||||
}
|
||||
|
||||
let EventEmitterPubSub = {
|
||||
createPublisher,
|
||||
createSubscriber
|
||||
}
|
||||
|
||||
export {
|
||||
EventEmitterPubSub
|
||||
}
|
||||
22
src/LiveQuery/Id.js
Normal file
22
src/LiveQuery/Id.js
Normal file
@@ -0,0 +1,22 @@
|
||||
class Id {
|
||||
className: string;
|
||||
objectId: string;
|
||||
|
||||
constructor(className: string, objectId: string) {
|
||||
this.className = className;
|
||||
this.objectId = objectId;
|
||||
}
|
||||
toString(): string {
|
||||
return this.className + ':' + this.objectId;
|
||||
}
|
||||
|
||||
static fromString(str: string) {
|
||||
var split = str.split(':');
|
||||
if (split.length !== 2) {
|
||||
throw new TypeError('Cannot create Id object from this string');
|
||||
}
|
||||
return new Id(split[0], split[1]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Id;
|
||||
41
src/LiveQuery/PLog.js
Normal file
41
src/LiveQuery/PLog.js
Normal file
@@ -0,0 +1,41 @@
|
||||
let LogLevel = {
|
||||
'VERBOSE': 0,
|
||||
'DEBUG': 1,
|
||||
'INFO': 2,
|
||||
'ERROR': 3,
|
||||
'NONE': 4
|
||||
}
|
||||
|
||||
function getCurrentLogLevel() {
|
||||
if (PLog.logLevel && PLog.logLevel in LogLevel) {
|
||||
return LogLevel[PLog.logLevel];
|
||||
}
|
||||
return LogLevel['ERROR'];
|
||||
}
|
||||
|
||||
function verbose(): void {
|
||||
if (getCurrentLogLevel() <= LogLevel['VERBOSE']) {
|
||||
console.log.apply(console, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
function log(): void {
|
||||
if (getCurrentLogLevel() <= LogLevel['INFO']) {
|
||||
console.log.apply(console, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
function error(): void {
|
||||
if (getCurrentLogLevel() <= LogLevel['ERROR']) {
|
||||
console.error.apply(console, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
let PLog = {
|
||||
log: log,
|
||||
error: error,
|
||||
verbose: verbose,
|
||||
logLevel: 'INFO'
|
||||
};
|
||||
|
||||
module.exports = PLog;
|
||||
37
src/LiveQuery/ParseCloudCodePublisher.js
Normal file
37
src/LiveQuery/ParseCloudCodePublisher.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ParsePubSub } from './ParsePubSub';
|
||||
import PLog from './PLog';
|
||||
|
||||
class ParseCloudCodePublisher {
|
||||
parsePublisher: Object;
|
||||
|
||||
// config object of the publisher, right now it only contains the redisURL,
|
||||
// but we may extend it later.
|
||||
constructor(config: any = {}) {
|
||||
this.parsePublisher = ParsePubSub.createPublisher(config);
|
||||
}
|
||||
|
||||
onCloudCodeAfterSave(request: any): void {
|
||||
this._onCloudCodeMessage('afterSave', request);
|
||||
}
|
||||
|
||||
onCloudCodeAfterDelete(request: any): void {
|
||||
this._onCloudCodeMessage('afterDelete', request);
|
||||
}
|
||||
|
||||
// Request is the request object from cloud code functions. request.object is a ParseObject.
|
||||
_onCloudCodeMessage(type: string, request: any): void {
|
||||
PLog.verbose('Raw request from cloud code current : %j | original : %j', request.object, request.original);
|
||||
// We need the full JSON which includes className
|
||||
let message = {
|
||||
currentParseObject: request.object._toFullJSON()
|
||||
}
|
||||
if (request.original) {
|
||||
message.originalParseObject = request.original._toFullJSON();
|
||||
}
|
||||
this.parsePublisher.publish(type, JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
ParseCloudCodePublisher
|
||||
}
|
||||
460
src/LiveQuery/ParseLiveQueryServer.js
Normal file
460
src/LiveQuery/ParseLiveQueryServer.js
Normal file
@@ -0,0 +1,460 @@
|
||||
import tv4 from 'tv4';
|
||||
import Parse from 'parse/node';
|
||||
import { Subscription } from './Subscription';
|
||||
import { Client } from './Client';
|
||||
import { ParseWebSocketServer } from './ParseWebSocketServer';
|
||||
import PLog from './PLog';
|
||||
import RequestSchema from './RequestSchema';
|
||||
import { matchesQuery, queryHash } from './QueryTools';
|
||||
import { ParsePubSub } from './ParsePubSub';
|
||||
import { SessionTokenCache } from './SessionTokenCache';
|
||||
|
||||
class ParseLiveQueryServer {
|
||||
clientId: number;
|
||||
clients: Object;
|
||||
// className -> (queryHash -> subscription)
|
||||
subscriptions: Object;
|
||||
parseWebSocketServer: Object;
|
||||
keyPairs : any;
|
||||
// The subscriber we use to get object update from publisher
|
||||
subscriber: Object;
|
||||
|
||||
constructor(server: any, config: any) {
|
||||
this.clientId = 0;
|
||||
this.clients = new Map();
|
||||
this.subscriptions = new Map();
|
||||
|
||||
config = config || {};
|
||||
// Set LogLevel
|
||||
PLog.logLevel = config.logLevel || 'INFO';
|
||||
|
||||
// Store keys, convert obj to map
|
||||
let keyPairs = config.keyPairs || {};
|
||||
this.keyPairs = new Map();
|
||||
for (let key of Object.keys(keyPairs)) {
|
||||
this.keyPairs.set(key, keyPairs[key]);
|
||||
}
|
||||
PLog.verbose('Support key pairs', this.keyPairs);
|
||||
|
||||
// Initialize Parse
|
||||
Parse.Object.disableSingleInstance();
|
||||
Parse.User.enableUnsafeCurrentUser();
|
||||
|
||||
let serverURL = config.serverURL || Parse.serverURL;
|
||||
Parse.serverURL = serverURL;
|
||||
let appId = config.appId || Parse.applicationId;
|
||||
let javascriptKey = Parse.javaScriptKey;
|
||||
let masterKey = config.masterKey || Parse.masterKey;
|
||||
Parse.initialize(appId, javascriptKey, masterKey);
|
||||
|
||||
// Initialize websocket server
|
||||
this.parseWebSocketServer = new ParseWebSocketServer(
|
||||
server,
|
||||
(parseWebsocket) => this._onConnect(parseWebsocket),
|
||||
config.websocketTimeout
|
||||
);
|
||||
|
||||
// Initialize subscriber
|
||||
this.subscriber = ParsePubSub.createSubscriber({
|
||||
redisURL: config.redisURL
|
||||
});
|
||||
this.subscriber.subscribe('afterSave');
|
||||
this.subscriber.subscribe('afterDelete');
|
||||
// Register message handler for subscriber. When publisher get messages, it will publish message
|
||||
// to the subscribers and the handler will be called.
|
||||
this.subscriber.on('message', (channel, messageStr) => {
|
||||
PLog.verbose('Subscribe messsage %j', messageStr);
|
||||
let message = JSON.parse(messageStr);
|
||||
this._inflateParseObject(message);
|
||||
if (channel === 'afterSave') {
|
||||
this._onAfterSave(message);
|
||||
} else if (channel === 'afterDelete') {
|
||||
this._onAfterDelete(message);
|
||||
} else {
|
||||
PLog.error('Get message %s from unknown channel %j', message, channel);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize sessionToken cache
|
||||
this.sessionTokenCache = new SessionTokenCache(config.cacheTimeout);
|
||||
}
|
||||
|
||||
// Message is the JSON object from publisher. Message.currentParseObject is the ParseObject JSON after changes.
|
||||
// Message.originalParseObject is the original ParseObject JSON.
|
||||
_inflateParseObject(message: any): void {
|
||||
// Inflate merged object
|
||||
let currentParseObject = message.currentParseObject;
|
||||
let className = currentParseObject.className;
|
||||
let parseObject = new Parse.Object(className);
|
||||
parseObject._finishFetch(currentParseObject);
|
||||
message.currentParseObject = parseObject;
|
||||
// Inflate original object
|
||||
let originalParseObject = message.originalParseObject;
|
||||
if (originalParseObject) {
|
||||
className = originalParseObject.className;
|
||||
parseObject = new Parse.Object(className);
|
||||
parseObject._finishFetch(originalParseObject);
|
||||
message.originalParseObject = parseObject;
|
||||
}
|
||||
}
|
||||
|
||||
// Message is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes.
|
||||
// Message.originalParseObject is the original ParseObject.
|
||||
_onAfterDelete(message: any): void {
|
||||
PLog.verbose('afterDelete is triggered');
|
||||
|
||||
let deletedParseObject = message.currentParseObject.toJSON();
|
||||
let className = deletedParseObject.className;
|
||||
PLog.verbose('ClassName: %j | ObjectId: %s', className, deletedParseObject.id);
|
||||
PLog.verbose('Current client number : %d', this.clients.size);
|
||||
|
||||
let classSubscriptions = this.subscriptions.get(className);
|
||||
if (typeof classSubscriptions === 'undefined') {
|
||||
PLog.error('Can not find subscriptions under this class ' + className);
|
||||
return;
|
||||
}
|
||||
for (let subscription of classSubscriptions.values()) {
|
||||
let isSubscriptionMatched = this._matchesSubscription(deletedParseObject, subscription);
|
||||
if (!isSubscriptionMatched) {
|
||||
continue;
|
||||
}
|
||||
for (let [clientId, requestIds] of subscription.clientRequestIds.entries()) {
|
||||
let client = this.clients.get(clientId);
|
||||
if (typeof client === 'undefined') {
|
||||
continue;
|
||||
}
|
||||
for (let requestId of requestIds) {
|
||||
let acl = message.currentParseObject.getACL();
|
||||
// Check ACL
|
||||
this._matchesACL(acl, client, requestId).then((isMatched) => {
|
||||
if (!isMatched) {
|
||||
return null;
|
||||
}
|
||||
client.pushDelete(requestId, deletedParseObject);
|
||||
}, (error) => {
|
||||
PLog.error('Matching ACL error : ', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Message is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes.
|
||||
// Message.originalParseObject is the original ParseObject.
|
||||
_onAfterSave(message: any): void {
|
||||
PLog.verbose('afterSave is triggered');
|
||||
|
||||
let originalParseObject = null;
|
||||
if (message.originalParseObject) {
|
||||
originalParseObject = message.originalParseObject.toJSON();
|
||||
}
|
||||
let currentParseObject = message.currentParseObject.toJSON();
|
||||
let className = currentParseObject.className;
|
||||
PLog.verbose('ClassName: %s | ObjectId: %s', className, currentParseObject.id);
|
||||
PLog.verbose('Current client number : %d', this.clients.size);
|
||||
|
||||
let classSubscriptions = this.subscriptions.get(className);
|
||||
if (typeof classSubscriptions === 'undefined') {
|
||||
PLog.error('Can not find subscriptions under this class ' + className);
|
||||
return;
|
||||
}
|
||||
for (let subscription of classSubscriptions.values()) {
|
||||
let isOriginalSubscriptionMatched = this._matchesSubscription(originalParseObject, subscription);
|
||||
let isCurrentSubscriptionMatched = this._matchesSubscription(currentParseObject, subscription);
|
||||
for (let [clientId, requestIds] of subscription.clientRequestIds.entries()) {
|
||||
let client = this.clients.get(clientId);
|
||||
if (typeof client === 'undefined') {
|
||||
continue;
|
||||
}
|
||||
for (let requestId of requestIds) {
|
||||
// Set orignal ParseObject ACL checking promise, if the object does not match
|
||||
// subscription, we do not need to check ACL
|
||||
let originalACLCheckingPromise;
|
||||
if (!isOriginalSubscriptionMatched) {
|
||||
originalACLCheckingPromise = Parse.Promise.as(false);
|
||||
} else {
|
||||
let originalACL;
|
||||
if (message.originalParseObject) {
|
||||
originalACL = message.originalParseObject.getACL();
|
||||
}
|
||||
originalACLCheckingPromise = this._matchesACL(originalACL, client, requestId);
|
||||
}
|
||||
// Set current ParseObject ACL checking promise, if the object does not match
|
||||
// subscription, we do not need to check ACL
|
||||
let currentACLCheckingPromise;
|
||||
if (!isCurrentSubscriptionMatched) {
|
||||
currentACLCheckingPromise = Parse.Promise.as(false);
|
||||
} else {
|
||||
let currentACL = message.currentParseObject.getACL();
|
||||
currentACLCheckingPromise = this._matchesACL(currentACL, client, requestId);
|
||||
}
|
||||
|
||||
Parse.Promise.when(
|
||||
originalACLCheckingPromise,
|
||||
currentACLCheckingPromise
|
||||
).then((isOriginalMatched, isCurrentMatched) => {
|
||||
PLog.verbose('Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s',
|
||||
originalParseObject,
|
||||
currentParseObject,
|
||||
isOriginalSubscriptionMatched,
|
||||
isCurrentSubscriptionMatched,
|
||||
isOriginalMatched,
|
||||
isCurrentMatched,
|
||||
subscription.hash
|
||||
);
|
||||
|
||||
// Decide event type
|
||||
let type;
|
||||
if (isOriginalMatched && isCurrentMatched) {
|
||||
type = 'Update';
|
||||
} else if (isOriginalMatched && !isCurrentMatched) {
|
||||
type = 'Leave';
|
||||
} else if (!isOriginalMatched && isCurrentMatched) {
|
||||
if (originalParseObject) {
|
||||
type = 'Enter';
|
||||
} else {
|
||||
type = 'Create';
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
let functionName = 'push' + type;
|
||||
client[functionName](requestId, currentParseObject);
|
||||
}, (error) => {
|
||||
PLog.error('Matching ACL error : ', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onConnect(parseWebsocket: any): void {
|
||||
parseWebsocket.on('message', (request) => {
|
||||
if (typeof request === 'string') {
|
||||
request = JSON.parse(request);
|
||||
}
|
||||
PLog.verbose('Request: %j', request);
|
||||
|
||||
// Check whether this request is a valid request, return error directly if not
|
||||
if (!tv4.validate(request, RequestSchema['general']) || !tv4.validate(request, RequestSchema[request.op])) {
|
||||
Client.pushError(parseWebsocket, 1, tv4.error.message);
|
||||
PLog.error('Connect message error %s', tv4.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
switch(request.op) {
|
||||
case 'connect':
|
||||
this._handleConnect(parseWebsocket, request);
|
||||
break;
|
||||
case 'subscribe':
|
||||
this._handleSubscribe(parseWebsocket, request);
|
||||
break;
|
||||
case 'unsubscribe':
|
||||
this._handleUnsubscribe(parseWebsocket, request);
|
||||
break;
|
||||
default:
|
||||
Client.pushError(parseWebsocket, 3, 'Get unknown operation');
|
||||
PLog.error('Get unknown operation', request.op);
|
||||
}
|
||||
});
|
||||
|
||||
parseWebsocket.on('disconnect', () => {
|
||||
PLog.log('Client disconnect: %d', parseWebsocket.clientId);
|
||||
let clientId = parseWebsocket.clientId;
|
||||
if (!this.clients.has(clientId)) {
|
||||
PLog.error('Can not find client %d on disconnect', clientId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete client
|
||||
let client = this.clients.get(clientId);
|
||||
this.clients.delete(clientId);
|
||||
|
||||
// Delete client from subscriptions
|
||||
for (let [requestId, subscriptionInfo] of client.subscriptionInfos.entries()) {
|
||||
let subscription = subscriptionInfo.subscription;
|
||||
subscription.deleteClientSubscription(clientId, requestId);
|
||||
|
||||
// If there is no client which is subscribing this subscription, remove it from subscriptions
|
||||
let classSubscriptions = this.subscriptions.get(subscription.className);
|
||||
if (!subscription.hasSubscribingClient()) {
|
||||
classSubscriptions.delete(subscription.hash);
|
||||
}
|
||||
// If there is no subscriptions under this class, remove it from subscriptions
|
||||
if (classSubscriptions.size === 0) {
|
||||
this.subscriptions.delete(subscription.className);
|
||||
}
|
||||
}
|
||||
|
||||
PLog.verbose('Current clients %d', this.clients.size);
|
||||
PLog.verbose('Current subscriptions %d', this.subscriptions.size);
|
||||
});
|
||||
}
|
||||
|
||||
_matchesSubscription(parseObject: any, subscription: any): boolean {
|
||||
// Object is undefined or null, not match
|
||||
if (!parseObject) {
|
||||
return false;
|
||||
}
|
||||
return matchesQuery(parseObject, subscription.query);
|
||||
}
|
||||
|
||||
_matchesACL(acl: any, client: any, requestId: number): any {
|
||||
// If ACL is undefined or null, or ACL has public read access, return true directly
|
||||
if (!acl || acl.getPublicReadAccess()) {
|
||||
return Parse.Promise.as(true);
|
||||
}
|
||||
// Check subscription sessionToken matches ACL first
|
||||
let subscriptionInfo = client.getSubscriptionInfo(requestId);
|
||||
if (typeof subscriptionInfo === 'undefined') {
|
||||
return Parse.Promise.as(false);
|
||||
}
|
||||
|
||||
let subscriptionSessionToken = subscriptionInfo.sessionToken;
|
||||
return this.sessionTokenCache.getUserId(subscriptionSessionToken).then((userId) => {
|
||||
return acl.getReadAccess(userId);
|
||||
}).then((isSubscriptionSessionTokenMatched) => {
|
||||
if (isSubscriptionSessionTokenMatched) {
|
||||
return Parse.Promise.as(true);
|
||||
}
|
||||
// Check client sessionToken matches ACL
|
||||
let clientSessionToken = client.sessionToken;
|
||||
return this.sessionTokenCache.getUserId(clientSessionToken).then((userId) => {
|
||||
return acl.getReadAccess(userId);
|
||||
});
|
||||
}).then((isMatched) => {
|
||||
return Parse.Promise.as(isMatched);
|
||||
}, (error) => {
|
||||
return Parse.Promise.as(false);
|
||||
});
|
||||
}
|
||||
|
||||
_handleConnect(parseWebsocket: any, request: any): any {
|
||||
if (!this._validateKeys(request, this.keyPairs)) {
|
||||
Client.pushError(parseWebsocket, 4, 'Key in request is not valid');
|
||||
PLog.error('Key in request is not valid');
|
||||
return;
|
||||
}
|
||||
let client = new Client(this.clientId, parseWebsocket);
|
||||
parseWebsocket.clientId = this.clientId;
|
||||
this.clientId += 1;
|
||||
this.clients.set(parseWebsocket.clientId, client);
|
||||
PLog.log('Create new client: %d', parseWebsocket.clientId);
|
||||
client.pushConnect();
|
||||
}
|
||||
|
||||
_validateKeys(request: any, validKeyPairs: any): boolean {
|
||||
if (!validKeyPairs || validKeyPairs.size == 0) {
|
||||
return true;
|
||||
}
|
||||
let isValid = false;
|
||||
for (let [key, secret] of validKeyPairs) {
|
||||
if (!request[key] || request[key] !== secret) {
|
||||
continue;
|
||||
}
|
||||
isValid = true;
|
||||
break;
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
|
||||
_handleSubscribe(parseWebsocket: any, request: any): any {
|
||||
// If we can not find this client, return error to client
|
||||
if (!parseWebsocket.hasOwnProperty('clientId')) {
|
||||
Client.pushError(parseWebsocket, 2, 'Can not find this client, make sure you connect to server before subscribing');
|
||||
PLog.error('Can not find this client, make sure you connect to server before subscribing');
|
||||
return;
|
||||
}
|
||||
let client = this.clients.get(parseWebsocket.clientId);
|
||||
|
||||
// Get subscription from subscriptions, create one if necessary
|
||||
let subscriptionHash = queryHash(request.query);
|
||||
// Add className to subscriptions if necessary
|
||||
let className = request.query.className;
|
||||
if (!this.subscriptions.has(className)) {
|
||||
this.subscriptions.set(className, new Map());
|
||||
}
|
||||
let classSubscriptions = this.subscriptions.get(className);
|
||||
let subscription;
|
||||
if (classSubscriptions.has(subscriptionHash)) {
|
||||
subscription = classSubscriptions.get(subscriptionHash);
|
||||
} else {
|
||||
subscription = new Subscription(className, request.query.where, subscriptionHash);
|
||||
classSubscriptions.set(subscriptionHash, subscription);
|
||||
}
|
||||
|
||||
// Add subscriptionInfo to client
|
||||
let subscriptionInfo = {
|
||||
subscription: subscription
|
||||
};
|
||||
// Add selected fields and sessionToken for this subscription if necessary
|
||||
if (request.query.fields) {
|
||||
subscriptionInfo.fields = request.query.fields;
|
||||
}
|
||||
if (request.sessionToken) {
|
||||
subscriptionInfo.sessionToken = request.sessionToken;
|
||||
}
|
||||
client.addSubscriptionInfo(request.requestId, subscriptionInfo);
|
||||
|
||||
// Add clientId to subscription
|
||||
subscription.addClientSubscription(parseWebsocket.clientId, request.requestId);
|
||||
|
||||
client.pushSubscribe(request.requestId);
|
||||
|
||||
PLog.verbose('Create client %d new subscription: %d', parseWebsocket.clientId, request.requestId);
|
||||
PLog.verbose('Current client number: %d', this.clients.size);
|
||||
}
|
||||
|
||||
_handleUnsubscribe(parseWebsocket: any, request: any): any {
|
||||
// If we can not find this client, return error to client
|
||||
if (!parseWebsocket.hasOwnProperty('clientId')) {
|
||||
Client.pushError(parseWebsocket, 2, 'Can not find this client, make sure you connect to server before unsubscribing');
|
||||
PLog.error('Can not find this client, make sure you connect to server before unsubscribing');
|
||||
return;
|
||||
}
|
||||
let requestId = request.requestId;
|
||||
let client = this.clients.get(parseWebsocket.clientId);
|
||||
if (typeof client === 'undefined') {
|
||||
Client.pushError(parseWebsocket, 2, 'Cannot find client with clientId ' + parseWebsocket.clientId +
|
||||
'. Make sure you connect to live query server before unsubscribing.');
|
||||
PLog.error('Can not find this client ' + parseWebsocket.clientId);
|
||||
return;
|
||||
}
|
||||
|
||||
let subscriptionInfo = client.getSubscriptionInfo(requestId);
|
||||
if (typeof subscriptionInfo === 'undefined') {
|
||||
Client.pushError(parseWebsocket, 2, 'Cannot find subscription with clientId ' + parseWebsocket.clientId +
|
||||
' subscriptionId ' + requestId + '. Make sure you subscribe to live query server before unsubscribing.');
|
||||
PLog.error('Can not find subscription with clientId ' + parseWebsocket.clientId + ' subscriptionId ' + requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove subscription from client
|
||||
client.deleteSubscriptionInfo(requestId);
|
||||
// Remove client from subscription
|
||||
let subscription = subscriptionInfo.subscription;
|
||||
let className = subscription.className;
|
||||
subscription.deleteClientSubscription(parseWebsocket.clientId, requestId);
|
||||
// If there is no client which is subscribing this subscription, remove it from subscriptions
|
||||
let classSubscriptions = this.subscriptions.get(className);
|
||||
if (!subscription.hasSubscribingClient()) {
|
||||
classSubscriptions.delete(subscription.hash);
|
||||
}
|
||||
// If there is no subscriptions under this class, remove it from subscriptions
|
||||
if (classSubscriptions.size === 0) {
|
||||
this.subscriptions.delete(className);
|
||||
}
|
||||
|
||||
client.pushUnsubscribe(request.requestId);
|
||||
|
||||
PLog.verbose('Delete client: %d | subscription: %d', parseWebsocket.clientId, request.requestId);
|
||||
}
|
||||
}
|
||||
|
||||
ParseLiveQueryServer.setLogLevel = function(logLevel) {
|
||||
PLog.logLevel = logLevel;
|
||||
}
|
||||
|
||||
export {
|
||||
ParseLiveQueryServer
|
||||
}
|
||||
29
src/LiveQuery/ParsePubSub.js
Normal file
29
src/LiveQuery/ParsePubSub.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { RedisPubSub } from './RedisPubSub';
|
||||
import { EventEmitterPubSub } from './EventEmitterPubSub';
|
||||
|
||||
let ParsePubSub = {};
|
||||
|
||||
function useRedis(config: any): boolean {
|
||||
let redisURL = config.redisURL;
|
||||
return typeof redisURL !== 'undefined' && redisURL !== '';
|
||||
}
|
||||
|
||||
ParsePubSub.createPublisher = function(config: any): any {
|
||||
if (useRedis(config)) {
|
||||
return RedisPubSub.createPublisher(config.redisURL);
|
||||
} else {
|
||||
return EventEmitterPubSub.createPublisher();
|
||||
}
|
||||
}
|
||||
|
||||
ParsePubSub.createSubscriber = function(config: any): void {
|
||||
if (useRedis(config)) {
|
||||
return RedisPubSub.createSubscriber(config.redisURL);
|
||||
} else {
|
||||
return EventEmitterPubSub.createSubscriber();
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
ParsePubSub
|
||||
}
|
||||
44
src/LiveQuery/ParseWebSocketServer.js
Normal file
44
src/LiveQuery/ParseWebSocketServer.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import PLog from './PLog';
|
||||
|
||||
let typeMap = new Map([['disconnect', 'close']]);
|
||||
|
||||
export class ParseWebSocketServer {
|
||||
server: Object;
|
||||
|
||||
constructor(server: any, onConnect: Function, websocketTimeout: number = 10 * 1000) {
|
||||
let WebSocketServer = require('ws').Server;
|
||||
let wss = new WebSocketServer({ server: server });
|
||||
wss.on('listening', () => {
|
||||
PLog.log('Parse LiveQuery Server starts running');
|
||||
});
|
||||
wss.on('connection', (ws) => {
|
||||
onConnect(new ParseWebSocket(ws));
|
||||
// Send ping to client periodically
|
||||
let pingIntervalId = setInterval(() => {
|
||||
if (ws.readyState == ws.OPEN) {
|
||||
ws.ping();
|
||||
} else {
|
||||
clearInterval(pingIntervalId);
|
||||
}
|
||||
}, websocketTimeout);
|
||||
});
|
||||
this.server = wss;
|
||||
}
|
||||
}
|
||||
|
||||
export class ParseWebSocket {
|
||||
ws: any;
|
||||
|
||||
constructor(ws: any) {
|
||||
this.ws = ws;
|
||||
}
|
||||
|
||||
on(type: string, callback): void {
|
||||
let wsType = typeMap.has(type) ? typeMap.get(type) : type;
|
||||
this.ws.on(wsType, callback);
|
||||
}
|
||||
|
||||
send(message: any, channel: string): void {
|
||||
this.ws.send(message);
|
||||
}
|
||||
}
|
||||
280
src/LiveQuery/QueryTools.js
Normal file
280
src/LiveQuery/QueryTools.js
Normal file
@@ -0,0 +1,280 @@
|
||||
var equalObjects = require('./equalObjects');
|
||||
var Id = require('./Id');
|
||||
var Parse = require('parse/node');
|
||||
|
||||
/**
|
||||
* Query Hashes are deterministic hashes for Parse Queries.
|
||||
* Any two queries that have the same set of constraints will produce the same
|
||||
* hash. This lets us reliably group components by the queries they depend upon,
|
||||
* and quickly determine if a query has changed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert $or queries into an array of where conditions
|
||||
*/
|
||||
function flattenOrQueries(where) {
|
||||
if (!where.hasOwnProperty('$or')) {
|
||||
return where;
|
||||
}
|
||||
var accum = [];
|
||||
for (var i = 0; i < where.$or.length; i++) {
|
||||
accum = accum.concat(where.$or[i]);
|
||||
}
|
||||
return accum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministically turns an object into a string. Disregards ordering
|
||||
*/
|
||||
function stringify(object): string {
|
||||
if (typeof object !== 'object' || object === null) {
|
||||
if (typeof object === 'string') {
|
||||
return '"' + object.replace(/\|/g, '%|') + '"';
|
||||
}
|
||||
return object + '';
|
||||
}
|
||||
if (Array.isArray(object)) {
|
||||
var copy = object.map(stringify);
|
||||
copy.sort();
|
||||
return '[' + copy.join(',') + ']';
|
||||
}
|
||||
var sections = [];
|
||||
var keys = Object.keys(object);
|
||||
keys.sort();
|
||||
for (var k = 0; k < keys.length; k++) {
|
||||
sections.push(stringify(keys[k]) + ':' + stringify(object[keys[k]]));
|
||||
}
|
||||
return '{' + sections.join(',') + '}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a hash from a query, with unique fields for columns, values, order,
|
||||
* skip, and limit.
|
||||
*/
|
||||
function queryHash(query) {
|
||||
if (query instanceof Parse.Query) {
|
||||
query = {
|
||||
className: query.className,
|
||||
where: query._where
|
||||
}
|
||||
}
|
||||
var where = flattenOrQueries(query.where || {});
|
||||
var columns = [];
|
||||
var values = [];
|
||||
var i;
|
||||
if (Array.isArray(where)) {
|
||||
var uniqueColumns = {};
|
||||
for (i = 0; i < where.length; i++) {
|
||||
var subValues = {};
|
||||
var keys = Object.keys(where[i]);
|
||||
keys.sort();
|
||||
for (var j = 0; j < keys.length; j++) {
|
||||
subValues[keys[j]] = where[i][keys[j]];
|
||||
uniqueColumns[keys[j]] = true;
|
||||
}
|
||||
values.push(subValues);
|
||||
}
|
||||
columns = Object.keys(uniqueColumns);
|
||||
columns.sort();
|
||||
} else {
|
||||
columns = Object.keys(where);
|
||||
columns.sort();
|
||||
for (i = 0; i < columns.length; i++) {
|
||||
values.push(where[columns[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
var sections = [columns.join(','), stringify(values)];
|
||||
|
||||
return query.className + ':' + sections.join('|');
|
||||
}
|
||||
|
||||
/**
|
||||
* matchesQuery -- Determines if an object would be returned by a Parse Query
|
||||
* It's a lightweight, where-clause only implementation of a full query engine.
|
||||
* Since we find queries that match objects, rather than objects that match
|
||||
* queries, we can avoid building a full-blown query tool.
|
||||
*/
|
||||
function matchesQuery(object: any, query: any): boolean {
|
||||
if (query instanceof Parse.Query) {
|
||||
var className =
|
||||
(object.id instanceof Id) ? object.id.className : object.className;
|
||||
if (className !== query.className) {
|
||||
return false;
|
||||
}
|
||||
return matchesQuery(object, query._where);
|
||||
}
|
||||
for (var field in query) {
|
||||
if (!matchesKeyConstraints(object, field, query[field])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determines whether an object matches a single key's constraints
|
||||
*/
|
||||
function matchesKeyConstraints(object, key, constraints) {
|
||||
var i;
|
||||
if (key === '$or') {
|
||||
for (i = 0; i < constraints.length; i++) {
|
||||
if (matchesQuery(object, constraints[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (key === '$relatedTo') {
|
||||
// Bail! We can't handle relational queries locally
|
||||
return false;
|
||||
}
|
||||
// Equality (or Array contains) cases
|
||||
if (typeof constraints !== 'object') {
|
||||
if (Array.isArray(object[key])) {
|
||||
return object[key].indexOf(constraints) > -1;
|
||||
}
|
||||
return object[key] === constraints;
|
||||
}
|
||||
var compareTo;
|
||||
if (constraints.__type) {
|
||||
if (constraints.__type === 'Pointer') {
|
||||
return (
|
||||
constraints.className === object[key].className &&
|
||||
constraints.objectId === object[key].objectId
|
||||
);
|
||||
}
|
||||
compareTo = Parse._decode(key, constraints);
|
||||
if (Array.isArray(object[key])) {
|
||||
for (i = 0; i < object[key].length; i++) {
|
||||
if (equalObjects(object[key][i], compareTo)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return equalObjects(object[key], compareTo);
|
||||
}
|
||||
// More complex cases
|
||||
for (var condition in constraints) {
|
||||
compareTo = constraints[condition];
|
||||
if (compareTo.__type) {
|
||||
compareTo = Parse._decode(key, compareTo);
|
||||
}
|
||||
switch (condition) {
|
||||
case '$lt':
|
||||
if (object[key] >= compareTo) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$lte':
|
||||
if (object[key] > compareTo) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$gt':
|
||||
if (object[key] <= compareTo) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$gte':
|
||||
if (object[key] < compareTo) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$ne':
|
||||
if (equalObjects(object[key], compareTo)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$in':
|
||||
if (compareTo.indexOf(object[key]) < 0) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$nin':
|
||||
if (compareTo.indexOf(object[key]) > -1) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$all':
|
||||
for (i = 0; i < compareTo.length; i++) {
|
||||
if (object[key].indexOf(compareTo[i]) < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case '$exists':
|
||||
if (typeof object[key] === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$regex':
|
||||
if (typeof compareTo === 'object') {
|
||||
return compareTo.test(object[key]);
|
||||
}
|
||||
// JS doesn't support perl-style escaping
|
||||
var expString = '';
|
||||
var escapeEnd = -2;
|
||||
var escapeStart = compareTo.indexOf('\\Q');
|
||||
while (escapeStart > -1) {
|
||||
// Add the unescaped portion
|
||||
expString += compareTo.substring(escapeEnd + 2, escapeStart);
|
||||
escapeEnd = compareTo.indexOf('\\E', escapeStart);
|
||||
if (escapeEnd > -1) {
|
||||
expString += compareTo.substring(escapeStart + 2, escapeEnd)
|
||||
.replace(/\\\\\\\\E/g, '\\E').replace(/\W/g, '\\$&');
|
||||
}
|
||||
|
||||
escapeStart = compareTo.indexOf('\\Q', escapeEnd);
|
||||
}
|
||||
expString += compareTo.substring(Math.max(escapeStart, escapeEnd + 2));
|
||||
var exp = new RegExp(expString, constraints.$options || '');
|
||||
if (!exp.test(object[key])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$nearSphere':
|
||||
var distance = compareTo.radiansTo(object[key]);
|
||||
var max = constraints.$maxDistance || Infinity;
|
||||
return distance <= max;
|
||||
case '$within':
|
||||
var southWest = compareTo.$box[0];
|
||||
var northEast = compareTo.$box[1];
|
||||
if (southWest.latitude > northEast.latitude ||
|
||||
southWest.longitude > northEast.longitude) {
|
||||
// Invalid box, crosses the date line
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
object[key].latitude > southWest.latitude &&
|
||||
object[key].latitude < northEast.latitude &&
|
||||
object[key].longitude > southWest.longitude &&
|
||||
object[key].longitude < northEast.longitude
|
||||
);
|
||||
case '$options':
|
||||
// Not a query type, but a way to add options to $regex. Ignore and
|
||||
// avoid the default
|
||||
break;
|
||||
case '$maxDistance':
|
||||
// Not a query type, but a way to add a cap to $nearSphere. Ignore and
|
||||
// avoid the default
|
||||
break;
|
||||
case '$select':
|
||||
return false;
|
||||
case '$dontSelect':
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
var QueryTools = {
|
||||
queryHash: queryHash,
|
||||
matchesQuery: matchesQuery
|
||||
};
|
||||
|
||||
module.exports = QueryTools;
|
||||
18
src/LiveQuery/RedisPubSub.js
Normal file
18
src/LiveQuery/RedisPubSub.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import redis from 'redis';
|
||||
|
||||
function createPublisher(redisURL: string): any {
|
||||
return redis.createClient(redisURL, { no_ready_check: true });
|
||||
}
|
||||
|
||||
function createSubscriber(redisURL: string): any {
|
||||
return redis.createClient(redisURL, { no_ready_check: true });
|
||||
}
|
||||
|
||||
let RedisPubSub = {
|
||||
createPublisher,
|
||||
createSubscriber
|
||||
}
|
||||
|
||||
export {
|
||||
RedisPubSub
|
||||
}
|
||||
101
src/LiveQuery/RequestSchema.js
Normal file
101
src/LiveQuery/RequestSchema.js
Normal file
@@ -0,0 +1,101 @@
|
||||
let general = {
|
||||
'title': 'General request schema',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'op': {
|
||||
'type': 'string',
|
||||
'enum': ['connect', 'subscribe', 'unsubscribe']
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let connect = {
|
||||
'title': 'Connect operation schema',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'op': 'connect',
|
||||
'applicationId': {
|
||||
'type': 'string'
|
||||
},
|
||||
'javascriptKey': {
|
||||
type: 'string'
|
||||
},
|
||||
'masterKey': {
|
||||
type: 'string'
|
||||
},
|
||||
'clientKey': {
|
||||
type: 'string'
|
||||
},
|
||||
'windowsKey': {
|
||||
type: 'string'
|
||||
},
|
||||
'restAPIKey': {
|
||||
'type': 'string'
|
||||
},
|
||||
'sessionToken': {
|
||||
'type': 'string'
|
||||
}
|
||||
},
|
||||
'required': ['op', 'applicationId'],
|
||||
"additionalProperties": false
|
||||
};
|
||||
|
||||
let subscribe = {
|
||||
'title': 'Subscribe operation schema',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'op': 'subscribe',
|
||||
'requestId': {
|
||||
'type': 'number'
|
||||
},
|
||||
'query': {
|
||||
'title': 'Query field schema',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'className': {
|
||||
'type': 'string'
|
||||
},
|
||||
'where': {
|
||||
'type': 'object'
|
||||
},
|
||||
'fields': {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
}
|
||||
},
|
||||
'required': ['where', 'className'],
|
||||
'additionalProperties': false
|
||||
},
|
||||
'sessionToken': {
|
||||
'type': 'string'
|
||||
}
|
||||
},
|
||||
'required': ['op', 'requestId', 'query'],
|
||||
'additionalProperties': false
|
||||
};
|
||||
|
||||
let unsubscribe = {
|
||||
'title': 'Unsubscribe operation schema',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'op': 'unsubscribe',
|
||||
'requestId': {
|
||||
'type': 'number'
|
||||
}
|
||||
},
|
||||
'required': ['op', 'requestId'],
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
||||
let RequestSchema = {
|
||||
'general': general,
|
||||
'connect': connect,
|
||||
'subscribe': subscribe,
|
||||
'unsubscribe': unsubscribe
|
||||
}
|
||||
|
||||
export default RequestSchema;
|
||||
38
src/LiveQuery/SessionTokenCache.js
Normal file
38
src/LiveQuery/SessionTokenCache.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import Parse from 'parse/node';
|
||||
import LRU from 'lru-cache';
|
||||
import PLog from './PLog';
|
||||
|
||||
class SessionTokenCache {
|
||||
cache: Object;
|
||||
|
||||
constructor(timeout: number = 30 * 24 * 60 *60 * 1000, maxSize: number = 10000) {
|
||||
this.cache = new LRU({
|
||||
max: maxSize,
|
||||
maxAge: timeout
|
||||
});
|
||||
}
|
||||
|
||||
getUserId(sessionToken: string): any {
|
||||
if (!sessionToken) {
|
||||
return Parse.Promise.error('Empty sessionToken');
|
||||
}
|
||||
let userId = this.cache.get(sessionToken);
|
||||
if (userId) {
|
||||
PLog.verbose('Fetch userId %s of sessionToken %s from Cache', userId, sessionToken);
|
||||
return Parse.Promise.as(userId);
|
||||
}
|
||||
return Parse.User.become(sessionToken).then((user) => {
|
||||
PLog.verbose('Fetch userId %s of sessionToken %s from Parse', user.id, sessionToken);
|
||||
let userId = user.id;
|
||||
this.cache.set(sessionToken, userId);
|
||||
return Parse.Promise.as(userId);
|
||||
}, (error) => {
|
||||
PLog.error('Can not fetch userId for sessionToken %j, error %j', sessionToken, error);
|
||||
return Parse.Promise.error(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
SessionTokenCache
|
||||
}
|
||||
55
src/LiveQuery/Subscription.js
Normal file
55
src/LiveQuery/Subscription.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import {matchesQuery, queryHash} from './QueryTools';
|
||||
import PLog from './PLog';
|
||||
|
||||
export type FlattenedObjectData = { [attr: string]: any };
|
||||
export type QueryData = { [attr: string]: any };
|
||||
|
||||
class Subscription {
|
||||
// It is query condition eg query.where
|
||||
query: QueryData;
|
||||
className: string;
|
||||
hash: string;
|
||||
clientRequestIds: Object;
|
||||
|
||||
constructor(className: string, query: QueryData, queryHash: string) {
|
||||
this.className = className;
|
||||
this.query = query;
|
||||
this.hash = queryHash;
|
||||
this.clientRequestIds = new Map();
|
||||
}
|
||||
|
||||
addClientSubscription(clientId: number, requestId: number): void {
|
||||
if (!this.clientRequestIds.has(clientId)) {
|
||||
this.clientRequestIds.set(clientId, []);
|
||||
}
|
||||
let requestIds = this.clientRequestIds.get(clientId);
|
||||
requestIds.push(requestId);
|
||||
}
|
||||
|
||||
deleteClientSubscription(clientId: number, requestId: number): void {
|
||||
let requestIds = this.clientRequestIds.get(clientId);
|
||||
if (typeof requestIds === 'undefined') {
|
||||
PLog.error('Can not find client %d to delete', clientId);
|
||||
return;
|
||||
}
|
||||
|
||||
let index = requestIds.indexOf(requestId);
|
||||
if (index < 0) {
|
||||
PLog.error('Can not find client %d subscription %d to delete', clientId, requestId);
|
||||
return;
|
||||
}
|
||||
requestIds.splice(index, 1);
|
||||
// Delete client reference if it has no subscription
|
||||
if (requestIds.length == 0) {
|
||||
this.clientRequestIds.delete(clientId);
|
||||
}
|
||||
}
|
||||
|
||||
hasSubscribingClient(): boolean {
|
||||
return this.clientRequestIds.size > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
Subscription
|
||||
}
|
||||
48
src/LiveQuery/equalObjects.js
Normal file
48
src/LiveQuery/equalObjects.js
Normal file
@@ -0,0 +1,48 @@
|
||||
var toString = Object.prototype.toString;
|
||||
|
||||
/**
|
||||
* Determines whether two objects represent the same primitive, special Parse
|
||||
* type, or full Parse Object.
|
||||
*/
|
||||
function equalObjects(a, b) {
|
||||
if (typeof a !== typeof b) {
|
||||
return false;
|
||||
}
|
||||
if (typeof a !== 'object') {
|
||||
return (a === b);
|
||||
}
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
if (toString.call(a) === '[object Date]') {
|
||||
if (toString.call(b) === '[object Date]') {
|
||||
return (+a === +b);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(a)) {
|
||||
if (Array.isArray(b)) {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; i < a.length; i++) {
|
||||
if (!equalObjects(a[i], b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (Object.keys(a).length !== Object.keys(b).length) {
|
||||
return false;
|
||||
}
|
||||
for (var key in a) {
|
||||
if (!equalObjects(a[key], b[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = equalObjects;
|
||||
Reference in New Issue
Block a user