Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
5b879be
Initial test
gismya Apr 18, 2023
24828db
Comments
gismya Apr 18, 2023
67f6ee3
Fetch error handling
gismya Apr 18, 2023
e83367d
Separate handle methods
gismya Apr 18, 2023
f82e685
Comments and code clarity
gismya Apr 18, 2023
ee02b81
Fetch timeout
gismya Apr 18, 2023
ca3335c
Add error event handler
gismya Apr 18, 2023
19e57b7
Change error handler to use handleClose
gismya Apr 18, 2023
176a79b
Added error packet type handler
gismya Apr 18, 2023
3144260
Setup connect event
gismya Apr 18, 2023
cd01dbb
accidentally removed the initialization
gismya Apr 18, 2023
698e989
Few tests, some working some not
gismya Apr 18, 2023
0d34757
rename
gismya Apr 18, 2023
296e94b
rename
gismya Apr 18, 2023
91c24d0
Optional chaining
gismya Apr 18, 2023
d20f3b3
Add https version of needed endpoint
gismya Apr 18, 2023
c91be00
Export packet_types for use in tests
gismya Apr 18, 2023
e6468ac
Round of test writing
gismya Apr 19, 2023
d6b1db0
DRYer code
gismya Apr 19, 2023
476cc63
Improve comments
gismya Apr 19, 2023
c834a91
Further test improvements
gismya Apr 19, 2023
b685485
A bunch more tests
gismya Apr 19, 2023
5c739b5
Remove non working attempt to setup a mock websocket server
gismya Apr 19, 2023
8da3169
Include external isomorphic-ws
gismya Apr 19, 2023
46a30bc
Move ws to peer dependencies
gismya Apr 19, 2023
0f4e38d
Add tsdoc comments
gismya Apr 19, 2023
9111ff6
DRYer msw server
gismya Apr 19, 2023
4b37c54
Start of event hub tests
gismya Apr 19, 2023
5cdbb8f
Temporarily re-add WS to regular dependencies
gismya Apr 19, 2023
b5aab44
Remove accidental test script
gismya Apr 19, 2023
840ca66
Move ws dependencies around
gismya Apr 19, 2023
1d4d94d
Remove commonjs build plugin
gismya Apr 19, 2023
4ba1185
peerDependenciesMeta
gismya Apr 19, 2023
64b72d5
Implement simple event queue
gismya Apr 19, 2023
3b6184b
Improve reconnection logic
gismya Apr 20, 2023
c6dd1ce
Update the reconnect documentation
gismya Apr 20, 2023
3220751
Add disconnect method
gismya Apr 20, 2023
353451b
Add off method
gismya Apr 20, 2023
fd7068e
Add tests for new functionality
gismya Apr 20, 2023
9ca5847
Slight readability changes
gismya Apr 21, 2023
7e82176
Reconnect if no heartbeat recieved. [wip]
gismya Apr 21, 2023
1ab12d8
Improved naming
gismya Apr 28, 2023
cae48f4
Cont. heartbeattimeout
gismya Apr 28, 2023
cc00cdf
Create factory method to reuse connection if one already exists.
gismya Apr 28, 2023
9bb84b0
Added option to force new instance
gismya Apr 28, 2023
04feff1
Reconnect WIP
gismya May 12, 2023
96ae6bf
chore(deps): update dependency prettier to v2.8.8 (#102)
renovate[bot] May 4, 2023
453ba50
chore(deps): update dependency vite-plugin-dts to v1.7.3 (#103)
renovate[bot] May 4, 2023
f733d34
Fixed the reconnect test
gismya May 12, 2023
230b9fc
Add a tsdoc
gismya May 12, 2023
7c9fff7
cleanup
gismya May 15, 2023
3bf55f4
Improve backwards compatibility
gismya May 19, 2023
3f4f3e8
Updated tests
gismya May 19, 2023
a7834e5
Change open to a getter
gismya May 19, 2023
3491353
Merge remote-tracking branch 'origin/main' into rewrite-socketio-client
gismya May 23, 2023
f9a0bd8
Upgrade
gismya May 23, 2023
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
lib/
socket.io-websocket-only.cjs
doc/resource
coverage/
build/
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,4 @@ package.tgz
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.DS_Store
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@
},
"homepage": "http://ftrack.com",
"dependencies": {
"isomorphic-ws": "^5.0.0",
"loglevel": "^1.8.1",
"moment": "^2.29.4",
"uuid": "^9.0.0"
"uuid": "^9.0.0",
"ws": "^8.13.0"
},
"lint-staged": {
"*.js": "eslint --cache --fix",
Expand Down
8 changes: 4 additions & 4 deletions source/event.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// :copyright: Copyright (c) 2016 ftrack
import { v4 as uuidV4 } from "uuid";

import { EventSource } from "./event_hub";
/**
* ftrack API Event class.
*/
Expand All @@ -11,7 +11,7 @@ export class Event {
target: string;
inReplyToEvent: string | null;
id: string;
source?: any;
source?: EventSource;
};

/**
Expand All @@ -37,12 +37,12 @@ export class Event {
}

/** Return event data. */
getData(): { [key: string]: any } {
getData() {
return this._data;
}

/** Add source to event data. */
addSource(source: any): void {
addSource(source: EventSource): void {
this._data.source = source;
}
}
23 changes: 7 additions & 16 deletions source/event_hub.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// :copyright: Copyright (c) 2016 ftrack
import { v4 as uuidV4 } from "uuid";
import loglevel from "loglevel";
import io, { SocketIO } from "./socket.io-websocket-only.cjs";
import io from "./simple_socketio";
import { Event } from "./event";
import {
EventServerConnectionTimeoutError,
Expand Down Expand Up @@ -72,11 +72,11 @@ export type EventPayload =
| UpdateEventPayload;

export interface EventSource {
clientToken: string;
clientToken?: string;
applicationId: string;
user: {
username: string;
id: string;
id?: string;
};
id: string;
}
Expand Down Expand Up @@ -132,7 +132,7 @@ export class EventHub {
};
private _unsentEvents: ConnectionCallback[];
private _subscribers: Subscriber[];
private _socketIo: SocketIO | null;
private _socketIo: io | null;

/**
* Construct EventHub instance with API credentials.
Expand Down Expand Up @@ -177,16 +177,7 @@ export class EventHub {

/** Connect to the event server. */
connect(): void {
this._socketIo = io.connect(this._serverUrl, {
"max reconnection attempts": Infinity,
"reconnection limit": 10000,
"reconnection delay": 5000,
transports: ["websocket"],
query: new URLSearchParams({
api_user: this._apiUser,
api_key: this._apiKey,
}).toString(),
});
this._socketIo = new io(this._serverUrl, this._apiUser, this._apiKey);

this._socketIo.on("connect", this._onSocketConnected);
this._socketIo.on("ftrack.event", this._handle);
Expand All @@ -197,7 +188,7 @@ export class EventHub {
* @return {Boolean}
*/
isConnected(): boolean {
return (this._socketIo && this._socketIo.socket.connected) || false;
return this._socketIo?.isConnected() || false;
}

/**
Expand Down Expand Up @@ -368,7 +359,7 @@ export class EventHub {
// Force reconnect socket if not automatically reconnected. This
// happens for example in Adobe After Effects when rendering a
// sequence takes longer than ~30s and the JS thread is blocked.
this._socketIo.socket.reconnect();
this._socketIo.reconnect();
}
} else {
callback();
Expand Down
207 changes: 207 additions & 0 deletions source/simple_socketio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// :copyright: Copyright (c) 2023 ftrack
import WebSocket from "isomorphic-ws";
import type { Event } from "./event";
export const PACKET_TYPES = {
disconnect: "0",
connect: "1",
heartbeat: "2",
message: "3",
json: "4",
event: "5",
acknowledge: "6",
error: "7",
} as const;

interface EventHandlers {
[eventName: string]: ((eventData: Event["_data"]) => void)[];
}

interface Payload {
name: string;
args: Event["_data"][];
}

export default class SimpleSocketIOClient {
private ws: WebSocket;
private handlers: EventHandlers;
private reconnectTimeout: ReturnType<typeof setInterval> | undefined;
private heartbeatInterval: ReturnType<typeof setInterval> | undefined;
private serverUrl: string;
private wsUrl: string;
private heartbeatIntervalMs: number;
private query: string;
private apiUser: string;
private apiKey: string;
private sessionId?: string;

// Added socket object with connected, reconnect and transport properties to match current API
public socket: {
connected: boolean;
reconnect: () => void;
transport: WebSocket;
};

constructor(
serverUrl: string,
apiUser: string,
apiKey: string,
heartbeatIntervalMs: number = 10000
) {
// Convert the http(s) URL to ws(s) URL
const wsUrl = serverUrl.replace(/^(http)/, "ws");
this.serverUrl = serverUrl;
this.wsUrl = wsUrl;
this.query = new URLSearchParams({
api_user: apiUser,
api_key: apiKey,
}).toString();
this.handlers = {};
this.heartbeatIntervalMs = heartbeatIntervalMs;
this.apiUser = apiUser;
this.apiKey = apiKey;
this.socket = {
connected: false,
reconnect: this.reconnect.bind(this),
transport: null,
};
this.initializeWebSocket();
}
// Fetch the session ID from the ftrack server
private async fetchSessionId(): Promise<string> {
try {
const url = new URL(`${this.serverUrl}/socket.io/1/`);
url.searchParams.append("api_user", this.apiUser);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 7000);
const response = await fetch(url, {
headers: {
"ftrack-api-user": this.apiUser,
"ftrack-api-key": this.apiKey,
},
method: "GET",
signal: controller.signal,
});

clearTimeout(timeoutId);

if (!response.ok) {
throw new Error(`Error fetching session ID: ${response.statusText}`);
}

const responseText = await response.text();
const sessionId = responseText.split(":")[0];
this.sessionId = sessionId;
return sessionId;
} catch (error) {
console.error("Error fetching session ID:", error);
throw error;
}
}
private async initializeWebSocket(): Promise<void> {
const sessionId = this.sessionId ?? (await this.fetchSessionId());
const urlWithQueryAndSession = `${this.wsUrl}/socket.io/1/websocket/${sessionId}?${this.query}`;
this.ws = new WebSocket(urlWithQueryAndSession);
// Set transport property as a public alias of the websocket
this.socket.transport = this.ws;
this.addInitialEventListeners();
}

private addInitialEventListeners(): void {
this.ws?.addEventListener("message", this.handleMessage.bind(this));
this.ws?.addEventListener("open", this.handleOpen.bind(this));
this.ws?.addEventListener("close", this.handleClose.bind(this));
this.ws?.addEventListener("error", this.handleError.bind(this));
}

private handleError(event: Event): void {
this.handleClose();
console.error("WebSocket error:", event);
}
private handleMessage(event: MessageEvent): void {
const [packetType, data] = event.data.split(/:::?/);
if (packetType === PACKET_TYPES.event) {
const parsedData = JSON.parse(data) as Payload;
const { name, args } = parsedData;
this.handleEvent(name, args[0]);
return;
}
if (packetType === PACKET_TYPES.heartbeat) {
// Respond to server heartbeat with a heartbeat
this.ws?.send(`${PACKET_TYPES.heartbeat}::`);
return;
}
if (packetType === PACKET_TYPES.error) {
// Respond to server heartbeat with a heartbeat
console.log("WebSocket message error: ", event);
this.handleClose();
return;
}
}
private handleOpen(): void {
this.startHeartbeat();
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = undefined;
}
this.handleEvent("connect", {});
// Set connected property to true
this.socket.connected = true;
}

private handleClose(): void {
this.stopHeartbeat();
this.scheduleReconnect();
// Set connected property to false
this.socket.connected = false;
}
private handleEvent(eventName: string, eventData?: any): void {
this.handlers[eventName]?.forEach((callback) => callback(eventData));
}
// Setup event callbacks for a given eventName
public on(eventName: string, eventCallback: (eventData: any) => void): void {
if (!this.handlers[eventName]) {
this.handlers[eventName] = [];
}
this.handlers[eventName].push(eventCallback);
}
// Emit an event with the given eventName and eventData
public emit(eventName: string, eventData: Event["_data"]): void {
const payload = {
name: eventName,
args: [eventData],
};
const dataString = eventData ? `:::${JSON.stringify(payload)}` : "";
this.ws?.send(`${PACKET_TYPES.event}${dataString}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

it feels a bit wrong to do nothing if the ws isn't initialized. what did the previous client do?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Don't think it was handled, the event_hub code does unsent event queueing. But I can investigate

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added a simple queue

}
// Heartbeat methods, to tell the server to keep the connection alive
private startHeartbeat(): void {
this.heartbeatInterval = setInterval(() => {
this.ws?.send(`${PACKET_TYPES.heartbeat}::`);
}, this.heartbeatIntervalMs);
}

private stopHeartbeat(): void {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = undefined;
}
}

public isConnected(): boolean {
return this.ws?.readyState === WebSocket.OPEN;
}
// Reconnect methods, to reconnect to the server if the connection is lost using the same session ID
public reconnect(): void {
if (this.socket.connected) {
this.ws?.close();
}
this.initializeWebSocket();
}
private scheduleReconnect(reconnectDelayMs: number = 5000): void {
if (!this.reconnectTimeout) {
this.reconnectTimeout = setTimeout(() => {
this.reconnect();
}, reconnectDelayMs);
}
}
}
Loading