Repository: neet/masto.js
Pull request #228 (2020-03-07 03:01:07)

Decouple websocket


Patch:
diff --git a/src/gateway/__mocks__/event-handler-impl.ts b/src/gateway/__mocks__/event-handler-impl.ts
new file mode 100644
index 00000000..92df386f
--- /dev/null
+++ b/src/gateway/__mocks__/event-handler-impl.ts
@@ -0,0 +1,9 @@
+import { EventHandler, EventTypeMap } from '../event-handler';
+import EventEmitter from 'eventemitter3';
+
+export const mockConnect = jest.fn();
+
+export class EventHandlerImpl extends EventEmitter<EventTypeMap> implements EventHandler {
+  connect = mockConnect;
+  disconnect = () => {/* noop */}
+}
diff --git a/src/gateway/__mocks__/websocket.ts b/src/gateway/__mocks__/websocket.ts
deleted file mode 100644
index 5dd9b7f9..00000000
--- a/src/gateway/__mocks__/websocket.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export const mockConnect = jest.fn();
-
-export const WebSocketEvents = jest.fn(() => ({
-  connect: mockConnect,
-}));
diff --git a/src/gateway/create-form-data.ts b/src/gateway/create-form-data.ts
index 128b9bff..c320a154 100644
--- a/src/gateway/create-form-data.ts
+++ b/src/gateway/create-form-data.ts
@@ -1,6 +1,8 @@
 // eslint-disable-next-line import/no-unassigned-import
 import 'isomorphic-form-data';
 
+export const isArray = (x: unknown): x is unknown[] =>
+  typeof x === 'object' && x !== null && x.constructor === Array;
 export const isObject = (x: unknown): x is { [key: string]: unknown } =>
   typeof x === 'object' && x !== null && x.constructor === Object;
 
@@ -32,7 +34,7 @@ export function createFormData(
   result = new FormData(),
   parentKey = '',
 ) {
-  if (Array.isArray(encodable)) {
+  if (isArray(encodable)) {
     encodable.forEach((value, i) => {
       createFormData(value, result, `${parentKey}[${i}]`);
     });
diff --git a/src/gateway/websocket.ts b/src/gateway/event-handler-impl.ts
similarity index 58%
rename from src/gateway/websocket.ts
rename to src/gateway/event-handler-impl.ts
index 206482d2..ffc4d9e9 100644
--- a/src/gateway/websocket.ts
+++ b/src/gateway/event-handler-impl.ts
@@ -1,34 +1,12 @@
 import EventEmitter from 'eventemitter3';
 import WebSocket from 'isomorphic-ws';
-import { Conversation, Notification, Status } from '../entities';
-
-/** Map of event name and callback argument */
-export interface EventTypeMap {
-  /** Status posted */
-  update: [Status];
-  /** Status deleted */
-  delete: [Status['id']];
-  /** User's notification */
-  notification: [Notification];
-  /** User's filter changed */
-  filters_changed: [];
-  /** Status added to a conversation */
-  conversation: [Conversation];
-}
-
-/** Supported event names */
-export type EventType = keyof EventTypeMap;
-
-/** Mastodon event */
-export interface Event {
-  event: EventType;
-  payload: string;
-}
+import { EventHandler, EventTypeMap, EventType, Event } from './event-handler';
 
 /**
  * Mastodon streaming api wrapper
  */
-export class WebSocketEvents extends EventEmitter<EventTypeMap> {
+export class EventHandlerImpl extends EventEmitter<EventTypeMap>
+  implements EventHandler {
   private ws?: WebSocket;
 
   /**
@@ -38,7 +16,7 @@ export class WebSocketEvents extends EventEmitter<EventTypeMap> {
    * @param params URL parameters
    */
   connect(url: string, protocols?: string | string[]) {
-    return new Promise<WebSocketEvents>((resolve, reject) => {
+    return new Promise<this>((resolve, reject) => {
       this.ws = new WebSocket(url, protocols);
       this.ws.addEventListener('open', () => resolve(this));
       this.ws.addEventListener('message', this.handleMessage);
@@ -59,7 +37,7 @@ export class WebSocketEvents extends EventEmitter<EventTypeMap> {
    * @param message Websocket message
    */
   handleMessage = ({ data }: { data: string }) => {
-    const event = JSON.parse(data) as Event;
+    const event: Event = JSON.parse(data);
     let args: EventTypeMap[EventType];
 
     try {
diff --git a/src/gateway/event-handler.ts b/src/gateway/event-handler.ts
new file mode 100644
index 00000000..a3957048
--- /dev/null
+++ b/src/gateway/event-handler.ts
@@ -0,0 +1,30 @@
+import EventEmitter from 'eventemitter3';
+import { Conversation, Notification, Status } from '../entities';
+
+/** Map of event name and callback argument */
+export interface EventTypeMap {
+  /** Status posted */
+  update: [Status];
+  /** Status deleted */
+  delete: [Status['id']];
+  /** User's notification */
+  notification: [Notification];
+  /** User's filter changed */
+  filters_changed: [];
+  /** Status added to a conversation */
+  conversation: [Conversation];
+}
+
+/** Supported event names */
+export type EventType = keyof EventTypeMap;
+
+/** Mastodon event */
+export interface Event {
+  event: EventType;
+  payload: string;
+}
+
+export interface EventHandler extends EventEmitter<EventTypeMap> {
+  connect(url: string, protocols: string[]): Promise<EventHandler>;
+  disconnect(): void;
+}
diff --git a/src/gateway/gateway-impl.ts b/src/gateway/gateway-impl.ts
index 824d38fd..798b12eb 100644
--- a/src/gateway/gateway-impl.ts
+++ b/src/gateway/gateway-impl.ts
@@ -12,7 +12,7 @@ import {
 } from '../errors';
 import { createFormData } from './create-form-data';
 import { isAxiosError } from './is-axios-error';
-import { WebSocketEvents } from './websocket';
+import { EventHandlerImpl } from './event-handler-impl';
 import {
   Gateway,
   GatewayConstructorParams,
@@ -311,7 +311,7 @@ export class GatewayImpl implements Gateway<AxiosRequestConfig> {
       path +
       (Object.keys(params).length ? `?${querystring.stringify(params)}` : '');
 
-    return new WebSocketEvents().connect(url, protocols);
+    return new EventHandlerImpl().connect(url, protocols);
   }
 
   /**
diff --git a/src/gateway/gateway.ts b/src/gateway/gateway.ts
index de691249..eb6e1d3f 100644
--- a/src/gateway/gateway.ts
+++ b/src/gateway/gateway.ts
@@ -1,5 +1,5 @@
 import { ParsedUrlQueryInput } from 'querystring';
-import EventEmitter from 'eventemitter3';
+import { EventHandler } from './event-handler';
 
 /**
  * Argument of `Gateway.paginate().next()`.
@@ -27,7 +27,7 @@ export interface Gateway<Options = undefined> {
   put<T>(path: string, data: unknown, options?: Options): Promise<T>;
   patch<T>(path: string, data: unknown, options?: Options): Promise<T>;
   delete<T>(path: string, data: unknown, options?: Options): Promise<T>;
-  stream(path: string, params: ParsedUrlQueryInput): Promise<EventEmitter>;
+  stream(path: string, params: ParsedUrlQueryInput): Promise<EventHandler>;
   paginate<T, U>(
     url: string,
     params?: U,
diff --git a/src/gateway/index.ts b/src/gateway/index.ts
index 9e31d393..e359096e 100644
--- a/src/gateway/index.ts
+++ b/src/gateway/index.ts
@@ -1,6 +1,7 @@
 export * from './create-form-data';
 export * from './decorators';
-export * from './gateway';
+export * from './event-handler-impl';
+export * from './event-handler';
 export * from './gateway-impl';
+export * from './gateway';
 export * from './is-axios-error';
-export * from './websocket';


Test patch:
diff --git a/src/clients/masto/__tests__/masto.spec.ts b/src/clients/masto/__tests__/masto.spec.ts
index d2a39c72..9f897446 100644
--- a/src/clients/masto/__tests__/masto.spec.ts
+++ b/src/clients/masto/__tests__/masto.spec.ts
@@ -4,7 +4,6 @@ import { Masto } from '../masto';
 // prettier-ignore
 import { Gateway, mockGet, mockPost, mockDelete, mockPut, mockPatch, mockPaginate, mockStream } from '../../../gateway/gateway-impl';
 
-jest.mock('../../../gateway/websocket');
 jest.mock('../../../gateway/gateway-impl');
 
 describe('Masto', () => {
diff --git a/src/gateway/__tests__/create-form-data.spec.ts b/src/gateway/__tests__/create-form-data.spec.ts
index 03646ab4..74f0b878 100644
--- a/src/gateway/__tests__/create-form-data.spec.ts
+++ b/src/gateway/__tests__/create-form-data.spec.ts
@@ -1,6 +1,6 @@
 // eslint-disable-next-line import/no-unassigned-import
 import 'isomorphic-form-data';
-import { createFormData, isObject } from '../create-form-data';
+import { createFormData, isArray, isObject } from '../create-form-data';
 
 /**
  * Unfortunately form-data node.js package doesn't have
@@ -27,6 +27,18 @@ const getAll = (formData: FormData, name: string) =>
 
 const get = (formData: FormData, name: string) => getAll(formData, name)[0];
 
+test('typed isArray', () => {
+  const r1 = isArray([]);
+  const r2 = isArray([1, 2]);
+  const r3 = isArray({});
+  const r4 = isArray(null);
+
+  expect(r1).toBe(true);
+  expect(r2).toBe(true);
+  expect(r3).toBe(false);
+  expect(r4).toBe(false);
+});
+
 test('typed isObject', () => {
   const r1 = isObject(null);
   const r2 = isObject([1, 2]);
diff --git a/src/gateway/__tests__/websocket.spec.ts b/src/gateway/__tests__/event-handler-impl.spec.ts
similarity index 91%
rename from src/gateway/__tests__/websocket.spec.ts
rename to src/gateway/__tests__/event-handler-impl.spec.ts
index 795767ac..665f68ed 100644
--- a/src/gateway/__tests__/websocket.spec.ts
+++ b/src/gateway/__tests__/event-handler-impl.spec.ts
@@ -1,5 +1,5 @@
 import WebSocket from 'isomorphic-ws';
-import { WebSocketEvents } from '../websocket';
+import { EventHandlerImpl } from '../event-handler-impl';
 
 const mockOn = jest.fn();
 const mockEmit = jest.fn();
@@ -24,11 +24,11 @@ jest.mock('isomorphic-ws', () => {
   }));
 });
 
-describe('WebSocketEvents', () => {
-  let wsEvents!: WebSocketEvents;
+describe('EventHandlerImpl', () => {
+  let wsEvents!: EventHandlerImpl;
 
   beforeEach(() => {
-    wsEvents = new WebSocketEvents();
+    wsEvents = new EventHandlerImpl();
     mockOn.mockClear();
     mockAddEventListenerWs.mockClear();
     mockEmit.mockClear();
@@ -75,6 +75,7 @@ describe('WebSocketEvents', () => {
     expect(mockCloseWs).toBeCalled();
   });
 
+  // Fix this tests: handleMessage supposed to be private
   test('emit event with given props', () => {
     const originalPayload = {
       aaaa: 'foobar',
diff --git a/src/gateway/__tests__/gateway-impl.spec.ts b/src/gateway/__tests__/gateway-impl.spec.ts
index 3c8ad9ae..feeb4f33 100644
--- a/src/gateway/__tests__/gateway-impl.spec.ts
+++ b/src/gateway/__tests__/gateway-impl.spec.ts
@@ -2,7 +2,7 @@
 import axios from 'axios';
 import { GatewayImpl } from '../gateway-impl';
 // @ts-ignore
-import { WebSocketEvents, mockConnect } from '../websocket';
+import { EventHandlerImpl, mockConnect } from '../event-handler-impl';
 import {
   MastoNotFoundError,
   MastoRateLimitError,
@@ -12,7 +12,7 @@ import {
 } from '../../errors';
 import 'isomorphic-form-data';
 
-jest.mock('../websocket');
+jest.mock('../event-handler-impl');
 
 // Mock `axios.create`. We don't use any functions from axios
 // but from `axios.create`