UNPKG

@casual-simulation/aux-common

Version:
523 lines 19.9 kB
import { filter, map, switchMap, tap, finalize, first, scan, } from 'rxjs/operators'; import { merge, of, Subject } from 'rxjs'; export const DEFAULT_BRANCH_NAME = 'default'; /** * Defines a client for inst records. */ export class InstRecordsClient { get onSyncUpdatesEvent() { return this._onSyncUpdatesEvent; } /** * Gets the amount of time in miliseconds that the client should wait before resending updates that were never acknowledged. * If null, then the client will never resend updates based on time. */ get resendUpdatesAfterMs() { return this._resendUpdatesAfter; } /** * Sets the amount of time in miliseconds that the client should wait before resending updates that were never acknowledged. * If null, then the client will never resend updates based on time. */ set resendUpdatesAfterMs(value) { this._resendUpdatesAfter = value; } get resendUpdatesIntervalMs() { return this._resendUpdatesInterval; } set resendUpdatesIntervalMs(value) { this._resendUpdatesInterval = value; if (this._resendUpdatesInterval) { this._startResendUpdatesInterval(); } else { this._stopResendUpdatesInterval(); } } _stopResendUpdatesInterval() { if (this._resendUpdatesIntervalId) { clearInterval(this._resendUpdatesIntervalId); } } _startResendUpdatesInterval() { this._stopResendUpdatesInterval(); this._resendUpdatesIntervalId = setInterval(() => { this._resendUpdates(); }, this._resendUpdatesInterval); } _resendUpdates() { for (let [branchKey, updates] of this._sentUpdates) { for (let [updateId, sentUpdate] of updates) { const lastTryTime = sentUpdate.lastTryTimeMs; const retryAfter = this._resendUpdatesAfter * Math.pow(2, Math.min(sentUpdate.tryCount - 1, 3)); const now = Date.now(); if (lastTryTime + retryAfter <= now) { sentUpdate.tryCount += 1; sentUpdate.lastTryTimeMs = now; let [recordName, inst, branch] = branchKey.split('/'); if (!recordName) { recordName = null; } this._sendAddUpdates(recordName, inst, branch, sentUpdate.updates, updateId); } } } } constructor(connection) { this._updateCounter = 0; this._timeSyncCounter = 0; this._resendUpdatesAfter = null; this._resendUpdatesInterval = null; this._resendUpdatesIntervalId = null; this._onSyncUpdatesEvent = new Subject(); this._client = connection; this._forcedOffline = false; this._sentUpdates = new Map(); this._connectedDevices = new Map(); this._watchedBranches = new Set(); this._connectedBranches = new Set(); this._connectedDeviceBranches = new Set(); } /** * Gets the connection that this client is using. */ get connection() { return this._client; } get onError() { return this._client.onError; } /** * Gets whether the client is forcing the connection to be offline or not. */ get forcedOffline() { return this._forcedOffline; } /** * Sets whether the client is forcing the connection to be offline or not. */ set forcedOffline(value) { if (value === this._forcedOffline) { return; } this._forcedOffline = value; if (this._forcedOffline) { this._client.disconnect(); } else { this._client.connect(); } } /** * Starts watching the given branch. * @param name The name of the branch to watch. */ watchBranchUpdates(nameOrEvent) { var _a; let branchEvent; if (typeof nameOrEvent === 'string') { branchEvent = { type: 'repo/watch_branch', recordName: null, inst: nameOrEvent, branch: DEFAULT_BRANCH_NAME, }; } else { branchEvent = { ...nameOrEvent, }; } const recordName = (_a = branchEvent.recordName) !== null && _a !== void 0 ? _a : null; const inst = branchEvent.inst; const branch = branchEvent.branch; const watchedBranchKey = branchKey(recordName, inst, branch); this._watchedBranches.add(watchedBranchKey); return this._whenConnected().pipe(tap((connected) => { if (connected && this._connectedBranches.has(watchedBranchKey)) { this._connectedBranches.delete(watchedBranchKey); this._client.send({ type: 'repo/unwatch_branch', recordName, inst, branch, }); } this._client.send(branchEvent); }), switchMap((connected) => merge(this._client.event('repo/watch_branch_result').pipe(filter((event) => event.recordName === recordName && event.inst === inst && event.branch === branch), tap((e) => { this._connectedBranches.add(watchedBranchKey); if (e.success) { let list = this._getSentUpdates(recordName, inst, branch); for (let [key, value] of list) { this._sendAddUpdates(recordName, inst, branch, value.updates, key); } } })), this._client.event('repo/add_updates').pipe(filter((event) => event.recordName === recordName && event.inst === inst && event.branch === branch), scan((acc, event) => { // This is the first event if (acc[0] === null) { // first event is initial event, // skip to processing events if (event.initial) { return ['event', event]; } else { // first event is not initial. // store it for later. return ['waiting', [event]]; } } else if (acc[0] === 'waiting') { // This event is happening while we are waiting for the initial event. const events = acc[1]; if (event.initial) { // current event is initial event, // merge events. const allEvents = events; const allUpdates = allEvents.flatMap((e) => { var _a; return (_a = e.updates) !== null && _a !== void 0 ? _a : []; }); return [ 'event', { branch: event.branch, updates: [ ...allUpdates, ...event.updates, ], }, ]; } else { // current event is not initial, // store event return [ 'waiting', [...events, event], ]; } } else { // This event is happening after we have got the initial event return ['event', event]; } }, [null]), filter(([type, event]) => type === 'event'), map(([type, event]) => event), map((e) => ({ type: 'updates', updates: e.updates, }))), this._client.event('repo/updates_received').pipe(filter((event) => event.recordName === recordName && event.inst === inst && event.branch === branch), tap((event) => { if (branchEvent.temporary) { return; } // TODO: Decide whether to mark off the updates // as saved or not when an error occurs. // Right now, if the the updates are not stored on the server // because too much space is used, then they will never be sent back to the server again. let list = this._getSentUpdates(recordName, inst, branch); list.delete(event.updateId); if (list.size === 0) { this._onSyncUpdatesEvent.next({ type: 'synced', recordName, inst, branch, }); } }), map((event) => { if (event.errorCode === 'max_size_reached') { return { type: 'error', kind: event.errorCode, maxBranchSizeInBytes: event.maxBranchSizeInBytes, neededBranchSizeInBytes: event.neededBranchSizeInBytes, }; } return { type: 'updates_received', }; })), this._client.event('repo/receive_action').pipe(filter((event) => event.recordName === recordName && event.inst === inst && event.branch === branch), map((event) => ({ type: 'event', action: event.action, }))), this._client.onError.pipe(filter((error) => error.recordName === recordName && error.inst === inst && error.branch === branch), map((error) => ({ type: 'error', kind: 'error', info: error, })))).pipe(filter(isClientUpdatesOrEvents))), finalize(() => { this._watchedBranches.delete(watchedBranchKey); this._connectedBranches.delete(watchedBranchKey); if (this._client.isConnected) { this._client.send({ type: 'repo/unwatch_branch', recordName, inst, branch, }); } })); } /** * Watches for rate limit exceeded events. */ watchRateLimitExceeded() { return this._whenConnected().pipe(switchMap(() => this._client.event('rate_limit_exceeded'))); } /** * Gets the updates stored on the given branch. * @param recordName The name of the record. * @param inst The name of the inst. * @param name The name of the branch to get. */ getBranchUpdates(recordName, inst, branch) { return this._whenConnected().pipe(first((connected) => connected), tap((connected) => { this._client.send({ type: 'repo/get_updates', recordName, inst, branch, }); }), switchMap((connected) => this._client .event('repo/add_updates') .pipe(first((event) => event.recordName === recordName && event.inst === inst && event.branch === branch)))); } /** * Watches for device connection/disconnection events on the given branch. * @param branch The branch to watch. */ watchBranchDevices(recordName, inst, branch) { return this._whenConnected(false).pipe(switchMap((connected) => // Grab all of the currently connected devices // and send disconnected events for them !connected ? this._disconnectDevices(recordName, inst, branch) : this._watchConnectedDevices(recordName, inst, branch))); } _disconnectDevices(recordName, inst, branch) { return of(...[ ...this._getConnectedDevices(recordName, inst, branch).values(), ].map((device) => ({ type: 'repo/disconnected_from_branch', broadcast: false, recordName, inst, branch: branch, connection: device, }))); } _watchConnectedDevices(recordName, inst, branch) { const watchedBranchKey = branchKey(recordName, inst, branch); return of(true).pipe(tap((connected) => { if (connected && this._connectedDeviceBranches.has(watchedBranchKey)) { this._client.send({ type: 'repo/unwatch_branch_devices', recordName, inst, branch, }); } this._connectedDeviceBranches.add(watchedBranchKey); this._client.send({ type: 'repo/watch_branch_devices', recordName, inst, branch, }); }), switchMap((connected) => merge(this._client.event('repo/connected_to_branch').pipe(filter((e) => e.broadcast === false && e.branch.recordName === recordName && e.branch.inst === inst && e.branch.branch === branch && !this._isDeviceConnected(recordName, inst, branch, e.connection)), tap((e) => { const devices = this._getConnectedDevices(recordName, inst, branch); devices.set(e.connection.connectionId, e.connection); }), map((e) => ({ type: 'repo/connected_to_branch', ...e, }))), this._client.event('repo/disconnected_from_branch').pipe(filter((e) => e.broadcast === false && e.recordName === recordName && e.inst === inst && e.branch === branch && this._isDeviceConnected(recordName, inst, branch, e.connection)), tap((e) => { const devices = this._getConnectedDevices(recordName, inst, branch); devices.delete(e.connection.connectionId); }), map((e) => ({ type: 'repo/disconnected_from_branch', ...e, }))))), finalize(() => { this._connectedDeviceBranches.delete(watchedBranchKey); if (this._client.isConnected) { this._client.send({ type: 'repo/unwatch_branch_devices', recordName, inst, branch, }); } })); } /** * Adds the given updates to the given branch. * @param recordName The name of the record. * @param inst The name of the inst. * @param branch The name of the branch. * @param updates The updates. */ addUpdates(recordName, inst, branch, updates) { if (updates.length <= 0) { return; } let list = this._getSentUpdates(recordName, inst, branch); this._updateCounter += 1; list.set(this._updateCounter, { updates, updateId: this._updateCounter, sentTimeMs: Date.now(), lastTryTimeMs: Date.now(), tryCount: 1, }); this._sendAddUpdates(recordName, inst, branch, updates, this._updateCounter); if (list.size === 1) { this._onSyncUpdatesEvent.next({ type: 'syncing', recordName, inst, branch, }); } } /** * Sends the given action to devices on the given branch. * @param branch The branch. * @param action The action. */ sendAction(recordName, inst, branch, action) { this._client.send({ type: 'repo/send_action', recordName, inst, branch, action, }); } /** * Sends a SyncTimeRequest to the server. */ sampleServerTime() { let count = this._timeSyncCounter + 1; this._timeSyncCounter = count; const observable = this._whenConnected().pipe(first((c) => c), tap((connected) => { this._client.send({ type: 'sync/time', id: count, clientRequestTime: Date.now(), }); }), switchMap((connected) => this._client.event('sync/time/response').pipe(first((event) => event.id === count), map((r) => ({ clientRequestTime: r.clientRequestTime, currentTime: Date.now(), serverReceiveTime: r.serverReceiveTime, serverTransmitTime: r.serverTransmitTime, }))))); return new Promise((resolve, reject) => { observable.subscribe({ next: (o) => resolve(o), error: (err) => reject(err), }); }); } /** * Requests the number of devices that are currently connected. * @param branch The branch that the devices should be counted on. */ connectionCount(recordName = null, inst = null, branch = null) { recordName = recordName !== null && recordName !== void 0 ? recordName : null; inst = inst !== null && inst !== void 0 ? inst : null; branch = branch !== null && branch !== void 0 ? branch : null; return this._whenConnected().pipe(tap((connected) => { this._client.send({ type: 'repo/connection_count', recordName, inst, branch, }); }), switchMap((connected) => merge(this._client .event('repo/connection_count') .pipe(first((e) => e.recordName === recordName && e.inst === inst && e.branch === branch)))), map((e) => e.count)); } _whenConnected(filter = true) { return whenConnected(this._client.connectionState, filter); } _sendAddUpdates(recordName, inst, branch, updates, updateId) { if (this._watchedBranches.has(branch) && !this.connection.isConnected) { // Skip sending the atoms because we're watching the branch and we're not connected. // This means that the new atoms are saved in the sent atoms list so they will be resent // when we reconnect. return; } this._client.send({ type: 'repo/add_updates', recordName, inst, branch, updates, updateId, }); } _getSentUpdates(recordName, inst, branch) { const key = branchKey(recordName, inst, branch); let map = this._sentUpdates.get(key); if (!map) { map = new Map(); this._sentUpdates.set(key, map); } return map; } _getConnectedDevices(recordName, inst, branch) { const key = branchKey(recordName, inst, branch); let map = this._connectedDevices.get(key); if (!map) { map = new Map(); this._connectedDevices.set(key, map); } return map; } _isDeviceConnected(recordName, inst, branch, device) { const map = this._getConnectedDevices(recordName, inst, branch); return map.has(device.connectionId); } } export function isClientEvent(event) { return event.type === 'event'; } export function isClientUpdates(event) { return event.type === 'updates'; } export function isClientUpdatesOrEvents(event) { return (event.type === 'updates' || event.type === 'event' || event.type === 'error' || event.type === 'repo/watch_branch_result'); } export function isClientError(event) { return event.type === 'error'; } export function isWatchBranchResult(event) { return event.type === 'repo/watch_branch_result'; } function whenConnected(observable, filterConnected = true) { return observable.pipe(map((s) => s.connected), filterConnected ? filter((connected) => connected) : (a) => a); } function branchKey(recordName, inst, branch) { return `${recordName !== null && recordName !== void 0 ? recordName : ''}/${inst}/${branch}`; } //# sourceMappingURL=InstRecordsClient.js.map