From 208203b88e11a38c43eae1ad8b0ecc5ee82fe9b2 Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Tue, 15 Mar 2022 12:42:40 +1300 Subject: [PATCH] feat: DiscordWSService --- DiscordWS/index.ts | 78 +++++++++++++++++++++++++++++++++++++++ Log/index.ts | 18 +++++++++ WS/index.ts | 45 +++++++++-------------- mod.ts | 26 ++++++------- package.json | 4 +- yarn.lock | 91 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 220 insertions(+), 42 deletions(-) create mode 100644 Log/index.ts diff --git a/DiscordWS/index.ts b/DiscordWS/index.ts index e69de29..fddee97 100644 --- a/DiscordWS/index.ts +++ b/DiscordWS/index.ts @@ -0,0 +1,78 @@ +import * as T from "@effect-ts/core/Effect" +import * as S from "@effect-ts/core/Effect/Experimental/Stream" +import * as SC from "@effect-ts/core/Effect/Schedule" +import { pipe } from "@effect-ts/core/Function" +import { tag } from "@effect-ts/core/Has" +import { _A } from "@effect-ts/core/Utils" +import { RawData } from "ws" +import { log } from "../Log" +import { GatewayPayload } from "../types" +import * as WS from "../WS" + +export type Message = GatewayPayload | WS.Reconnect + +export interface OpenOpts { + url?: string + version?: number + encoding?: Encoding + outgoing: WS.OutboundStream +} + +export interface Encoding { + type: "json" | "etf" + encode: (p: GatewayPayload) => string | Buffer | ArrayBuffer + decode: (p: RawData) => GatewayPayload +} + +export const jsonEncoding: Encoding = { + type: "json", + encode: (p) => JSON.stringify(p), + decode: (p) => JSON.parse(p.toString("utf8")), +} + +const makeOutgoing = (s: S.UIO, e: Encoding): WS.OutboundStream => + pipe( + s, + S.map((data) => { + if (data === WS.Reconnect) { + return data + } + + return e.encode(data) + }) + ) + +const openImpl = ({ + url = "wss://gateway.discord.gg/", + version = 9, + encoding = jsonEncoding, + outgoing, +}: OpenOpts) => + pipe( + WS.open( + `${url}?v=${version}&encoding=${encoding.type}`, + makeOutgoing(outgoing, encoding) + ), + S.unwrap, + S.onError((e) => + e._tag === "Fail" ? log(serviceTag, "error", e.value) : T.unit + ), + S.retry(SC.exponential(10)), + S.map(encoding.decode) + ) + +export type Connection = ReturnType + +// Service definition +const serviceTag = "DiscordWSService" as const +const makeDiscordWS = T.succeed({ + _tag: serviceTag, + open: openImpl, +} as const) +export interface DiscordWS extends _A {} +export const DiscordWS = tag() +export const LiveDiscordWS = T.toLayer(DiscordWS)(makeDiscordWS) + +// Helpers +export const open = (opts: OpenOpts) => + T.accessService(DiscordWS)(({ open }) => open(opts)) diff --git a/Log/index.ts b/Log/index.ts new file mode 100644 index 0000000..574d66c --- /dev/null +++ b/Log/index.ts @@ -0,0 +1,18 @@ +import * as T from "@effect-ts/core/Effect" +import { tag } from "@effect-ts/core/Has" +import { _A } from "@effect-ts/core/Utils" + +const service = { + _tag: "LogService", + log: (...args: any[]) => + T.succeedWith(() => { + console.error(...args) + }), +} as const + +export type Log = typeof service +export const Log = tag() +export const LiveLog = T.toLayer(Log)(T.succeed(service)) + +export const log = (...args: any[]) => + T.accessServiceM(Log)(({ log }) => log(...args)) diff --git a/WS/index.ts b/WS/index.ts index 6d8ec2f..d54bbe3 100644 --- a/WS/index.ts +++ b/WS/index.ts @@ -1,7 +1,6 @@ import * as T from "@effect-ts/core/Effect" import * as S from "@effect-ts/core/Effect/Experimental/Stream" import * as M from "@effect-ts/core/Effect/Managed" -import * as Q from "@effect-ts/core/Effect/Queue" import * as SC from "@effect-ts/core/Effect/Schedule" import { pipe } from "@effect-ts/core/Function" import { tag } from "@effect-ts/core/Has" @@ -14,17 +13,14 @@ export type WsError = | { _tag: "error"; cause: unknown } | { _tag: "write"; cause: unknown } -type WebSocketStream = S.Stream +export type WebSocketStream = S.Stream export const Reconnect = Symbol() -export type Message = Ws.RawData | typeof Reconnect +export type Reconnect = typeof Reconnect +export type Message = string | Buffer | ArrayBuffer | Reconnect +export type OutboundStream = S.UIO -export interface WebSocketConnection { - read: WebSocketStream - write: Q.Queue -} - -const open = (url: string, options?: Ws.ClientOptions) => +const openSocket = (url: string, options?: Ws.ClientOptions) => pipe( T.succeedWith(() => new Ws.WebSocket(url, options)), M.makeExit((ws) => @@ -53,7 +49,7 @@ const recv = (ws: Ws.WebSocket): WebSocketStream => ) }) -const send = (out: Q.Queue) => (ws: Ws.WebSocket) => +const send = (out: OutboundStream) => (ws: Ws.WebSocket) => pipe( T.effectAsync((cb) => { if (ws.readyState & ws.OPEN) { @@ -64,7 +60,7 @@ const send = (out: Q.Queue) => (ws: Ws.WebSocket) => }) } }), - T.map(() => S.fromQueue()(out)), + T.map(() => out), S.unwrap, S.tap((data) => T.effectAsync((cb) => { @@ -85,33 +81,21 @@ const send = (out: Q.Queue) => (ws: Ws.WebSocket) => S.drain ) -const duplex = (out: Q.Queue) => (ws: Ws.WebSocket) => +const duplex = (out: OutboundStream) => (ws: Ws.WebSocket) => pipe(recv(ws), S.mergeTerminateLeft(send(out)(ws))) -const openDuplexWithQueue = ( +const openDuplex = ( url: string, - out: Q.Queue, + out: OutboundStream, options?: Ws.ClientOptions ): WebSocketStream => pipe( - open(url, options), + openSocket(url, options), M.map(duplex(out)), S.unwrapManaged, S.retry(SC.recurWhile((e) => e._tag === "close" && e.code === 1012)) ) -const openDuplex = ( - url: string, - options?: Ws.ClientOptions -): T.UIO => - pipe( - Q.makeUnbounded(), - T.map((write) => ({ - read: openDuplexWithQueue(url, write, options), - write, - })) - ) - const makeWS = T.succeed({ _tag: "WSService", open: openDuplex, @@ -120,3 +104,10 @@ const makeWS = T.succeed({ export interface WS extends _A {} export const WS = tag() export const LiveWS = T.toLayer(WS)(makeWS) + +// Helpers +export const open = ( + url: string, + out: OutboundStream, + options?: Ws.ClientOptions +) => T.accessService(WS)(({ open }) => open(url, out, options)) diff --git a/mod.ts b/mod.ts index de8b2a7..e8b2c44 100644 --- a/mod.ts +++ b/mod.ts @@ -1,24 +1,22 @@ import { Effect as T, pipe } from "@effect-ts/core" import * as S from "@effect-ts/core/Effect/Experimental/Stream" -import { exponential } from "@effect-ts/core/Effect/Schedule" +import * as Q from "@effect-ts/core/Effect/Queue" import * as R from "@effect-ts/node/Runtime" +import * as DiscordWS from "./DiscordWS" +import { LiveLog, log } from "./Log" import * as WS from "./WS" pipe( - T.accessServiceM(WS.WS)(({ open }) => - open("wss://gateway.discord.gg/?v=9&encoding=json") - ), - T.chain(({ read, write }) => - pipe( - read, - S.retry(exponential(10)), - S.forEach((data) => - T.succeedWith(() => { - console.error(data.toString("utf8")) - }) - ) - ) + Q.makeUnbounded(), + T.map(S.fromQueue()), + T.chain((outgoing) => + T.accessService(DiscordWS.DiscordWS)(({ open }) => open({ outgoing })) ), + T.chain(S.forEach((payload) => log(payload))), + + T.provideSomeLayer(LiveLog), T.provideSomeLayer(WS.LiveWS), + T.provideSomeLayer(DiscordWS.LiveDiscordWS), + R.runMain ) diff --git a/package.json b/package.json index 801afc6..28c8185 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "scripts": { "prepare": "ttsc", "types": "discord-api-codegen ./discord-api-docs -l typescript -o 'imports=Response|./DiscordREST/types' 'endpointReturnType=Response' > types.ts && prettier -w types.ts", - "clean": "git clean -fxd -e node_modules/ -e .env" + "clean": "git clean -fxd -e node_modules/ -e .env", + "dev": "ts-node -C ttypescript mod.ts" }, "files": [ "**/*.ts", @@ -21,6 +22,7 @@ "@tim-smart/discord-api-docs-parser": "^0.3.0", "@types/ws": "^8.5.3", "prettier": "^2.5.1", + "ts-node": "^10.7.0", "ttypescript": "^1.5.13", "typescript": "^4.6.2" }, diff --git a/yarn.lock b/yarn.lock index 393223b..c3eb5bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,18 @@ # yarn lockfile v1 +"@cspotcode/source-map-consumer@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" + integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== + +"@cspotcode/source-map-support@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" + integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== + dependencies: + "@cspotcode/source-map-consumer" "0.8.0" + "@effect-ts/core@^0.58.0": version "0.58.0" resolved "https://registry.yarnpkg.com/@effect-ts/core/-/core-0.58.0.tgz#5bdd3493d4d52ae997f57c0a04d4898df16ddccb" @@ -40,6 +52,26 @@ string "^3.3.3" yargs "^17.0.1" +"@tsconfig/node10@^1.0.7": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" + integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== + +"@tsconfig/node12@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" + integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== + +"@tsconfig/node14@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" + integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== + +"@tsconfig/node16@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" + integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== + "@types/node@*": version "17.0.21" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.21.tgz#864b987c0c68d07b4345845c3e63b75edd143644" @@ -52,6 +84,16 @@ dependencies: "@types/node" "*" +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.4.1: + version "8.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" + integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -64,6 +106,11 @@ ansi-styles@^4.0.0: dependencies: color-convert "^2.0.1" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -114,6 +161,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + css-select@^4.1.3: version "4.2.1" resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.2.1.tgz#9e665d6ae4c7f9d65dbe69d0316e3221fb274cdd" @@ -130,6 +182,11 @@ css-what@^5.0.1, css-what@^5.1.0: resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe" integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + dom-serializer@^1.0.1, dom-serializer@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" @@ -224,6 +281,11 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + marked@^2.0.3: version "2.1.3" resolved "https://registry.yarnpkg.com/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753" @@ -315,6 +377,25 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +ts-node@^10.7.0: + version "10.7.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.7.0.tgz#35d503d0fab3e2baa672a0e94f4b40653c2463f5" + integrity sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== + dependencies: + "@cspotcode/source-map-support" "0.7.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.0" + yn "3.1.1" + tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" @@ -332,6 +413,11 @@ typescript@^4.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4" integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg== +v8-compile-cache-lib@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz#0582bcb1c74f3a2ee46487ceecf372e46bce53e8" + integrity sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA== + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -368,3 +454,8 @@ yargs@^17.0.1: string-width "^4.2.3" y18n "^5.0.5" yargs-parser "^21.0.0" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==