Skip to content

Commit

Permalink
port longpoll.js to ts
Browse files Browse the repository at this point in the history
  • Loading branch information
tylerbarker committed May 12, 2024
1 parent 958e933 commit faf0944
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 42 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ This effort isn't officially endorsed by the Phoenix team, just a bit of fun. Th
- [x] Port timer.js to TypeScript
- [x] Port serializer.js to TypeScript
- [x] Port push.js to TypeScript
- [ ] Port longpoll.js to TypeScript
- [x] Port longpoll.js to TypeScript
- [ ] Port channel.js to TypeScript
- [ ] Port presence.js to TypeScript
- [ ] Port socket.js to TypeScript
- [ ] Circle back to `any` types after everything is ported
- [ ] Circle back to `as` type assertions after everything is ported
- [ ] Reassess bundling targets e.g what do we need to support?
- [ ] Configure as Hex package (minimal Elixir scaffolding)
- [ ] Write installation documentation
Expand Down
13 changes: 7 additions & 6 deletions phoenix/ajax.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { global as globalNoIE, XHR_STATES } from "./constants";
import type { Global, ParsedJSON, SerializableObject } from "./constants";

export type AjaxRequest = XMLHttpRequest | XDomainRequest;
export type AjaxRequestCallback = (response?: ParsedJSON) => void;
export type RequestMethod = "GET" | "POST" | "PUT" | "DELETE";

// IE8, IE9
export interface XDomainRequest {
new (): XDomainRequest;
abort(): () => void;
timeout: number;
onload: () => void;
onerror: () => void;
Expand All @@ -22,8 +25,6 @@ const global = globalNoIE as Global & {
XDomainRequest?: XDomainRequest;
};

type CallbackFn = (response?: ParsedJSON) => void;

export default class Ajax {
static request(
method: RequestMethod,
Expand All @@ -32,8 +33,8 @@ export default class Ajax {
body: Document | XMLHttpRequestBodyInit | null,
timeout: number,
ontimeout: () => void,
callback: CallbackFn,
): XMLHttpRequest | XDomainRequest {
callback: AjaxRequestCallback,
): AjaxRequest {
if (global.XDomainRequest) {
let req = new global.XDomainRequest(); // IE8, IE9
return this.xdomainRequest(
Expand Down Expand Up @@ -67,7 +68,7 @@ export default class Ajax {
body: Document | XMLHttpRequestBodyInit | null,
timeout: number,
ontimeout: () => void,
callback: CallbackFn,
callback: AjaxRequestCallback,
): XDomainRequest {
req.timeout = timeout;
req.open(method, endPoint);
Expand All @@ -94,7 +95,7 @@ export default class Ajax {
body: Document | XMLHttpRequestBodyInit | null,
timeout: number,
ontimeout: () => void,
callback: CallbackFn,
callback: AjaxRequestCallback,
): XMLHttpRequest {
req.open(method, endPoint, true);
req.timeout = timeout;
Expand Down
89 changes: 60 additions & 29 deletions phoenix/longpoll.js → phoenix/longpoll.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { SOCKET_STATES, TRANSPORTS } from "./constants";

import Ajax from "./ajax";
import { DEFAULT_TIMEOUT, SOCKET_STATES, TRANSPORTS } from "./constants";
import type { AjaxRequest, AjaxRequestCallback, RequestMethod } from "./ajax";
import type { TimerId } from "./timer";

type PhxResponse = {
status: number;
token: string | null;
messages: Record<string | number, unknown>[];
};

let arrayBufferToBase64 = (buffer) => {
let arrayBufferToBase64 = (buffer: ArrayBuffer) => {
let binary = "";
let bytes = new Uint8Array(buffer);
let len = bytes.byteLength;
Expand All @@ -13,26 +20,34 @@ let arrayBufferToBase64 = (buffer) => {
};

export default class LongPoll {
constructor(endPoint) {
this.endPoint = null;
this.token = null;
this.skipHeartbeat = true;
this.reqs = new Set();
this.awaitingBatchAck = false;
this.currentBatch = null;
this.currentBatchTimer = null;
this.batchBuffer = [];
endPoint: string | null = null;
token: string | null = null;
timeout: number = DEFAULT_TIMEOUT;
skipHeartbeat: boolean = true;
reqs: Set<AjaxRequest> = new Set();
awaitingBatchAck: boolean = false;
currentBatch: any = null;
currentBatchTimer: TimerId | null = null;
batchBuffer: any[] = [];
pollEndpoint: string;
readyState: SOCKET_STATES = SOCKET_STATES.connecting;

onopen: (() => void) | ((event: any) => void);
onerror: (() => void) | ((error: any) => void);
onmessage: (() => void) | ((event: any) => void);
onclose: (() => void) | ((event: any) => void);

constructor(endPoint: string) {
this.pollEndpoint = this.normalizeEndpoint(endPoint);
this.onopen = function () {}; // noop
this.onerror = function () {}; // noop
this.onmessage = function () {}; // noop
this.onclose = function () {}; // noop
this.pollEndpoint = this.normalizeEndpoint(endPoint);
this.readyState = SOCKET_STATES.connecting;
// we must wait for the caller to finish setting up our callbacks and timeout properties
setTimeout(() => this.poll(), 0);
}

normalizeEndpoint(endPoint) {
normalizeEndpoint(endPoint: string) {
return endPoint
.replace("ws://", "http://")
.replace("wss://", "https://")
Expand All @@ -43,10 +58,13 @@ export default class LongPoll {
}

endpointURL() {
return Ajax.appendParams(this.pollEndpoint, { token: this.token });
return Ajax.appendParams(
this.pollEndpoint,
this.token ? { token: this.token } : {},
);
}

closeAndRetry(code, reason, wasClean) {
closeAndRetry(code: number, reason: string, wasClean: boolean | number) {
this.close(code, reason, wasClean);
this.readyState = SOCKET_STATES.connecting;
}
Expand All @@ -70,14 +88,20 @@ export default class LongPoll {
null,
() => this.ontimeout(),
(resp) => {
let status = 0;
let messages: Record<string | number, unknown>[] = [];
if (resp) {
var { status, token, messages } = resp;
let {
status: respStatus,
messages: respMessages,
token,
} = resp as PhxResponse;
status = respStatus;
messages = respMessages;
this.token = token;
} else {
status = 0;
}

switch (status) {
switch (resp && status) {
case 200:
messages.forEach((msg) => {
// Tasks are what things like event handlers, setTimeout callbacks,
Expand Down Expand Up @@ -130,7 +154,7 @@ export default class LongPoll {
// setTimeout 0, which optimizes back-to-back procedural
// pushes against an empty buffer

send(body) {
send(body: string | ArrayBuffer) {
if (typeof body !== "string") {
body = arrayBufferToBase64(body);
}
Expand All @@ -147,17 +171,18 @@ export default class LongPoll {
}
}

batchSend(messages) {
batchSend(messages: string[] | ArrayBuffer[]) {
this.awaitingBatchAck = true;
this.ajax(
"POST",
"application/x-ndjson",
messages.join("\n"),
() => this.onerror("timeout"),
(resp) => {
const phxResp = resp as PhxResponse;
this.awaitingBatchAck = false;
if (!resp || resp.status !== 200) {
this.onerror(resp && resp.status);
if (!phxResp || phxResp.status !== 200) {
this.onerror(phxResp && phxResp.status);
this.closeAndRetry(1011, "internal server error", false);
} else if (this.batchBuffer.length > 0) {
this.batchSend(this.batchBuffer);
Expand All @@ -167,7 +192,7 @@ export default class LongPoll {
);
}

close(code, reason, wasClean) {
close(code: number, reason: string, wasClean: boolean | number) {
for (let req of this.reqs) {
req.abort();
}
Expand All @@ -177,7 +202,7 @@ export default class LongPoll {
{ code, reason, wasClean },
);
this.batchBuffer = [];
clearTimeout(this.currentBatchTimer);
this.currentBatchTimer && clearTimeout(this.currentBatchTimer);
this.currentBatchTimer = null;
if (typeof CloseEvent !== "undefined") {
this.onclose(new CloseEvent("close", opts));
Expand All @@ -186,8 +211,14 @@ export default class LongPoll {
}
}

ajax(method, contentType, body, onCallerTimeout, callback) {
let req;
ajax(
method: RequestMethod,
contentType: string,
body: Document | XMLHttpRequestBodyInit | null,
onCallerTimeout: () => void,
callback: AjaxRequestCallback,
) {
let req: AjaxRequest;
let ontimeout = () => {
this.reqs.delete(req);
onCallerTimeout();
Expand Down
12 changes: 6 additions & 6 deletions phoenix/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ export enum BINARY_KINDS {
broadcast = 2,
}

export interface MessageMeta {
export type MessageMeta = {
join_ref: string | null;
ref: string | null;
topic: string;
event: string;
}
};

export interface ObjectMessage extends MessageMeta {
export type ObjectMessage = MessageMeta & {
payload: Record<string | number, unknown>;
}
};

export interface BinaryMessage extends MessageMeta {
export type BinaryMessage = MessageMeta & {
payload: ArrayBuffer;
}
};

export type DecodedMessage = ObjectMessage | BinaryMessage;
export type EncodedMessage = ArrayBuffer | string;
Expand Down

0 comments on commit faf0944

Please sign in to comment.