syncable-server
Version:
Syncable server.
301 lines • 11.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const events_1 = require("events");
const uuid = require("uuid");
const object_queue_1 = require("./object-queue");
class ChangeEmitter extends object_queue_1.ObjectQueue {
constructor(socket) {
super();
this.socket = socket;
}
emit(object) {
this.socket.emit('change', object);
}
resolveChannel({ change: { subject }, }) {
return subject;
}
}
exports.ChangeEmitter = ChangeEmitter;
class Server extends events_1.EventEmitter {
constructor(socketServer) {
super();
this.socketServer = socketServer;
this.errorEmitter = this.emit.bind(this, 'error');
this.subjectToDefinitionMap = new Map();
this.subjectToSocketSetMap = new Map();
this.initChangeQueueListener();
this.initSocketServer();
}
register(subject, definition) {
definition._server = this;
this.subjectToDefinitionMap.set(subject, definition);
}
async spawnChange(change, session) {
let { uid, subject, resource } = change;
let definition = this.subjectToDefinitionMap.get(subject);
let lock;
if (resource) {
lock = await this.lock(`resource-${resource}`, 1000);
}
let snapshot;
try {
let timestamp = await this.generateTimestamp();
if (isCreation(change)) {
snapshot = await definition.create(change, timestamp, session);
let queuedBroadcastCreation = {
uid,
subject,
resource: snapshot.uid,
type: 'create',
snapshot,
timestamp,
};
await this.queueChange(queuedBroadcastCreation, session);
}
else if (isRemoval(change)) {
await definition.remove(change, timestamp, session);
let queuedBroadcastRemoval = Object.assign({}, change, { timestamp });
await this.queueChange(queuedBroadcastRemoval, session);
}
else {
snapshot = await definition.update(change, timestamp, session);
let queuedBroadcastChange = Object.assign({}, change, { snapshot,
timestamp });
await this.queueChange(queuedBroadcastChange, session);
}
}
finally {
if (lock) {
await lock.release();
}
}
return snapshot;
}
initChangeQueueListener() {
this.on('change', ({ change, session }) => {
try {
this.handleChangeFromQueue(change, session);
}
catch (error) {
this.emit('error', error);
}
});
}
initSocketServer() {
this.socketServer.on('connect', socket => {
socket.subjectToSubscriptionInfoMap = new Map();
socket.on('subscribe', subscription => {
let { subjectToSubscriptionInfoMap } = socket;
let { subject, loaded } = subscription;
let existingInfo = subjectToSubscriptionInfoMap.get(subject);
if (existingInfo) {
existingInfo.valid = false;
}
let info = {
subscription,
changeEmitter: new ChangeEmitter(socket),
visibleSet: new Set(loaded),
valid: true,
};
subjectToSubscriptionInfoMap.set(subject, info);
let { subjectToSocketSetMap } = this;
if (subjectToSocketSetMap.has(subject)) {
subjectToSocketSetMap.get(subject).add(socket);
}
else {
subjectToSocketSetMap.set(subject, new Set([socket]));
}
socket.emit('subscribed', subscription);
this.initSubscription(info, socket).catch(this.errorEmitter);
});
socket.on('request', request => {
this.loadAndEmitUponRequest(request, socket).catch(this.errorEmitter);
});
socket.on('change', change => {
let session = this.getSession(socket);
this.spawnChange(change, session).catch(this.errorEmitter);
});
socket.on('close', () => {
for (let subject of socket.subjectToSubscriptionInfoMap.keys()) {
this.subjectToSocketSetMap.get(subject).delete(socket);
}
});
});
}
async initSubscription(info, socket) {
let { subscription: { timestamp } } = info;
if (typeof timestamp === 'number') {
await this.loadAndEmitChanges(info, socket);
}
else {
await this.loadAndEmitSnapshots(info, socket);
}
}
async loadAndEmitSnapshots(info, socket) {
let { changeEmitter, subscription, visibleSet } = info;
let { subject } = subscription;
changeEmitter.pause(subject);
let definition = this.subjectToDefinitionMap.get(subject);
let snapshotsTimestamp = await this.generateTimestamp();
let snapshots = await definition.loadSnapshots(subscription, socket);
if (!info.valid) {
return;
}
for (let { uid, timestamp } of snapshots) {
visibleSet.add(uid);
snapshotsTimestamp = Math.max(snapshotsTimestamp, timestamp);
}
let data = Object.assign({ snapshots, timestamp: snapshotsTimestamp }, subscription);
socket.emit('snapshots', data);
let resourceToTimestampMap = new Map();
for (let { uid, timestamp } of snapshots) {
resourceToTimestampMap.set(uid, timestamp);
}
changeEmitter.resume(subject, ({ change: { resource, timestamp } }) => {
let resourceTimestamp = resourceToTimestampMap.get(resource);
return !resourceTimestamp || resourceTimestamp < timestamp;
});
}
async loadAndEmitUponRequest(request, socket) {
let { subject, resources } = request;
let { subjectToSubscriptionInfoMap } = socket;
let { changeEmitter, visibleSet, subscription, } = subjectToSubscriptionInfoMap.get(subject);
resources = resources.filter(resource => !visibleSet.has(resource));
if (!resources.length) {
return;
}
changeEmitter.pause(subject);
let definition = this.subjectToDefinitionMap.get(subject);
let snapshots = await definition.loadSnapshotsUponRequest(resources, subscription, socket);
for (let snapshot of snapshots) {
let { uid: resource, timestamp } = snapshot;
let creation = {
uid: uuid(),
subject,
resource,
type: 'create',
snapshot,
timestamp,
};
socket.emit('change', {
change: creation,
session: undefined,
});
visibleSet.add(resource);
}
changeEmitter.resume(subject);
}
async loadAndEmitChanges(info, socket) {
let { changeEmitter, subscription } = info;
let { subject } = subscription;
changeEmitter.pause(subject);
let definition = this.subjectToDefinitionMap.get(subject);
let changes = await definition.loadChanges(subscription, socket);
if (!info.valid) {
return;
}
for (let change of changes) {
socket.emit('change', change);
}
let latestTimestamp = changes.length
? changes[changes.length - 1].change.timestamp
: 0;
changeEmitter.resume(subject, ({ change: { timestamp } }) => timestamp > latestTimestamp);
}
handleChangeFromQueue(change, session) {
let { subject, resource } = change;
let socketSet = this.subjectToSocketSetMap.get(subject);
if (!socketSet) {
return;
}
let clientSession = this.resolveClientSession(session);
let definition = this.subjectToDefinitionMap.get(subject);
for (let socket of socketSet) {
let { subjectToSubscriptionInfoMap } = socket;
let { subscription, changeEmitter, visibleSet, } = subjectToSubscriptionInfoMap.get(subject);
if (!definition.onChange(change, session, subscription, socket)) {
continue;
}
let changeToBroadcast;
if (isQueuedBroadcastCreation(change)) {
if (definition.testVisibility(change.snapshot, subscription, socket)) {
visibleSet.add(resource);
changeToBroadcast = pruneAsBroadcastCreation(change);
}
else {
continue;
}
}
else if (isQueuedBroadcastRemoval(change)) {
if (visibleSet.has(resource)) {
visibleSet.delete(resource);
changeToBroadcast = change;
}
else {
continue;
}
}
else {
let visibility = definition.testVisibility(change.snapshot, subscription, socket);
if (visibility) {
if (visibleSet.has(resource)) {
changeToBroadcast = definition.pruneBroadcastChange(change);
}
else if (visibility === true) {
visibleSet.add(resource);
changeToBroadcast = pruneAsBroadcastCreation(change);
}
else {
continue;
}
}
else {
if (visibleSet.has(resource)) {
visibleSet.delete(resource);
changeToBroadcast = pruneAsBroadcastRemoval(change);
}
else {
continue;
}
}
}
changeEmitter.add({
change: changeToBroadcast,
session: clientSession,
});
}
}
}
exports.Server = Server;
function isCreation(object) {
return object.type === 'create';
}
function isRemoval(object) {
return object.type === 'remove';
}
function isQueuedBroadcastCreation(object) {
return object.type === 'create';
}
function isQueuedBroadcastRemoval(object) {
return object.type === 'remove';
}
function pruneAsBroadcastCreation({ uid, subject, resource, snapshot, timestamp, }) {
return {
uid,
subject,
resource,
type: 'create',
snapshot,
timestamp,
};
}
function pruneAsBroadcastRemoval({ uid, subject, resource, timestamp, }) {
return {
uid,
subject,
resource,
type: 'remove',
timestamp,
};
}
//# sourceMappingURL=server.js.map