pubnub
Version:
Publish & Subscribe Real-time Messaging with PubNub
200 lines (170 loc) • 6.57 kB
text/typescript
/**
* Subscribe Event Engine module.
*
* @internal
*/
import { ReceivingState, ReceivingStateContext } from './states/receiving';
import { Dependencies, EventEngineDispatcher } from './dispatcher';
import { subscriptionTimetokenFromReference } from '../core/utils';
import { UnsubscribedState } from './states/unsubscribed';
import { Dispatcher, Engine } from './core';
import * as utils from '../core/utils';
import * as effects from './effects';
import * as events from './events';
/**
* Subscribe Event Engine Core.
*
* @internal
*/
export class EventEngine {
private readonly engine: Engine<events.Events, effects.Effects>;
private dispatcher: Dispatcher<effects.Effects, Dependencies>;
private dependencies: Dependencies;
channels: string[] = [];
groups: string[] = [];
get _engine() {
return this.engine;
}
private readonly _unsubscribeEngine!: () => void;
constructor(dependencies: Dependencies) {
this.dependencies = dependencies;
this.engine = new Engine(dependencies.config.logger());
this.dispatcher = new EventEngineDispatcher(this.engine, dependencies);
dependencies.config.logger().debug('EventEngine', 'Create subscribe event engine.');
this._unsubscribeEngine = this.engine.subscribe((change) => {
if (change.type === 'invocationDispatched') {
this.dispatcher.dispatch(change.invocation);
}
});
this.engine.start(UnsubscribedState, undefined);
}
/**
* Subscription-based current timetoken.
*
* @returns Timetoken based on current timetoken plus diff between current and loop start time.
*/
get subscriptionTimetoken(): string | undefined {
const currentState = this.engine.currentState;
if (!currentState) return undefined;
let referenceTimetoken: string | undefined;
let currentTimetoken = '0';
if (currentState.label === ReceivingState.label) {
const context: ReceivingStateContext = this.engine.currentContext;
currentTimetoken = context.cursor.timetoken;
referenceTimetoken = context.referenceTimetoken;
}
return subscriptionTimetokenFromReference(currentTimetoken, referenceTimetoken ?? '0');
}
subscribe({
channels,
channelGroups,
timetoken,
withPresence,
}: {
channels?: string[];
channelGroups?: string[];
timetoken?: string | number;
withPresence?: boolean;
}): void {
this.channels = [...this.channels, ...(channels ?? [])];
this.groups = [...this.groups, ...(channelGroups ?? [])];
if (withPresence) {
this.channels.map((c) => this.channels.push(`${c}-pnpres`));
this.groups.map((g) => this.groups.push(`${g}-pnpres`));
}
if (timetoken) {
this.engine.transition(
events.restore(
Array.from(new Set([...this.channels, ...(channels ?? [])])),
Array.from(new Set([...this.groups, ...(channelGroups ?? [])])),
timetoken,
),
);
} else {
this.engine.transition(
events.subscriptionChange(
Array.from(new Set([...this.channels, ...(channels ?? [])])),
Array.from(new Set([...this.groups, ...(channelGroups ?? [])])),
),
);
}
if (this.dependencies.join) {
this.dependencies.join({
channels: Array.from(new Set(this.channels.filter((c) => !c.endsWith('-pnpres')))),
groups: Array.from(new Set(this.groups.filter((g) => !g.endsWith('-pnpres')))),
});
}
}
unsubscribe({ channels = [], channelGroups = [] }: { channels?: string[]; channelGroups?: string[] }): void {
const filteredChannels = utils.removeSingleOccurrence(this.channels, [
...channels,
...channels.map((c) => `${c}-pnpres`),
]);
const filteredGroups = utils.removeSingleOccurrence(this.groups, [
...channelGroups,
...channelGroups.map((c) => `${c}-pnpres`),
]);
if (
new Set(this.channels).size !== new Set(filteredChannels).size ||
new Set(this.groups).size !== new Set(filteredGroups).size
) {
const channelsToLeave = utils.findUniqueCommonElements(this.channels, channels);
const groupsToLeave = utils.findUniqueCommonElements(this.groups, channelGroups);
if (this.dependencies.presenceState) {
channelsToLeave?.forEach((c) => delete this.dependencies.presenceState[c]);
groupsToLeave?.forEach((g) => delete this.dependencies.presenceState[g]);
}
this.channels = filteredChannels;
this.groups = filteredGroups;
this.engine.transition(
events.subscriptionChange(
Array.from(new Set(this.channels.slice(0))),
Array.from(new Set(this.groups.slice(0))),
),
);
if (this.dependencies.leave) {
this.dependencies.leave({
channels: channelsToLeave.slice(0),
groups: groupsToLeave.slice(0),
});
}
}
}
unsubscribeAll(isOffline: boolean = false): void {
const channelGroups = this.getSubscribedChannelGroups();
const channels = this.getSubscribedChannels();
this.channels = [];
this.groups = [];
if (this.dependencies.presenceState) {
Object.keys(this.dependencies.presenceState).forEach((objectName) => {
delete this.dependencies.presenceState[objectName];
});
}
this.engine.transition(events.subscriptionChange(this.channels.slice(0), this.groups.slice(0), isOffline));
if (this.dependencies.leaveAll) this.dependencies.leaveAll({ channels, groups: channelGroups, isOffline });
}
reconnect({ timetoken, region }: { timetoken?: string; region?: number }): void {
const channelGroups = this.getSubscribedChannels();
const channels = this.getSubscribedChannels();
this.engine.transition(events.reconnect(timetoken, region));
if (this.dependencies.presenceReconnect) this.dependencies.presenceReconnect({ channels, groups: channelGroups });
}
disconnect(isOffline: boolean = false): void {
const channelGroups = this.getSubscribedChannels();
const channels = this.getSubscribedChannels();
this.engine.transition(events.disconnect(isOffline));
if (this.dependencies.presenceDisconnect)
this.dependencies.presenceDisconnect({ channels, groups: channelGroups, isOffline });
}
getSubscribedChannels(): string[] {
return Array.from(new Set(this.channels.slice(0)));
}
getSubscribedChannelGroups(): string[] {
return Array.from(new Set(this.groups.slice(0)));
}
dispose(): void {
this.disconnect(true);
this._unsubscribeEngine();
this.dispatcher.dispose();
}
}