UNPKG

syncable-server

Version:
301 lines 11.2 kB
"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