From faf0944d5db77abc939e7f320f48cca7aba66b4e Mon Sep 17 00:00:00 2001 From: Tyler Barker Date: Mon, 13 May 2024 09:31:17 +1000 Subject: [PATCH] port longpoll.js to ts --- README.md | 3 +- phoenix/ajax.ts | 13 ++-- phoenix/{longpoll.js => longpoll.ts} | 89 +++++++++++++++++++--------- phoenix/serializer.ts | 12 ++-- 4 files changed, 75 insertions(+), 42 deletions(-) rename phoenix/{longpoll.js => longpoll.ts} (70%) diff --git a/README.md b/README.md index a226e97..b171a51 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/phoenix/ajax.ts b/phoenix/ajax.ts index ee82c5a..67c962a 100644 --- a/phoenix/ajax.ts +++ b/phoenix/ajax.ts @@ -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; @@ -22,8 +25,6 @@ const global = globalNoIE as Global & { XDomainRequest?: XDomainRequest; }; -type CallbackFn = (response?: ParsedJSON) => void; - export default class Ajax { static request( method: RequestMethod, @@ -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( @@ -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); @@ -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; diff --git a/phoenix/longpoll.js b/phoenix/longpoll.ts similarity index 70% rename from phoenix/longpoll.js rename to phoenix/longpoll.ts index 0aa72ab..a752964 100644 --- a/phoenix/longpoll.js +++ b/phoenix/longpoll.ts @@ -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[]; +}; -let arrayBufferToBase64 = (buffer) => { +let arrayBufferToBase64 = (buffer: ArrayBuffer) => { let binary = ""; let bytes = new Uint8Array(buffer); let len = bytes.byteLength; @@ -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 = 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://") @@ -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; } @@ -70,14 +88,20 @@ export default class LongPoll { null, () => this.ontimeout(), (resp) => { + let status = 0; + let messages: Record[] = []; 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, @@ -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); } @@ -147,7 +171,7 @@ export default class LongPoll { } } - batchSend(messages) { + batchSend(messages: string[] | ArrayBuffer[]) { this.awaitingBatchAck = true; this.ajax( "POST", @@ -155,9 +179,10 @@ export default class LongPoll { 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); @@ -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(); } @@ -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)); @@ -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(); diff --git a/phoenix/serializer.ts b/phoenix/serializer.ts index 62a74bd..5d9cae4 100644 --- a/phoenix/serializer.ts +++ b/phoenix/serializer.ts @@ -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; -} +}; -export interface BinaryMessage extends MessageMeta { +export type BinaryMessage = MessageMeta & { payload: ArrayBuffer; -} +}; export type DecodedMessage = ObjectMessage | BinaryMessage; export type EncodedMessage = ArrayBuffer | string;