From 8ece61b343437d176c8976ffb4dac988aedd3eb1 Mon Sep 17 00:00:00 2001 From: Jesse Kelly Date: Tue, 5 Dec 2023 19:15:51 +0100 Subject: [PATCH] feat: added supporting ws files --- packages/platform-browser/src/WebSocket.ts | 68 +++---------------- .../src/internal/webSocket.ts | 68 +++++++++++++++++++ packages/platform/src/Error.ts | 10 ++- packages/platform/src/WebSocket.ts | 44 ++++++++++++ packages/platform/src/internal/webSocket.ts | 17 +++++ 5 files changed, 149 insertions(+), 58 deletions(-) create mode 100644 packages/platform-browser/src/internal/webSocket.ts create mode 100644 packages/platform/src/WebSocket.ts create mode 100644 packages/platform/src/internal/webSocket.ts diff --git a/packages/platform-browser/src/WebSocket.ts b/packages/platform-browser/src/WebSocket.ts index e0dc1c1a..8d1db8f0 100644 --- a/packages/platform-browser/src/WebSocket.ts +++ b/packages/platform-browser/src/WebSocket.ts @@ -1,65 +1,19 @@ /** * @since 1.0.0 + * + * Also includes exports from [`@effect/platform/WebSocket`](https://effect-ts.github.io/platform/platform/WebSocket.ts.html). */ -import * as Chunk from "effect/Chunk" -import * as Context from "effect/Context" -import * as Effect from "effect/Effect" -import type * as Scope from "effect/Scope" -import * as Stream from "effect/Stream" - -interface Socket { - send: (data: string | Blob) => Effect.Effect - messages: Stream.Stream - errors: Stream.Stream -} - -export const tag = Context.Tag("@effect/platform/WebSocket") - -/** @internal */ -const createSocket = (url: string | URL) => - Effect.acquireRelease( - Effect.flatMap( - Effect.try({ - try: () => new WebSocket(url), - catch: (err) => err as Error - }), - (socket) => - Effect.async((resume) => { - socket.addEventListener("open", () => { - resume(Effect.succeed(socket)) - }) - }) - ), - (s) => Effect.succeed(s.close()) // could fail - ) +import * as internal from "./internal/webSocket.js" /** * @since 1.0.0 */ -export const layer = (url: string | URL): Effect.Effect => - Effect.gen(function*(_) { - const socket = yield* _(createSocket(url)) - - const send = (data: string | Blob) => Effect.sync(() => socket.send(data)) - - const messages = Stream.async((emit) => { - socket.addEventListener("message", (event) => { - emit(Effect.succeed(Chunk.of(event.data as string | Blob | ArrayBuffer))) - }) - }) - - const errors = Stream.async((emit) => { - socket.addEventListener("message", (event) => { - emit(Effect.succeed(Chunk.of(event.data))) - }) - }) +export * from "@effect/platform/WebSocket" - return tag.of({ send, messages, errors }) - }) - -const test = Effect.gen(function*(_) { - const ws = yield* _(layer("ws://localhost:3000")) - yield* _(ws.send("abc")) -}).pipe(Effect.scoped, Effect.tapError((err) => Effect.logError(err))) - -Effect.runFork(test) +/** + * Creates a WebSocket layer. + * + * @since 1.0.0 + * @category models + */ +export const layer = internal.layer diff --git a/packages/platform-browser/src/internal/webSocket.ts b/packages/platform-browser/src/internal/webSocket.ts new file mode 100644 index 00000000..9cbea75d --- /dev/null +++ b/packages/platform-browser/src/internal/webSocket.ts @@ -0,0 +1,68 @@ +/** + * @since 1.0.0 + */ +import * as PlatformError from "@effect/platform/Error" +import * as Socket from "@effect/platform/WebSocket" +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Stream from "effect/Stream" + +const make = (impl: Omit) => + Socket.Socket.of({ + [Socket.TypeId]: Socket.TypeId, + ...impl + }) + +/** @internal */ +const createSocket = (url: string | URL) => + Effect.acquireRelease( + Effect.flatMap( + Effect.try({ + try: () => new WebSocket(url), + catch: () => + PlatformError.SystemError({ + module: "WebSocket", + reason: "InvalidData", + message: "Unable to create WebSocket. Invalid host.", + method: "", + pathOrDescriptor: "" + }) + }), + (socket) => + Effect.async((resume) => { + socket.addEventListener("open", () => { + resume(Effect.succeed(socket)) + }) + }) + ), + (s) => Effect.succeed(s.close(1001)) // .close is unsafe.. + ) + +/** + * @since 1.0.0 + */ +export const layer = (url: string | URL): Layer.Layer => + Layer.scoped( + Socket.Socket, + Effect.gen(function*(_) { + const socket = yield* _(createSocket(url)) + + // Safe as long as the socket is fully connected. + const send = (data: string | Blob) => Effect.sync(() => socket.send(data)) + + const messages = Stream.async((emit) => { + socket.addEventListener("message", (event) => { + emit(Effect.succeed(Chunk.of(event.data as string | Blob | ArrayBuffer))) + }) + }) + + const errors = Stream.async((emit) => { + socket.addEventListener("error", (event) => { + emit(Effect.succeed(Chunk.of(event))) + }) + }) + + return make({ send, messages, errors }) + }) + ) diff --git a/packages/platform/src/Error.ts b/packages/platform/src/Error.ts index bf497352..cb5be117 100644 --- a/packages/platform/src/Error.ts +++ b/packages/platform/src/Error.ts @@ -33,7 +33,15 @@ export declare namespace PlatformError { export interface Base extends Data.Case { readonly [PlatformErrorTypeId]: typeof PlatformErrorTypeId readonly _tag: string - readonly module: "Clipboard" | "Command" | "FileSystem" | "KeyValueStore" | "Path" | "Stream" | "Terminal" + readonly module: + | "Clipboard" + | "Command" + | "FileSystem" + | "KeyValueStore" + | "Path" + | "Stream" + | "Terminal" + | "WebSocket" readonly method: string readonly message: string } diff --git a/packages/platform/src/WebSocket.ts b/packages/platform/src/WebSocket.ts new file mode 100644 index 00000000..19041019 --- /dev/null +++ b/packages/platform/src/WebSocket.ts @@ -0,0 +1,44 @@ +/** + * @since 1.0.0 + */ +import type * as Context from "effect/Context" +import type * as Effect from "effect/Effect" +import type * as Stream from "effect/Stream" +import * as internal from "./internal/webSocket.js" + +/** + * @since 1.0.0 + * @category type id + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 1.0.0 + * @category type id + */ +export type TypeId = typeof TypeId + +/** + * @since 1.0.0 + * @category models + */ +export interface Socket { + readonly [TypeId]: TypeId + send: (data: string | Blob) => Effect.Effect + messages: Stream.Stream + errors: Stream.Stream +} + +/** + * @since 1.0.0 + * @category constructors + */ +export const make: ( + impl: Omit +) => Socket = internal.make + +/** + * @since 1.0.0 + * @category tags + */ +export const Socket: Context.Tag = internal.tag diff --git a/packages/platform/src/internal/webSocket.ts b/packages/platform/src/internal/webSocket.ts new file mode 100644 index 00000000..bc3741c1 --- /dev/null +++ b/packages/platform/src/internal/webSocket.ts @@ -0,0 +1,17 @@ +/** + * @since 1.0.0 + */ +import * as Context from "effect/Context" +import type * as WS from "../WebSocket.js" + +export const TypeId: WS.TypeId = Symbol.for( + "@effect/platform/WebSocket" +) as WS.TypeId + +export const tag = Context.Tag("@effect/platform/WebSocket") + +export const make = (impl: Omit) => + tag.of({ + [TypeId]: TypeId, + ...impl + })