Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Broadcasting of requested resolution #8

Merged
merged 8 commits into from
Jan 8, 2021
51 changes: 31 additions & 20 deletions src/lib/AssetConnection.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import Ably, { Types as AblyTypes } from 'ably';
import { LocationListener, StatusListener } from '../types';
import { ClientTypes, LocationListener, Resolution, StatusListener } from '../types';
import Logger from './utils/Logger';
import { setImmediate } from './utils/utils';

enum EventNames {
raw = 'raw',
enhanced = 'enhanced',
}

enum ClientTypes {
subscriber = 'subscriber',
publisher = 'publisher',
Raw = 'raw',
Enhanced = 'enhanced',
}

class AssetConnection {
Expand All @@ -20,20 +16,23 @@ class AssetConnection {
onRawLocationUpdate?: LocationListener;
onEnhancedLocationUpdate?: LocationListener;
onStatusUpdate?: StatusListener;
resolution: Resolution | null;

constructor(
logger: Logger,
trackingId: string,
ablyOptions: AblyTypes.ClientOptions,
onRawLocationUpdate?: LocationListener,
onEnhancedLocationUpdate?: LocationListener,
onStatusUpdate?: StatusListener
onStatusUpdate?: StatusListener,
resolution?: Resolution
) {
this.logger = logger;
this.trackingId = trackingId;
this.onRawLocationUpdate = onRawLocationUpdate;
this.onEnhancedLocationUpdate = onEnhancedLocationUpdate;
this.onStatusUpdate = onStatusUpdate;
this.resolution = resolution ?? null;

this.ably = new Ably.Realtime.Promise(ablyOptions);
this.channel = this.ably.channels.get(trackingId, {
Expand All @@ -57,41 +56,53 @@ class AssetConnection {
this.ably.close();
};

performChangeResolution = async (resolution: Resolution): Promise<void> => {
return this.channel.presence.update({
type: ClientTypes.Publisher,
resolution,
});
};

private subscribeForRawEvents = (rawLocationListener: LocationListener) => {
this.channel.subscribe(EventNames.raw, (message) => {
this.channel.subscribe(EventNames.Raw, (message) => {
const parsedMessage = typeof message.data === 'string' ? JSON.parse(message.data) : message.data;
if (Array.isArray(parsedMessage)) {
parsedMessage.forEach(rawLocationListener);
parsedMessage.forEach((msg) => setImmediate(() => rawLocationListener(msg)));
} else {
rawLocationListener(parsedMessage);
}
});
};

private subscribeForEnhancedEvents = (enhancedLocationListener: LocationListener) => {
this.channel.subscribe(EventNames.enhanced, (message) => {
this.channel.subscribe(EventNames.Enhanced, (message) => {
const parsedMessage = typeof message.data === 'string' ? JSON.parse(message.data) : message.data;
if (Array.isArray(parsedMessage)) {
parsedMessage.forEach(enhancedLocationListener);
parsedMessage.forEach((msg) => setImmediate(() => enhancedLocationListener(msg)));
} else {
enhancedLocationListener(parsedMessage);
setImmediate(() => enhancedLocationListener(parsedMessage));
}
});
};

private joinChannelPresence = async () => {
this.channel.presence.subscribe(this.onPresenceMessage);
this.channel.presence.enterClient(this.ably.auth.clientId, ClientTypes.subscriber).catch((reason) => {
this.logger.logError(`Error entering channel presence: ${reason}`);
throw new Error(reason);
});
this.channel.presence
.enterClient(this.ably.auth.clientId, {
type: ClientTypes.Subscriber,
resolution: this.resolution,
})
.catch((reason) => {
this.logger.logError(`Error entering channel presence: ${reason}`);
throw new Error(reason);
});
};

private leaveChannelPresence = async () => {
this.channel.presence.unsubscribe();
this.notifyAssetIsOffline();
try {
await this.channel.presence.leaveClient(this.ably.auth.clientId, ClientTypes.subscriber);
await this.channel.presence.leaveClient(this.ably.auth.clientId);
} catch (e) {
this.logger.logError(`Error leaving channel presence: ${e.reason}`);
throw new Error(e.reason);
Expand All @@ -100,7 +111,7 @@ class AssetConnection {

private onPresenceMessage = (presenceMessage: AblyTypes.PresenceMessage) => {
const data = typeof presenceMessage.data === 'string' ? JSON.parse(presenceMessage.data) : presenceMessage.data;
if (data?.type === ClientTypes.publisher) {
if (data?.type === ClientTypes.Publisher) {
if (presenceMessage.action === 'enter') {
this.notifyAssetIsOnline();
} else if (presenceMessage.action === 'leave') {
Expand Down
17 changes: 15 additions & 2 deletions src/lib/AssetSubscriber.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Types as AblyTypes } from 'ably';
import { LocationListener, StatusListener } from '../types';
import { LocationListener, Resolution, StatusListener } from '../types';
import AssetConnection from './AssetConnection';
import Logger, { LoggerOptions } from './utils/Logger';

Expand All @@ -9,6 +9,7 @@ export type SubscriberOptions = {
onEnhancedLocationUpdate?: LocationListener;
onStatusUpdate?: StatusListener;
loggerOptions?: LoggerOptions;
resolution?: Resolution;
};

class AssetSubscriber {
Expand All @@ -18,13 +19,15 @@ class AssetSubscriber {
onEnhancedLocationUpdate?: LocationListener;
logger: Logger;
assetConnection?: AssetConnection;
resolution?: Resolution;
QuintinWillison marked this conversation as resolved.
Show resolved Hide resolved

constructor(options: SubscriberOptions) {
this.logger = new Logger(options.loggerOptions);
this.ablyOptions = options.ablyOptions;
this.onStatusUpdate = options.onStatusUpdate;
this.onRawLocationUpdate = options.onRawLocationUpdate;
this.onEnhancedLocationUpdate = options.onEnhancedLocationUpdate;
this.resolution = options.resolution;
}

start(trackingId: string): void {
Expand All @@ -34,10 +37,20 @@ class AssetSubscriber {
this.ablyOptions,
this.onRawLocationUpdate,
this.onEnhancedLocationUpdate,
this.onStatusUpdate
this.onStatusUpdate,
this.resolution
);
}

sendChangeRequest = async (resolution: Resolution): Promise<void> => {
this.resolution = resolution;
if (!this.assetConnection) {
throw new Error('Cannot change resolution; no asset is currently being tracked.');
} else {
return this.assetConnection.performChangeResolution(resolution);
}
};

stop = async (): Promise<void> => {
await this.assetConnection?.close?.();
delete this.assetConnection;
Expand Down
20 changes: 10 additions & 10 deletions src/lib/utils/Logger.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
export enum LogLevel {
none = 0,
error = 1, // default
major = 2,
minor = 3,
micro = 4,
None = 0,
Error = 1, // default
Major = 2,
Minor = 3,
Micro = 4,
}

export type LoggerOptions = {
level: LogLevel;
};

class Logger {
logLevel = LogLevel.error;
logLevel = LogLevel.Error;

constructor(options?: LoggerOptions) {
if (options?.level) this.logLevel = options.level;
}

logError(msg: string): void {
if (this.logLevel >= LogLevel.error) console.log(msg);
if (this.logLevel >= LogLevel.Error) console.log(msg);
}

logMajor(msg: string): void {
if (this.logLevel >= LogLevel.major) console.log(msg);
if (this.logLevel >= LogLevel.Major) console.log(msg);
}

logMinor(msg: string): void {
if (this.logLevel >= LogLevel.minor) console.log(msg);
if (this.logLevel >= LogLevel.Minor) console.log(msg);
}

logMicro(msg: string): void {
if (this.logLevel >= LogLevel.micro) console.log(msg);
if (this.logLevel >= LogLevel.Micro) console.log(msg);
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/lib/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// simple implementation of window.setImmediate or NodeJS process.nextTick
// used to execute user-provided callbacks asynchronously
export const setImmediate = (fn: () => unknown): void => {
setTimeout(fn, 0);
};
19 changes: 19 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,22 @@ export type GeoJsonMessage = unknown;
export type LocationListener = (geoJsonMsg: GeoJsonMessage) => unknown;

export type StatusListener = (isOnline: boolean) => unknown;

export enum Accuracy {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take a brief look around the place in terms of TypeScript enum definitions, some places seem to prefer CamelCase. Equally I would have expected this to be picked up by the linter and/or prettier if it's non-standard and that doesn't seem to be the case. Is there a convention we're aligning with here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am aware that camelCase is often used for enums but I'm aligning with the TypeScript documentation which to me feels more like the right thing to do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were you going to address this comment in the scope of this PR? It felt like you said you might elsewhere.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, forgot to do that - done now in 9abeb51.

Minimum = 1,
Low = 2,
Balanced = 3,
High = 4,
Maximum = 5,
}

export type Resolution = {
accuracy: Accuracy;
desiredInterval: number;
minimumDisplacement: number;
};

export enum ClientTypes {
Subscriber = 'subscriber',
Publisher = 'publisher',
}