| import * as Cause from "../../Cause.js"; | |
| import * as Context from "../../Context.js"; | |
| import * as Deferred from "../../Deferred.js"; | |
| import * as Effect from "../../Effect.js"; | |
| import * as Exit from "../../Exit.js"; | |
| import * as Fiber from "../../Fiber.js"; | |
| import { constant, constTrue, constVoid, identity } from "../../Function.js"; | |
| import { reportCauseUnsafe } from "../../internal/effect.js"; | |
| import * as Latch from "../../Latch.js"; | |
| import * as Layer from "../../Layer.js"; | |
| import * as Option from "../../Option.js"; | |
| import * as Predicate from "../../Predicate.js"; | |
| import * as Pull from "../../Pull.js"; | |
| import * as Queue from "../../Queue.js"; | |
| import * as Schedule from "../../Schedule.js"; | |
| import * as Schema from "../../Schema.js"; | |
| import * as Scope from "../../Scope.js"; | |
| import * as Semaphore from "../../Semaphore.js"; | |
| import { Stdio } from "../../Stdio.js"; | |
| import * as Stream from "../../Stream.js"; | |
| import * as Tracer from "../../Tracer.js"; | |
| import * as Headers from "../http/Headers.js"; | |
| import * as HttpRouter from "../http/HttpRouter.js"; | |
| import * as HttpServerRequest from "../http/HttpServerRequest.js"; | |
| import * as HttpServerResponse from "../http/HttpServerResponse.js"; | |
| import * as SocketServer from "../socket/SocketServer.js"; | |
| import * as Transferable from "../workers/Transferable.js"; | |
| import * as WorkerRunner from "../workers/WorkerRunner.js"; | |
| import * as Rpc from "./Rpc.js"; | |
| import { constEof, constPong, RequestId, ResponseDefectEncoded } from "./RpcMessage.js"; | |
| import * as RpcSchema from "./RpcSchema.js"; | |
| import * as RpcSerialization from "./RpcSerialization.js"; | |
| import { withRun } from "./Utils.js"; | |
| /** | |
| * Creates an RPC server for an already-decoded message channel, running | |
| * handlers for a group and sending decoded server responses through | |
| * `onFromServer`. | |
| * | |
| * @category server | |
| * @since 4.0.0 | |
| */ | |
| export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (group, options) { | |
| const enableTracing = options.disableTracing !== true; | |
| const enableSpanPropagation = options.disableSpanPropagation !== true; | |
| const supportsAck = options.disableClientAcks !== true; | |
| const spanPrefix = options.spanPrefix ?? "RpcServer"; | |
| const concurrency = options.concurrency ?? "unbounded"; | |
| const disableFatalDefects = options.disableFatalDefects ?? false; | |
| const services = yield* Effect.context(); | |
| const scope = Context.get(services, Scope.Scope); | |
| const trackFiber = Fiber.runIn(Scope.forkUnsafe(scope, "parallel")); | |
| const concurrencySemaphore = concurrency === "unbounded" ? undefined : yield* Semaphore.make(concurrency); | |
| const clients = new Map(); | |
| let isShutdown = false; | |
| const shutdownLatch = Latch.makeUnsafe(false); | |
| yield* Scope.addFinalizer(scope, Effect.withFiber(parent => { | |
| isShutdown = true; | |
| for (const client of clients.values()) { | |
| client.ended = true; | |
| if (client.fibers.size === 0) { | |
| trackFiber(Effect.runForkWith(services)(endClient(client))); | |
| continue; | |
| } | |
| for (const fiber of client.fibers.values()) { | |
| fiber.interruptUnsafe(parent.id); | |
| } | |
| } | |
| if (clients.size === 0) { | |
| return Effect.void; | |
| } | |
| return shutdownLatch.await; | |
| })); | |
| const disconnect = clientId => Effect.withFiber(parent => { | |
| const client = clients.get(clientId); | |
| if (!client) return Effect.void; | |
| for (const fiber of client.fibers.values()) { | |
| fiber.interruptUnsafe(parent.id); | |
| } | |
| clients.delete(clientId); | |
| return Effect.void; | |
| }); | |
| const write = (clientId, message) => Effect.catchDefect(Effect.withFiber(requestFiber => { | |
| if (isShutdown) return Effect.interrupt; | |
| let client = clients.get(clientId); | |
| if (!client) { | |
| client = { | |
| id: clientId, | |
| latches: new Map(), | |
| fibers: new Map(), | |
| ended: false, | |
| serverClient: new Rpc.ServerClient(clientId) | |
| }; | |
| clients.set(clientId, client); | |
| } else if (client.ended) { | |
| return Effect.interrupt; | |
| } | |
| switch (message._tag) { | |
| case "Request": | |
| { | |
| return handleRequest(requestFiber, client, message); | |
| } | |
| case "Ack": | |
| { | |
| const latch = client.latches.get(message.requestId); | |
| return latch ? latch.open : Effect.void; | |
| } | |
| case "Interrupt": | |
| { | |
| const fiber = client.fibers.get(message.requestId); | |
| if (fiber) { | |
| fiber.interruptUnsafe(requestFiber.id, RpcSchema.ClientAbort.annotation); | |
| return Effect.void; | |
| } | |
| return options.onFromServer({ | |
| _tag: "Exit", | |
| clientId, | |
| requestId: message.requestId, | |
| exit: Exit.interrupt() | |
| }); | |
| } | |
| case "Eof": | |
| { | |
| client.ended = true; | |
| if (client.fibers.size > 0) return Effect.void; | |
| return endClient(client); | |
| } | |
| default: | |
| { | |
| return sendDefect(client, `Unknown request tag: ${message._tag}`); | |
| } | |
| } | |
| }), defect => sendDefect(clients.get(clientId), defect)); | |
| const endClient = client => { | |
| clients.delete(client.id); | |
| const write = options.onFromServer({ | |
| _tag: "ClientEnd", | |
| clientId: client.id | |
| }); | |
| if (isShutdown && clients.size === 0) { | |
| return Effect.andThen(write, shutdownLatch.open); | |
| } | |
| return write; | |
| }; | |
| const handleRequest = (requestFiber, client, request) => { | |
| if (client.fibers.has(request.id)) { | |
| return Effect.interrupt; | |
| } | |
| const rpc = group.requests.get(request.tag); | |
| const entry = services.mapUnsafe.get(rpc?.key); | |
| if (!rpc || !entry) { | |
| const write = Effect.catchDefect(options.onFromServer({ | |
| _tag: "Exit", | |
| clientId: client.id, | |
| requestId: request.id, | |
| exit: Exit.die(`Unknown request tag: ${request.tag}`) | |
| }), defect => sendDefect(client, defect)); | |
| if (!client.ended || client.fibers.size > 0) return write; | |
| return Effect.ensuring(write, endClient(client)); | |
| } | |
| const isStream = RpcSchema.isStreamSchema(rpc.successSchema); | |
| const metadata = { | |
| rpc, | |
| client: client.serverClient, | |
| requestId: request.id, | |
| headers: request.headers, | |
| payload: request.payload | |
| }; | |
| const result = entry.handler(request.payload, metadata); | |
| // if the handler requested forking, then we skip the concurrency control | |
| const isWrapper = Rpc.isWrapper(result); | |
| const isFork = isWrapper && result.fork; | |
| const isUninterruptible = isWrapper && result.uninterruptible; | |
| // unwrap the fork data type | |
| const streamOrEffect = isWrapper ? result.value : result; | |
| const handler = isStream ? streamEffect(client, request, streamOrEffect) : streamOrEffect; | |
| const withMiddleware = rpc.middlewares.size > 0 ? applyMiddleware(services, handler, metadata) : handler; | |
| let responded = false; | |
| const scope = Scope.makeUnsafe(); | |
| let deferred = undefined; | |
| let effect = Effect.onExit(withMiddleware, exit => { | |
| responded = true; | |
| let write; | |
| if (exit._tag === "Success") { | |
| if (Deferred.isDeferred(exit.value)) { | |
| deferred = exit.value; | |
| write = Effect.void; | |
| } else { | |
| write = options.onFromServer({ | |
| _tag: "Exit", | |
| clientId: client.id, | |
| requestId: request.id, | |
| exit: exit | |
| }); | |
| } | |
| } else if (!disableFatalDefects && Cause.hasDies(exit.cause) && !Cause.hasInterrupts(exit.cause)) { | |
| write = sendDefect(client, Cause.squash(exit.cause)); | |
| } else { | |
| write = options.onFromServer({ | |
| _tag: "Exit", | |
| clientId: client.id, | |
| requestId: request.id, | |
| exit: exit | |
| }); | |
| } | |
| const close = Scope.closeUnsafe(scope, exit); | |
| if (exit._tag === "Failure") { | |
| reportCauseUnsafe(Fiber.getCurrent(), exit.cause); | |
| } | |
| return close ? Effect.ensuring(write, close) : write; | |
| }); | |
| if (enableTracing) { | |
| const parentSpan = requestFiber.context.mapUnsafe.get(Tracer.ParentSpan.key); | |
| effect = Effect.withSpan(effect, `${spanPrefix}.${request.tag}`, { | |
| captureStackTrace: false, | |
| attributes: options.spanAttributes, | |
| parent: enableSpanPropagation && request.spanId ? Tracer.externalSpan({ | |
| traceId: request.traceId, | |
| spanId: request.spanId, | |
| sampled: request.sampled | |
| }) : undefined, | |
| links: enableSpanPropagation && parentSpan ? [{ | |
| span: parentSpan, | |
| attributes: {} | |
| }] : undefined | |
| }); | |
| } | |
| if (!isFork && concurrencySemaphore) { | |
| effect = concurrencySemaphore.withPermit(effect); | |
| } | |
| const context = new Map(entry.context.mapUnsafe); | |
| requestFiber.context.mapUnsafe.forEach((value, key) => context.set(key, value)); | |
| context.set(Scope.Scope.key, scope); | |
| const runFork = Effect.runForkWith(Context.makeUnsafe(context)); | |
| const fiber = trackFiber(runFork(effect, isUninterruptible ? { | |
| uninterruptible: true | |
| } : undefined)); | |
| client.fibers.set(request.id, fiber); | |
| fiber.addObserver(function onExit(exit) { | |
| if (deferred) { | |
| const fiber = trackFiber(runFork(Effect.onExit(Deferred.await(deferred), exit => options.onFromServer({ | |
| _tag: "Exit", | |
| clientId: client.id, | |
| requestId: request.id, | |
| exit: exit | |
| })))); | |
| client.fibers.set(request.id, fiber); | |
| deferred = undefined; | |
| fiber.addObserver(onExit); | |
| return; | |
| } | |
| if (!responded && exit._tag === "Failure") { | |
| trackFiber(runFork(options.onFromServer({ | |
| _tag: "Exit", | |
| clientId: client.id, | |
| requestId: request.id, | |
| exit: Exit.interrupt() | |
| }))); | |
| } | |
| client.fibers.delete(request.id); | |
| client.latches.delete(request.id); | |
| if (client.ended && client.fibers.size === 0) { | |
| trackFiber(runFork(endClient(client))); | |
| } | |
| }); | |
| return Effect.void; | |
| }; | |
| const streamEffect = (client, request, stream) => { | |
| let latch = client.latches.get(request.id); | |
| if (supportsAck && !latch) { | |
| latch = Latch.makeUnsafe(false); | |
| client.latches.set(request.id, latch); | |
| } | |
| if (Effect.isEffect(stream)) { | |
| return stream.pipe(Effect.flatMap(queue => Effect.whileLoop({ | |
| while: constTrue, | |
| body: constant(Effect.flatMap(Queue.takeAll(queue), values => { | |
| const write = options.onFromServer({ | |
| _tag: "Chunk", | |
| clientId: client.id, | |
| requestId: request.id, | |
| values | |
| }); | |
| if (!latch) return write; | |
| latch.closeUnsafe(); | |
| return Effect.flatMap(write, () => latch.await); | |
| })), | |
| step: constVoid | |
| })), Pull.catchDone(() => Effect.void), Effect.scoped); | |
| } | |
| return Stream.runForEachArray(stream, values => { | |
| const write = options.onFromServer({ | |
| _tag: "Chunk", | |
| clientId: client.id, | |
| requestId: request.id, | |
| values | |
| }); | |
| if (!latch) return write; | |
| latch.closeUnsafe(); | |
| return Effect.andThen(write, latch.await); | |
| }); | |
| }; | |
| const sendDefect = (client, defect) => Effect.suspend(() => { | |
| const shouldEnd = client.ended && client.fibers.size === 0; | |
| const write = options.onFromServer({ | |
| _tag: "Defect", | |
| clientId: client.id, | |
| defect | |
| }); | |
| if (!shouldEnd) return write; | |
| return Effect.andThen(write, endClient(client)); | |
| }); | |
| return identity({ | |
| write, | |
| disconnect | |
| }); | |
| }); | |
| const applyMiddleware = (context, handler, options) => { | |
| for (const service of options.rpc.middlewares) { | |
| const middleware = Context.getUnsafe(context, service); | |
| handler = middleware(handler, options); | |
| } | |
| return handler; | |
| }; | |
| /** | |
| * Runs an RPC server for a group using the current server `Protocol`, decoding | |
| * requests, invoking handlers, encoding responses, and managing in-flight | |
| * request lifetime. | |
| * | |
| * @category server | |
| * @since 4.0.0 | |
| */ | |
| export const make = /*#__PURE__*/Effect.fnUntraced(function* (group, options) { | |
| const { | |
| disconnects, | |
| end, | |
| run, | |
| send, | |
| supportsAck, | |
| supportsSpanPropagation, | |
| supportsTransferables | |
| } = yield* Protocol; | |
| const services = yield* Effect.context(); | |
| const scope = yield* Scope.make(); | |
| const server = yield* makeNoSerialization(group, { | |
| ...options, | |
| disableClientAcks: !supportsAck, | |
| disableSpanPropagation: !supportsSpanPropagation, | |
| onFromServer(response) { | |
| const client = clients.get(response.clientId); | |
| if (!client) return Effect.void; | |
| switch (response._tag) { | |
| case "Chunk": | |
| { | |
| const schemas = client.schemas.get(response.requestId); | |
| if (!schemas) return Effect.void; | |
| return handleEncode(client, response.requestId, schemas.encodeDefect, schemas.collector, Effect.provideContext(schemas.encodeChunk(response.values), schemas.context), values => ({ | |
| _tag: "Chunk", | |
| requestId: String(response.requestId), | |
| values | |
| })); | |
| } | |
| case "Exit": | |
| { | |
| const schemas = client.schemas.get(response.requestId); | |
| if (!schemas) return Effect.void; | |
| client.schemas.delete(response.requestId); | |
| return handleEncode(client, response.requestId, schemas.encodeDefect, schemas.collector, Effect.provideContext(schemas.encodeExit(response.exit), schemas.context), exit => ({ | |
| _tag: "Exit", | |
| requestId: String(response.requestId), | |
| exit | |
| })); | |
| } | |
| case "Defect": | |
| { | |
| return sendDefect(client, response.defect); | |
| } | |
| case "ClientEnd": | |
| { | |
| clients.delete(response.clientId); | |
| return end(response.clientId); | |
| } | |
| } | |
| } | |
| }).pipe(Scope.provide(scope)); | |
| // handle disconnects | |
| yield* Effect.forkChild(Effect.whileLoop({ | |
| while: constTrue, | |
| body: constant(Effect.flatMap(Queue.take(disconnects), clientId => { | |
| clients.delete(clientId); | |
| return server.disconnect(clientId); | |
| })), | |
| step: constVoid | |
| })); | |
| const schemasCache = new WeakMap(); | |
| const getSchemas = rpc => { | |
| let schemas = schemasCache.get(rpc); | |
| if (!schemas) { | |
| const entry = services.mapUnsafe.get(rpc.key); | |
| const streamSchemas = RpcSchema.getStreamSchemas(rpc.successSchema); | |
| schemas = { | |
| decode: Schema.decodeUnknownEffect(Schema.toCodecJson(rpc.payloadSchema)), | |
| encodeChunk: Schema.encodeUnknownEffect(Schema.toCodecJson(Schema.Array(Option.isSome(streamSchemas) ? streamSchemas.value.success : Schema.Any))), | |
| encodeExit: Schema.encodeUnknownEffect(Schema.toCodecJson(Rpc.exitSchema(rpc))), | |
| encodeDefect: Schema.encodeUnknownEffect(Schema.toCodecJson(rpc.defectSchema)), | |
| context: entry.context | |
| }; | |
| schemasCache.set(rpc, schemas); | |
| } | |
| return schemas; | |
| }; | |
| const clients = new Map(); | |
| const handleEncode = (client, requestId, encodeDefect, collector, effect, onSuccess) => (collector ? Effect.provideService(effect, Transferable.Collector, collector) : effect).pipe(Effect.flatMap(a => send(client.id, onSuccess(a), collector && collector.clearUnsafe())), Effect.catchCause(cause => { | |
| client.schemas.delete(requestId); | |
| const defect = Cause.squash(Cause.map(cause, e => e.issue.toString())); | |
| return Effect.andThen(sendRequestDefect(client, requestId, encodeDefect, defect), server.write(client.id, { | |
| _tag: "Interrupt", | |
| requestId, | |
| interruptors: [] | |
| })); | |
| })); | |
| const sendRequestDefect = (client, requestId, encodeDefect, defect) => Effect.catchCause(Effect.flatMap(encodeDefect(defect), encodedDefect => send(client.id, { | |
| _tag: "Exit", | |
| requestId: String(requestId), | |
| exit: { | |
| _tag: "Failure", | |
| cause: [{ | |
| _tag: "Die", | |
| defect: encodedDefect | |
| }] | |
| } | |
| })), cause => sendDefect(client, Cause.squash(cause))); | |
| const sendDefect = (client, defect) => Effect.catchCause(send(client.id, ResponseDefectEncoded(defect)), cause => Effect.annotateLogs(Effect.logDebug(cause), { | |
| module: "RpcServer", | |
| method: "sendDefect" | |
| })); | |
| // main server loop | |
| return yield* run((clientId, request) => { | |
| let client = clients.get(clientId); | |
| if (!client) { | |
| client = { | |
| id: clientId, | |
| schemas: new Map() | |
| }; | |
| clients.set(clientId, client); | |
| } | |
| switch (request._tag) { | |
| case "Request": | |
| { | |
| const tag = Predicate.hasProperty(request, "tag") ? request.tag : ""; | |
| const rpc = group.requests.get(tag); | |
| if (!rpc) { | |
| return sendDefect(client, `Unknown request tag: ${tag}`); | |
| } | |
| let requestId; | |
| switch (typeof request.id) { | |
| case "bigint": | |
| case "string": | |
| { | |
| requestId = RequestId(request.id); | |
| break; | |
| } | |
| default: | |
| { | |
| return sendDefect(client, `Invalid request id: ${request.id}`); | |
| } | |
| } | |
| const schemas = getSchemas(rpc); | |
| return Effect.matchEffect(Effect.provideContext(schemas.decode(request.payload), schemas.context), { | |
| onFailure: error => sendRequestDefect(client, requestId, schemas.encodeDefect, error.issue.toString()), | |
| onSuccess: payload => { | |
| client.schemas.set(requestId, supportsTransferables ? { | |
| ...schemas, | |
| collector: Transferable.makeCollectorUnsafe() | |
| } : schemas); | |
| return server.write(clientId, { | |
| ...request, | |
| id: requestId, | |
| payload, | |
| headers: Headers.fromInput(request.headers) | |
| }); | |
| } | |
| }); | |
| } | |
| case "Ping": | |
| { | |
| return Effect.catchCause(send(client.id, constPong), cause => sendDefect(client, Cause.squash(cause))); | |
| } | |
| case "Eof": | |
| { | |
| return server.write(clientId, request); | |
| } | |
| case "Ack": | |
| { | |
| return server.write(clientId, { | |
| ...request, | |
| requestId: RequestId(request.requestId) | |
| }); | |
| } | |
| case "Interrupt": | |
| { | |
| return server.write(clientId, { | |
| ...request, | |
| requestId: RequestId(request.requestId), | |
| interruptors: [] | |
| }); | |
| } | |
| default: | |
| { | |
| return sendDefect(client, `Unknown request tag: ${request._tag}`); | |
| } | |
| } | |
| }).pipe(Effect.tapCause(cause => Effect.logFatal("BUG: RpcServer protocol crashed", cause)), Effect.onExit(exit => Scope.close(scope, exit))); | |
| }); | |
| /** | |
| * Provides a scoped layer that starts an RPC server for a group using the | |
| * current server `Protocol`. | |
| * | |
| * @category server | |
| * @since 4.0.0 | |
| */ | |
| export const layer = (group, options) => Layer.effectDiscard(Effect.forkScoped(make(group, options))); | |
| /** | |
| * Creates a RPC server that registers a HTTP route with a `HttpRouter`. | |
| * | |
| * **Details** | |
| * | |
| * Defaults to using websockets for communication, but can be configured to use | |
| * HTTP. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const layerHttp = options => layer(options.group, options).pipe(Layer.provide(options.protocol === "http" ? layerProtocolHttp(options) : layerProtocolWebsocket(options))); | |
| /** | |
| * Defines the service interface for an RPC server transport, responsible for receiving | |
| * encoded client messages, sending encoded responses, tracking clients, and | |
| * declaring transport capabilities. | |
| * | |
| * **When to use** | |
| * | |
| * Use to provide the transport boundary for RPC servers over HTTP, WebSocket, | |
| * workers, sockets, or custom protocols. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export class Protocol extends /*#__PURE__*/Context.Service()("effect/rpc/RpcServer/Protocol") { | |
| /** | |
| * Creates a server protocol service from the supplied RPC implementation. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| static make = /*#__PURE__*/withRun(); | |
| } | |
| /** | |
| * Creates a server `Protocol` backed by the current `SocketServer`, accepting | |
| * socket connections and routing decoded RPC messages. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const makeProtocolSocketServer = /*#__PURE__*/Effect.gen(function* () { | |
| const server = yield* SocketServer.SocketServer; | |
| const { | |
| onSocket, | |
| protocol | |
| } = yield* makeSocketProtocol; | |
| yield* Effect.forkScoped(server.run(Effect.fnUntraced(onSocket, Effect.scoped))); | |
| return protocol; | |
| }); | |
| /** | |
| * RPC protocol that uses `SocketServer` for communication. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const layerProtocolSocketServer = /*#__PURE__*/Layer.effect(Protocol)(makeProtocolSocketServer); | |
| /** | |
| * Creates a websocket server `Protocol` together with an HTTP effect that | |
| * upgrades the current request to a websocket and attaches it to the protocol. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const makeProtocolWithHttpEffectWebsocket = /*#__PURE__*/Effect.gen(function* () { | |
| const { | |
| onSocket, | |
| protocol | |
| } = yield* makeSocketProtocol; | |
| const httpEffect = Effect.gen(function* () { | |
| const request = yield* HttpServerRequest.HttpServerRequest; | |
| const socket = yield* Effect.orDie(request.upgrade); | |
| yield* onSocket(socket, Object.entries(request.headers)); | |
| return HttpServerResponse.empty(); | |
| }); | |
| return { | |
| protocol, | |
| httpEffect | |
| }; | |
| }); | |
| /** | |
| * Creates a websocket server `Protocol` and registers its upgrade handler as a | |
| * GET route on the current `HttpRouter`. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const makeProtocolWebsocket = /*#__PURE__*/Effect.fnUntraced(function* (options) { | |
| const { | |
| httpEffect, | |
| protocol | |
| } = yield* makeProtocolWithHttpEffectWebsocket; | |
| const router = yield* HttpRouter.HttpRouter; | |
| yield* router.add("GET", options.path, httpEffect); | |
| return protocol; | |
| }); | |
| /** | |
| * RPC protocol that uses WebSockets for communication. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const layerProtocolWebsocket = options => { | |
| return Layer.effect(Protocol)(makeProtocolWebsocket(options)); | |
| }; | |
| /** | |
| * Creates an HTTP request/response server `Protocol` together with an HTTP | |
| * effect that decodes the current request and streams or returns encoded RPC | |
| * responses. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const makeProtocolWithHttpEffect = /*#__PURE__*/Effect.gen(function* () { | |
| const serialization = yield* RpcSerialization.RpcSerialization; | |
| const includesFraming = serialization.includesFraming; | |
| const isBinary = !serialization.contentType.includes("json"); | |
| const disconnects = yield* Queue.make(); | |
| let writeRequest; | |
| let clientId = 0; | |
| const clients = new Map(); | |
| const clientIds = new Set(); | |
| const encoder = new TextEncoder(); | |
| const httpEffect = Effect.gen(function* () { | |
| const fiber = Fiber.getCurrent(); | |
| const request = Context.getUnsafe(fiber.context, HttpServerRequest.HttpServerRequest); | |
| const scope = Context.getUnsafe(fiber.context, Scope.Scope); | |
| const requestHeaders = Object.entries(request.headers); | |
| const data = yield* Effect.orDie(isBinary ? Effect.map(request.arrayBuffer, buf => new Uint8Array(buf)) : request.text); | |
| const id = clientId++; | |
| const queue = yield* Queue.make(); | |
| const parser = serialization.makeUnsafe(); | |
| const requestIds = []; | |
| const offer = data => typeof data === "string" ? Queue.offer(queue, encoder.encode(data)) : Queue.offer(queue, data); | |
| const client = { | |
| write: !includesFraming ? response => Queue.offer(queue, response) : response => { | |
| try { | |
| const encoded = parser.encode(response); | |
| if (encoded === undefined) return Effect.void; | |
| return offer(encoded); | |
| } catch (cause) { | |
| return offer(parser.encode(ResponseDefectEncoded(cause))); | |
| } | |
| }, | |
| end: Queue.end(queue) | |
| }; | |
| yield* Scope.addFinalizerExit(scope, () => { | |
| clients.delete(id); | |
| clientIds.delete(id); | |
| Queue.offerUnsafe(disconnects, id); | |
| if (queue.state._tag === "Done") return Effect.void; | |
| return Effect.forEach(requestIds, requestId => writeRequest(id, { | |
| _tag: "Interrupt", | |
| requestId: String(requestId) | |
| }), { | |
| discard: true | |
| }); | |
| }); | |
| clients.set(id, client); | |
| clientIds.add(id); | |
| // @effect-diagnostics-next-line tryCatchInEffectGen:off | |
| try { | |
| const decoded = parser.decode(data); | |
| for (let i = 0; i < decoded.length; i++) { | |
| const message = decoded[i]; | |
| if (message._tag === "Request") { | |
| requestIds.push(RequestId(message.id)); | |
| message.headers = requestHeaders.concat(message.headers); | |
| } | |
| yield* writeRequest(id, message); | |
| } | |
| } catch (cause) { | |
| yield* client.write(ResponseDefectEncoded(cause)); | |
| } | |
| yield* writeRequest(id, constEof); | |
| if (!includesFraming) { | |
| const responses = yield* Queue.collect(queue); | |
| return HttpServerResponse.text(parser.encode(responses), { | |
| contentType: serialization.contentType | |
| }); | |
| } | |
| const initialChunk = yield* Queue.takeAll(queue); | |
| if (queue.state._tag === "Done") { | |
| return HttpServerResponse.uint8Array(mergeUint8Arrays(initialChunk), { | |
| contentType: serialization.contentType | |
| }); | |
| } | |
| return HttpServerResponse.stream(Stream.fromArray(initialChunk).pipe(Stream.concat(Stream.fromQueue(queue))), { | |
| contentType: serialization.contentType | |
| }); | |
| }); | |
| const protocol = yield* Protocol.make(writeRequest_ => { | |
| writeRequest = writeRequest_; | |
| return Effect.succeed({ | |
| disconnects, | |
| send(clientId, response) { | |
| const client = clients.get(clientId); | |
| if (!client) return Effect.void; | |
| return client.write(response); | |
| }, | |
| end(clientId) { | |
| const client = clients.get(clientId); | |
| if (!client) return Effect.void; | |
| return client.end; | |
| }, | |
| clientIds: Effect.sync(() => clientIds), | |
| initialMessage: Effect.succeedNone, | |
| supportsAck: false, | |
| supportsTransferables: false, | |
| supportsSpanPropagation: false | |
| }); | |
| }); | |
| return { | |
| protocol, | |
| httpEffect | |
| }; | |
| }); | |
| const mergeUint8Arrays = arrays => { | |
| if (arrays.length === 0) return new Uint8Array(0); | |
| if (arrays.length === 1) return arrays[0]; | |
| const length = arrays.reduce((acc, arr) => acc + arr.length, 0); | |
| const result = new Uint8Array(length); | |
| let offset = 0; | |
| for (const arr of arrays) { | |
| result.set(arr, offset); | |
| offset += arr.length; | |
| } | |
| return result; | |
| }; | |
| /** | |
| * Creates an HTTP server `Protocol` and registers its request handler as a POST | |
| * route on the current `HttpRouter`. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const makeProtocolHttp = /*#__PURE__*/Effect.fnUntraced(function* (options) { | |
| const { | |
| httpEffect, | |
| protocol | |
| } = yield* makeProtocolWithHttpEffect; | |
| const router = yield* HttpRouter.HttpRouter; | |
| yield* router.add("POST", options.path, httpEffect); | |
| return protocol; | |
| }); | |
| /** | |
| * Provides a server `Protocol` that uses HTTP POST requests for RPC | |
| * communication. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const layerProtocolHttp = options => { | |
| return Layer.effect(Protocol)(makeProtocolHttp(options)); | |
| }; | |
| /** | |
| * Starts an RPC server for a group and returns the HTTP request/response effect | |
| * that serves the non-websocket HTTP RPC protocol. | |
| * | |
| * @category http app | |
| * @since 4.0.0 | |
| */ | |
| export const toHttpEffect = /*#__PURE__*/Effect.fnUntraced(function* (group, options) { | |
| const { | |
| httpEffect, | |
| protocol | |
| } = yield* makeProtocolWithHttpEffect; | |
| yield* make(group, options).pipe(Effect.provideService(Protocol, protocol), Effect.forkScoped); | |
| // @effect-diagnostics-next-line returnEffectInGen:off | |
| return httpEffect; | |
| }); | |
| /** | |
| * Starts an RPC server for a group and returns the HTTP effect that upgrades | |
| * requests to the websocket RPC protocol. | |
| * | |
| * @category http app | |
| * @since 4.0.0 | |
| */ | |
| export const toHttpEffectWebsocket = /*#__PURE__*/Effect.fnUntraced(function* (group, options) { | |
| const { | |
| httpEffect, | |
| protocol | |
| } = yield* makeProtocolWithHttpEffectWebsocket; | |
| yield* make(group, options).pipe(Effect.provideService(Protocol, protocol), Effect.forkScoped); | |
| // @effect-diagnostics-next-line returnEffectInGen:off | |
| return httpEffect; | |
| }); | |
| /** | |
| * Creates a server `Protocol` that reads RPC messages from `Stdio.stdin` and | |
| * writes encoded responses to `Stdio.stdout`. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const makeProtocolStdio = /*#__PURE__*/Effect.gen(function* () { | |
| const stdio = yield* Stdio; | |
| const fiber = Fiber.getCurrent(); | |
| const serialization = yield* RpcSerialization.RpcSerialization; | |
| return yield* Protocol.make(Effect.fnUntraced(function* (writeRequest) { | |
| const queue = yield* Queue.make(); | |
| const parser = serialization.makeUnsafe(); | |
| yield* stdio.stdin.pipe(Stream.runForEach(data => { | |
| const decoded = parser.decode(data); | |
| if (decoded.length === 0) return Effect.void; | |
| let i = 0; | |
| return Effect.whileLoop({ | |
| while: () => i < decoded.length, | |
| body: () => writeRequest(0, decoded[i++]), | |
| step: constVoid | |
| }); | |
| }), Effect.sandbox, Effect.tapError(Effect.logError), Effect.retry(Schedule.spaced(500)), Effect.ensuring(Effect.forkDetach(Fiber.interrupt(fiber), { | |
| startImmediately: true | |
| })), Effect.forkScoped); | |
| yield* Stream.fromQueue(queue).pipe(Stream.run(stdio.stdout()), Effect.retry(Schedule.spaced(500)), Effect.forkScoped); | |
| return { | |
| disconnects: yield* Queue.make(), | |
| send(_clientId, response) { | |
| const responseEncoded = parser.encode(response); | |
| if (responseEncoded === undefined) { | |
| return Effect.void; | |
| } | |
| return Queue.offer(queue, responseEncoded); | |
| }, | |
| end(_clientId) { | |
| return Queue.end(queue); | |
| }, | |
| clientIds: Effect.succeed(new Set([0])), | |
| initialMessage: Effect.succeedNone, | |
| supportsAck: true, | |
| supportsTransferables: false, | |
| supportsSpanPropagation: true | |
| }; | |
| })); | |
| }); | |
| /** | |
| * Provides a server `Protocol` that reads RPC messages from `Stdio.stdin` and | |
| * writes encoded responses to `Stdio.stdout`. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const layerProtocolStdio = /*#__PURE__*/Layer.effect(Protocol, makeProtocolStdio); | |
| /** | |
| * Creates a server `Protocol` backed by `WorkerRunnerPlatform`, routing worker | |
| * messages to the RPC server and server responses back to workers. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const makeProtocolWorkerRunner = /*#__PURE__*/Protocol.make(/*#__PURE__*/Effect.fnUntraced(function* (writeRequest) { | |
| const fiber = Fiber.getCurrent(); | |
| const runner = yield* WorkerRunner.WorkerRunnerPlatform; | |
| const backing = yield* runner.start(); | |
| const initialMessage = yield* Deferred.make(); | |
| const clientIds = new Set(); | |
| const disconnects = yield* Queue.make(); | |
| yield* backing.run((clientId, message) => { | |
| clientIds.add(clientId); | |
| if (message._tag === "InitialMessage") { | |
| return Deferred.succeed(initialMessage, message.value); | |
| } | |
| return writeRequest(clientId, message); | |
| }).pipe(Effect.tapCause(Effect.logError), Effect.onExit(() => Effect.sync(() => { | |
| fiber.currentDispatcher.scheduleTask(() => fiber.interruptUnsafe(fiber.id), 0); | |
| })), Effect.forkScoped); | |
| if (backing.disconnects) { | |
| yield* Queue.take(backing.disconnects).pipe(Effect.tap(clientId => { | |
| clientIds.delete(clientId); | |
| return Queue.offer(disconnects, clientId); | |
| }), Effect.forkScoped); | |
| } | |
| return { | |
| disconnects, | |
| send: backing.send, | |
| end(_clientId) { | |
| return Effect.void; | |
| }, | |
| clientIds: Effect.sync(() => clientIds), | |
| initialMessage: Effect.asSome(Deferred.await(initialMessage)), | |
| supportsAck: true, | |
| supportsTransferables: true, | |
| supportsSpanPropagation: true | |
| }; | |
| })); | |
| /** | |
| * Provides a server `Protocol` backed by the current `WorkerRunnerPlatform`. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const layerProtocolWorkerRunner = /*#__PURE__*/Layer.effect(Protocol)(makeProtocolWorkerRunner); | |
| // internal | |
| const makeSocketProtocol = /*#__PURE__*/Effect.gen(function* () { | |
| const serialization = yield* RpcSerialization.RpcSerialization; | |
| const disconnects = yield* Queue.make(); | |
| let clientId = 0; | |
| const clients = new Map(); | |
| const clientIds = new Set(); | |
| let writeRequest; | |
| const onSocket = function* (socket, headers) { | |
| const scope = yield* Effect.scope; | |
| const parser = serialization.makeUnsafe(); | |
| const id = clientId++; | |
| yield* Scope.addFinalizerExit(scope, () => { | |
| clients.delete(id); | |
| clientIds.delete(id); | |
| return Queue.offer(disconnects, id); | |
| }); | |
| const writeRaw = yield* socket.writer; | |
| const write = response => { | |
| try { | |
| const encoded = parser.encode(response); | |
| if (encoded === undefined) { | |
| return Effect.void; | |
| } | |
| return Effect.orDie(writeRaw(encoded)); | |
| } catch (cause) { | |
| return Effect.orDie(writeRaw(parser.encode(ResponseDefectEncoded(cause)))); | |
| } | |
| }; | |
| clients.set(id, { | |
| write | |
| }); | |
| clientIds.add(id); | |
| yield* socket.runRaw(data => { | |
| try { | |
| const decoded = parser.decode(data); | |
| if (decoded.length === 0) return Effect.void; | |
| let i = 0; | |
| return Effect.whileLoop({ | |
| while: () => i < decoded.length, | |
| body() { | |
| const message = decoded[i++]; | |
| if (message._tag === "Request" && headers) { | |
| ; | |
| message.headers = headers.concat(message.headers); | |
| } | |
| return writeRequest(id, message); | |
| }, | |
| step: constVoid | |
| }); | |
| } catch (cause) { | |
| return writeRaw(parser.encode(ResponseDefectEncoded(cause))); | |
| } | |
| }).pipe(Effect.catchReason("SocketError", "SocketCloseError", _ => Effect.void), Effect.orDie); | |
| }; | |
| const protocol = yield* Protocol.make(writeRequest_ => { | |
| writeRequest = writeRequest_; | |
| return Effect.succeed({ | |
| disconnects, | |
| send: (clientId, response) => { | |
| const client = clients.get(clientId); | |
| if (!client) return Effect.void; | |
| return Effect.orDie(client.write(response)); | |
| }, | |
| end(_clientId) { | |
| return Effect.void; | |
| }, | |
| clientIds: Effect.sync(() => clientIds), | |
| initialMessage: Effect.succeedNone, | |
| supportsAck: true, | |
| supportsTransferables: false, | |
| supportsSpanPropagation: true | |
| }); | |
| }); | |
| return { | |
| protocol, | |
| onSocket | |
| }; | |
| }); | |
| //# sourceMappingURL=RpcServer.js.map |
Xet Storage Details
- Size:
- 34.6 kB
- Xet hash:
- 18466952fa92b3796b15ffa9abfed914cb62840f9cd82f2ab48ea559c168f62a
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.