UNPKG

@casual-simulation/aux-common

Version:
389 lines 14.2 kB
import { getActiveObjects, asyncResult, action, ON_REMOTE_PLAYER_SUBSCRIBED_ACTION_NAME, ON_REMOTE_PLAYER_UNSUBSCRIBED_ACTION_NAME, stateUpdatedEvent, applyUpdates, ON_REMOTE_JOINED_ACTION_NAME, ON_REMOTE_LEAVE_ACTION_NAME, } from '../bots'; import { BehaviorSubject, Subject, Subscription, firstValueFrom } from 'rxjs'; import { filter, skip, startWith } from 'rxjs/operators'; import { sortBy } from 'es-toolkit/compat'; import { createRemoteClientYjsPartition } from './RemoteYjsPartition'; import { getConnectionId } from '../common'; export async function createOtherPlayersClientPartition(config, authSource) { if (config.type === 'other_players_client') { const partition = new OtherPlayersPartitionImpl(config.client, authSource, config); return partition; } return undefined; } /** * Defines a partition that watches for other players and loads their player partitions dynamically. */ export class OtherPlayersPartitionImpl { get space() { return this._space; } set space(value) { this._space = value; for (let p of this._partitions.values()) { p.space = value; } } get realtimeStrategy() { return 'delayed'; } get onBotsAdded() { return this._onBotsAdded.pipe(startWith(getActiveObjects(this.state))); } get onBotsRemoved() { return this._onBotsRemoved; } get onBotsUpdated() { return this._onBotsUpdated; } get onStateUpdated() { return this._onStateUpdated.pipe(startWith(stateUpdatedEvent(this.state, this._onVersionUpdated.value))); } get onVersionUpdated() { return this._onVersionUpdated; } get onError() { return this._onError; } get onEvents() { return this._onEvents; } get onStatusUpdated() { return this._onStatusUpdated; } unsubscribe() { this._sub.unsubscribe(); for (let sub of this._partitionSubs.values()) { sub.unsubscribe(); } } get closed() { return this._sub.closed; } get state() { return this._state; } get forcedOffline() { return this._client.forcedOffline; } set forcedOffline(value) { this._client.forcedOffline = value; } constructor(client, authSource, config) { var _a; this._onBotsAdded = new Subject(); this._onBotsRemoved = new Subject(); this._onBotsUpdated = new Subject(); this._onStateUpdated = new Subject(); this._onVersionUpdated = new BehaviorSubject({ currentSite: null, remoteSite: null, vector: {}, }); this._onError = new Subject(); this._onEvents = new Subject(); this._onStatusUpdated = new Subject(); this._hasRegisteredSubs = false; this._sub = new Subscription(); /** * Whether the partition is watching the branch. */ this._watchingBranch = false; this.type = 'other_players'; this._recordName = config.recordName; this._inst = config.inst; this._branch = config.branch; this._client = client; this._authSource = authSource; this._childParitionType = (_a = config.childPartitionType) !== null && _a !== void 0 ? _a : 'yjs_client'; this._state = {}; this._partitions = new Map(); this._partitionPromises = new Map(); this._partitionSubs = new Map(); this._devices = new Map(); this._synced = false; this._skipInitialLoad = config.skipInitialLoad; } async applyEvents(events) { return []; } async sendRemoteEvents(events) { for (let event of events) { if (event.type === 'remote' && event.event.type === 'get_remotes') { const action = event.event; const connectedDevices = [...this._devices.values()]; const sessionIds = sortBy([ this._client.connection.info.connectionId, ...connectedDevices.map((cd) => cd.deviceInfo.connectionId), ]); this._onEvents.next([asyncResult(event.taskId, sessionIds)]); } } } connect() { if (this._skipInitialLoad) { this._loadWithoutConnecting(); } else { this._watchDevices(); } } async enableCollaboration() { this._skipInitialLoad = false; this._synced = false; const promise = firstValueFrom(this._onStatusUpdated.pipe(filter((u) => u.type === 'sync' && u.synced))); this._watchDevices(); await promise; } _loadWithoutConnecting() { var _a; this._onStatusUpdated.next({ type: 'connection', connected: true, }); const indicator = this._client.connection.indicator; const connectionId = indicator ? getConnectionId(indicator) : 'missing-connection-id'; this._onStatusUpdated.next({ type: 'authentication', authenticated: true, info: (_a = this._client.connection.info) !== null && _a !== void 0 ? _a : { connectionId: connectionId, sessionId: null, userId: null, }, }); this._onStatusUpdated.next({ type: 'authorization', authorized: true, }); this._onStatusUpdated.next({ type: 'sync', synced: true, }); } _watchDevices() { this._sub.add(this._client.connection.connectionState.subscribe((state) => { const connected = state.connected; this._onStatusUpdated.next({ type: 'connection', connected: !!connected, }); if (connected) { this._onStatusUpdated.next({ type: 'authentication', authenticated: true, info: state.info, }); this._onStatusUpdated.next({ type: 'authorization', authorized: true, }); } else { this._updateSynced(false); } })); this._sub.add(this._client .watchBranchDevices(this._recordName, this._inst, this._branch) .subscribe((event) => { var _a; if (!this._synced) { this._updateSynced(true); } if (event.connection.connectionId === ((_a = this._client.connection.info) === null || _a === void 0 ? void 0 : _a.connectionId)) { return; } if (event.type === 'repo/connected_to_branch') { this._registerDevice(event.connection); this._tryLoadBranchForDevice(event.connection); } else if (event.type === 'repo/disconnected_from_branch') { this._unregisterDevice(event.connection); this._tryUnloadBranchForDevice(event.connection); } })); } _updateSynced(synced) { this._synced = synced; this._onStatusUpdated.next({ type: 'sync', synced: synced, }); } _registerDevice(device) { if (this._devices.has(device.connectionId)) { let connected = this._devices.get(device.connectionId); connected.connectionCount += 1; } else { const connected = { connectionCount: 1, deviceInfo: device, }; this._devices.set(device.connectionId, connected); this._onEvents.next([ action(ON_REMOTE_JOINED_ACTION_NAME, null, null, { remoteId: device.connectionId, }), action(ON_REMOTE_PLAYER_SUBSCRIBED_ACTION_NAME, null, null, { playerId: device.connectionId, }), ]); } } _unregisterDevice(device) { if (this._devices.has(device.connectionId)) { let connected = this._devices.get(device.connectionId); connected.connectionCount -= 1; if (connected.connectionCount <= 0) { this._devices.delete(device.connectionId); this._onEvents.next([ action(ON_REMOTE_LEAVE_ACTION_NAME, null, null, { remoteId: device.connectionId, }), action(ON_REMOTE_PLAYER_UNSUBSCRIBED_ACTION_NAME, null, null, { playerId: device.connectionId, }), ]); } } } /** * Attempts to load the player branch for the given device. * @param device The device. */ async _tryLoadBranchForDevice(device) { const branch = this._branchNameForDevice(device); if (!this._partitionPromises.has(branch)) { const promise = this._loadPartition(branch, device); this._partitionPromises.set(branch, promise); await promise; } } async _loadPartition(branch, device) { console.log(`[OtherPlayersPartitionImpl] Loading partition for ${device.connectionId}`); const sub = new Subscription(); const promise = this._childParitionType === 'yjs_client' ? createRemoteClientYjsPartition({ type: 'yjs_client', recordName: this._recordName, inst: this._inst, branch: branch, client: this._client, temporary: true, readOnly: true, }, this._authSource) : createRemoteClientYjsPartition({ type: 'yjs_client', recordName: this._recordName, inst: this._inst, branch: branch, client: this._client, temporary: true, readOnly: true, }, this._authSource); const partition = await promise; this._partitions.set(branch, partition); this._partitionSubs.set(branch, sub); sub.add(partition); sub.add(partition.onBotsAdded.subscribe({ next: (added) => { this._onBotsAdded.next(added); }, error: (err) => this._onBotsAdded.error(err), })); sub.add(partition.onBotsRemoved.subscribe({ next: (removed) => { this._onBotsRemoved.next(removed); }, error: (err) => this._onBotsRemoved.error(err), })); sub.add(partition.onBotsUpdated.subscribe({ next: (updated) => { this._onBotsUpdated.next(updated); }, error: (err) => this._onBotsUpdated.error(err), })); sub.add(partition.onError.subscribe(this._onError)); sub.add(partition.onStateUpdated.pipe(skip(1)).subscribe({ next: (update) => { this._state = applyUpdates(this._state, update); this._onStateUpdated.next({ ...update, version: null, }); }, error: (err) => this._onStateUpdated.error(err), })); partition.space = this.space; partition.connect(); return partition; } /** * Attempts to unload the player branch for the given device. * @param device The device. */ async _tryUnloadBranchForDevice(device) { const branch = this._branchNameForDevice(device); if (this._partitionPromises.has(branch)) { console.log(`[OtherPlayersPartitionImpl] Unloading partition for ${device.connectionId}`); const partition = await this._partitionPromises.get(branch); this._partitions.delete(branch); this._partitionPromises.delete(branch); const sub = this._partitionSubs.get(branch); this._partitionSubs.delete(branch); if (sub) { sub.unsubscribe(); } let update = {}; const state = partition.state; const ids = Object.keys(state); let deleted = []; for (let id of ids) { const bot = this._state[id]; if (bot.id) { deleted.push(id); update[id] = null; } else { // Bot is partial, which means // it was not created by this partition. if (bot.masks) { // Delete tag masks const tags = bot.masks[this.space]; if (tags) { for (let tag in tags) { if (!update[id]) { update[id] = {}; } const updatedBot = update[id]; if (!updatedBot.masks) { updatedBot.masks = {}; } if (!updatedBot.masks[this.space]) { updatedBot.masks[this.space] = {}; } updatedBot.masks[this.space][tag] = null; } } } } delete this._state[id]; } this._onBotsRemoved.next(deleted); const event = stateUpdatedEvent(update, null); if (event.addedBots.length > 0 || event.removedBots.length > 0 || event.updatedBots.length > 0) { this._onStateUpdated.next(event); } } } _branchNameForDevice(device) { return `${this._branch}-player-${device.connectionId}`; } } //# sourceMappingURL=OtherPlayersPartition.js.map