@casual-simulation/aux-common
Version:
Common library for AUX projects
389 lines • 14.2 kB
JavaScript
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