| import * as Cause from "../../Cause.js"; | |
| import * as Context from "../../Context.js"; | |
| import * as Effect from "../../Effect.js"; | |
| import * as Exit from "../../Exit.js"; | |
| import * as Fiber from "../../Fiber.js"; | |
| import { constVoid, dual, flow, identity } from "../../Function.js"; | |
| import * as Latch from "../../Latch.js"; | |
| import * as Layer from "../../Layer.js"; | |
| import * as Option from "../../Option.js"; | |
| import * as Pool from "../../Pool.js"; | |
| import * as Queue from "../../Queue.js"; | |
| import * as Result from "../../Result.js"; | |
| import * as Schedule from "../../Schedule.js"; | |
| import * as Schema from "../../Schema.js"; | |
| import * as Scope from "../../Scope.js"; | |
| import * as Stream from "../../Stream.js"; | |
| import * as Headers from "../http/Headers.js"; | |
| import * as HttpBody from "../http/HttpBody.js"; | |
| import * as HttpClient from "../http/HttpClient.js"; | |
| import { HttpClientErrorSchema } from "../http/HttpClientError.js"; | |
| import * as HttpClientRequest from "../http/HttpClientRequest.js"; | |
| import * as Socket from "../socket/Socket.js"; | |
| import * as Transferable from "../workers/Transferable.js"; | |
| import * as Worker from "../workers/Worker.js"; | |
| import * as Rpc from "./Rpc.js"; | |
| import { RpcClientDefect, RpcClientError } from "./RpcClientError.js"; | |
| import { constPing, RequestId } from "./RpcMessage.js"; | |
| import * as RpcSchema from "./RpcSchema.js"; | |
| import * as RpcSerialization from "./RpcSerialization.js"; | |
| import * as RpcWorker from "./RpcWorker.js"; | |
| import { withRunClient } from "./Utils.js"; | |
| let requestIdCounter = /*#__PURE__*/BigInt(0); | |
| /** | |
| * Creates an RPC client for an already-decoded message channel, returning the | |
| * client API together with a `write` function for delivering server messages | |
| * back to the client. | |
| * | |
| * @category client | |
| * @since 4.0.0 | |
| */ | |
| export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (group, options) { | |
| const spanPrefix = options?.spanPrefix ?? "RpcClient"; | |
| const supportsAck = options?.supportsAck ?? true; | |
| const disableTracing = options?.disableTracing ?? false; | |
| const generateRequestId = options?.generateRequestId ?? (() => requestIdCounter++); | |
| const services = yield* Effect.context(); | |
| const scope = Context.get(services, Scope.Scope); | |
| const entries = new Map(); | |
| let isShutdown = false; | |
| yield* Scope.addFinalizer(scope, Effect.withFiber(parent => { | |
| isShutdown = true; | |
| return clearEntries(Exit.interrupt(parent.id)); | |
| })); | |
| const clearEntries = Effect.fnUntraced(function* (exit) { | |
| for (const [id, entry] of entries) { | |
| entries.delete(id); | |
| if (entry._tag === "Queue") { | |
| yield* exit._tag === "Success" ? Queue.end(entry.queue) : Queue.failCause(entry.queue, exit.cause); | |
| } else { | |
| entry.resume(exit); | |
| } | |
| } | |
| }); | |
| const onRequest = rpc => { | |
| const isStream = RpcSchema.isStreamSchema(rpc.successSchema); | |
| const middleware = getRpcClientMiddleware(rpc); | |
| return (payload, opts) => { | |
| const headers = opts?.headers ? Headers.fromInput(opts.headers) : Headers.empty; | |
| const context = opts?.context ?? Context.empty(); | |
| if (!isStream) { | |
| const onRequest = span => onEffectRequest(rpc, middleware, span, rpc.payloadSchema.make(payload), headers, context, opts?.discard ?? false); | |
| return disableTracing ? onRequest(undefined) : Effect.useSpan(`${spanPrefix}.${rpc._tag}`, { | |
| attributes: options.spanAttributes | |
| }, onRequest); | |
| } | |
| const queue = onStreamRequest(rpc, middleware, rpc.payloadSchema.make(payload), headers, opts?.streamBufferSize ?? 16, context); | |
| if (opts?.asQueue) return queue; | |
| return Stream.unwrap(Effect.map(queue, Stream.fromQueue)); | |
| }; | |
| }; | |
| const onEffectRequest = (rpc, middleware, span, payload, headers, context, discard) => Effect.withFiber(parentFiber => { | |
| if (isShutdown) { | |
| return Effect.interrupt; | |
| } | |
| const id = generateRequestId(); | |
| const send = middleware(message => options.onFromClient({ | |
| message, | |
| context, | |
| discard | |
| }), { | |
| _tag: "Request", | |
| id, | |
| tag: rpc._tag, | |
| payload, | |
| ...(span ? { | |
| traceId: span.traceId, | |
| spanId: span.spanId, | |
| sampled: span.sampled | |
| } : {}), | |
| headers: Headers.merge(parentFiber.getRef(CurrentHeaders), headers) | |
| }); | |
| if (discard) { | |
| return send; | |
| } | |
| let fiber; | |
| return Effect.onInterrupt(Effect.callback(resume => { | |
| const entry = { | |
| _tag: "Effect", | |
| rpc, | |
| context, | |
| resume(exit) { | |
| resume(exit); | |
| if (fiber && !fiber.pollUnsafe()) { | |
| parentFiber.currentDispatcher.scheduleTask(() => { | |
| fiber.interruptUnsafe(parentFiber.id); | |
| }, 0); | |
| } | |
| } | |
| }; | |
| entries.set(id, entry); | |
| fiber = send.pipe(span ? Effect.withParentSpan(span, { | |
| captureStackTrace: false | |
| }) : identity, Effect.runForkWith(parentFiber.context)); | |
| fiber.addObserver(exit => { | |
| if (exit._tag === "Failure") { | |
| return resume(exit); | |
| } | |
| }); | |
| }), interruptors => { | |
| entries.delete(id); | |
| return Effect.andThen(Fiber.interrupt(fiber), sendInterrupt(id, Array.from(interruptors), context)); | |
| }); | |
| }); | |
| const onStreamRequest = Effect.fnUntraced(function* (rpc, middleware, payload, headers, streamBufferSize, context) { | |
| if (isShutdown) { | |
| return yield* Effect.interrupt; | |
| } | |
| const span = disableTracing ? undefined : yield* Effect.makeSpanScoped(`${spanPrefix}.${rpc._tag}`, { | |
| attributes: options.spanAttributes | |
| }); | |
| const fiber = Fiber.getCurrent(); | |
| const id = generateRequestId(); | |
| const scope = Context.getUnsafe(fiber.context, Scope.Scope); | |
| yield* Scope.addFinalizerExit(scope, exit => { | |
| if (!entries.has(id)) return Effect.void; | |
| entries.delete(id); | |
| return sendInterrupt(id, Exit.isFailure(exit) ? Array.from(Cause.interruptors(exit.cause)) : [], context); | |
| }); | |
| const queue = yield* Queue.bounded(streamBufferSize); | |
| entries.set(id, { | |
| _tag: "Queue", | |
| rpc, | |
| queue, | |
| scope, | |
| context | |
| }); | |
| yield* middleware(message => options.onFromClient({ | |
| message, | |
| context, | |
| discard: false | |
| }), { | |
| _tag: "Request", | |
| id, | |
| tag: rpc._tag, | |
| payload, | |
| ...(span ? { | |
| traceId: span.traceId, | |
| spanId: span.spanId, | |
| sampled: span.sampled | |
| } : {}), | |
| headers: Headers.merge(fiber.getRef(CurrentHeaders), headers) | |
| }).pipe(span ? Effect.withParentSpan(span, { | |
| captureStackTrace: false | |
| }) : identity, Effect.catchCause(error => Queue.failCause(queue, error)), Effect.interruptible, Effect.forkIn(scope, { | |
| startImmediately: true | |
| })); | |
| return queue; | |
| }); | |
| const getRpcClientMiddleware = rpc => { | |
| const middlewares = []; | |
| for (const tag of rpc.middlewares.values()) { | |
| const middleware = services.mapUnsafe.get(`${tag.key}/Client`); | |
| if (!middleware) continue; | |
| middlewares.push(middleware); | |
| } | |
| if (middlewares.length === 0) { | |
| return (send, request) => send(request); | |
| } | |
| return function loop(send, request, index = middlewares.length - 1) { | |
| if (index === -1) { | |
| return send(request); | |
| } | |
| return middlewares[index]({ | |
| rpc, | |
| request, | |
| next(request) { | |
| return loop(send, request, index - 1); | |
| } | |
| }); | |
| }; | |
| }; | |
| const sendInterrupt = (requestId, interruptors, context) => Effect.callback(resume => { | |
| const parentFiber = Fiber.getCurrent(); | |
| const fiber = options.onFromClient({ | |
| message: { | |
| _tag: "Interrupt", | |
| requestId, | |
| interruptors | |
| }, | |
| context, | |
| discard: false | |
| }).pipe(Effect.timeout(1000), Effect.runForkWith(parentFiber.context)); | |
| fiber.addObserver(() => { | |
| resume(Effect.void); | |
| }); | |
| }); | |
| const write = message => { | |
| switch (message._tag) { | |
| case "Chunk": | |
| { | |
| const requestId = message.requestId; | |
| const entry = entries.get(requestId); | |
| if (!entry || entry._tag !== "Queue") return Effect.void; | |
| return Queue.offerAll(entry.queue, message.values).pipe(supportsAck ? Effect.flatMap(() => options.onFromClient({ | |
| message: { | |
| _tag: "Ack", | |
| requestId: message.requestId | |
| }, | |
| context: entry.context, | |
| discard: false | |
| })) : identity, Effect.catchCause(cause => Queue.failCause(entry.queue, cause))); | |
| } | |
| case "Exit": | |
| { | |
| const requestId = message.requestId; | |
| const entry = entries.get(requestId); | |
| if (!entry) return Effect.void; | |
| entries.delete(requestId); | |
| if (entry._tag === "Effect") { | |
| entry.resume(message.exit); | |
| return Effect.void; | |
| } | |
| return message.exit._tag === "Success" ? Queue.end(entry.queue) : Queue.failCause(entry.queue, message.exit.cause); | |
| } | |
| case "Defect": | |
| { | |
| return clearEntries(Exit.die(message.defect)); | |
| } | |
| case "ClientEnd": | |
| { | |
| return Effect.void; | |
| } | |
| } | |
| }; | |
| let client; | |
| if (options.flatten) { | |
| const fns = new Map(); | |
| client = function client(tag, payload, options) { | |
| let fn = fns.get(tag); | |
| if (!fn) { | |
| fn = onRequest(group.requests.get(tag)); | |
| fns.set(tag, fn); | |
| } | |
| return fn(payload, options); | |
| }; | |
| } else { | |
| client = {}; | |
| group.requests.forEach(rpc => { | |
| client[rpc._tag] = onRequest(rpc); | |
| }); | |
| } | |
| return { | |
| client, | |
| write | |
| }; | |
| }); | |
| let clientIdCounter = 0; | |
| /** | |
| * Creates a schema-aware RPC client for a group using the current client | |
| * `Protocol`, encoding requests and decoding server responses. | |
| * | |
| * @category client | |
| * @since 4.0.0 | |
| */ | |
| export const make = /*#__PURE__*/Effect.fnUntraced(function* (group, options) { | |
| const clientId = clientIdCounter++; | |
| const { | |
| run, | |
| send, | |
| supportsAck, | |
| supportsTransferables | |
| } = yield* Protocol; | |
| const entries = new Map(); | |
| const { | |
| client, | |
| write | |
| } = yield* makeNoSerialization(group, { | |
| ...options, | |
| supportsAck, | |
| onFromClient({ | |
| message | |
| }) { | |
| switch (message._tag) { | |
| case "Request": | |
| { | |
| const rpc = group.requests.get(message.tag); | |
| const collector = supportsTransferables ? Transferable.makeCollectorUnsafe() : undefined; | |
| const fiber = Fiber.getCurrent(); | |
| const entry = { | |
| rpc, | |
| context: collector ? Context.add(fiber.context, Transferable.Collector, collector) : fiber.context, | |
| schemas: rpcSchemas(rpc) | |
| }; | |
| entries.set(message.id, entry); | |
| return entry.schemas.encodePayload(message.payload).pipe(Effect.provideContext(entry.context), Effect.orDie, Effect.flatMap(payload => send(clientId, { | |
| ...message, | |
| id: String(message.id), | |
| payload, | |
| headers: Object.entries(message.headers) | |
| }, collector && collector.readUnsafe()))); | |
| } | |
| case "Ack": | |
| { | |
| const entry = entries.get(message.requestId); | |
| if (!entry) return Effect.void; | |
| return send(clientId, { | |
| _tag: "Ack", | |
| requestId: String(message.requestId) | |
| }); | |
| } | |
| case "Interrupt": | |
| { | |
| const entry = entries.get(message.requestId); | |
| if (!entry) return Effect.void; | |
| entries.delete(message.requestId); | |
| return send(clientId, { | |
| _tag: "Interrupt", | |
| requestId: String(message.requestId) | |
| }); | |
| } | |
| case "Eof": | |
| { | |
| return Effect.void; | |
| } | |
| } | |
| } | |
| }); | |
| yield* run(clientId, message => { | |
| switch (message._tag) { | |
| case "Chunk": | |
| { | |
| const requestId = RequestId(message.requestId); | |
| const entry = entries.get(requestId); | |
| if (!entry || Option.isNone(entry.schemas.decodeChunk)) return Effect.void; | |
| return entry.schemas.decodeChunk.value(message.values).pipe(Effect.provideContext(entry.context), Effect.orDie, Effect.flatMap(chunk => write({ | |
| _tag: "Chunk", | |
| clientId: 0, | |
| requestId: RequestId(message.requestId), | |
| values: chunk | |
| })), Effect.onError(cause => write({ | |
| _tag: "Exit", | |
| clientId: 0, | |
| requestId: RequestId(message.requestId), | |
| exit: Exit.failCause(cause) | |
| }))); | |
| } | |
| case "Exit": | |
| { | |
| const requestId = RequestId(message.requestId); | |
| const entry = entries.get(requestId); | |
| if (!entry) return Effect.void; | |
| entries.delete(requestId); | |
| return entry.schemas.decodeExit(message.exit).pipe(Effect.provideContext(entry.context), Effect.orDie, Effect.matchCauseEffect({ | |
| onSuccess: exit => write({ | |
| _tag: "Exit", | |
| clientId: 0, | |
| requestId, | |
| exit | |
| }), | |
| onFailure: cause => write({ | |
| _tag: "Exit", | |
| clientId: 0, | |
| requestId, | |
| exit: Exit.failCause(cause) | |
| }) | |
| })); | |
| } | |
| case "Defect": | |
| { | |
| return write({ | |
| _tag: "Defect", | |
| clientId: 0, | |
| defect: decodeDefect(message.defect) | |
| }); | |
| } | |
| case "ClientProtocolError": | |
| { | |
| const exit = Exit.fail(message.error); | |
| return Effect.forEach(entries.keys(), requestId => write({ | |
| _tag: "Exit", | |
| clientId: 0, | |
| requestId, | |
| exit: exit | |
| })); | |
| } | |
| default: | |
| { | |
| return Effect.void; | |
| } | |
| } | |
| }).pipe(Effect.catchCause(Effect.logError), Effect.interruptible, Effect.forkScoped); | |
| return client; | |
| }); | |
| const rpcSchemasCache = /*#__PURE__*/new WeakMap(); | |
| const rpcSchemas = rpc => { | |
| let entry = rpcSchemasCache.get(rpc); | |
| if (entry !== undefined) { | |
| return entry; | |
| } | |
| const streamSchemas = RpcSchema.getStreamSchemas(rpc.successSchema); | |
| entry = { | |
| decodeChunk: Option.map(streamSchemas, streamSchemas => Schema.decodeUnknownEffect(Schema.toCodecJson(Schema.NonEmptyArray(streamSchemas.success)))), | |
| encodePayload: Schema.encodeEffect(Schema.toCodecJson(rpc.payloadSchema)), | |
| decodeExit: Schema.decodeUnknownEffect(Schema.toCodecJson(Rpc.exitSchema(rpc))) | |
| }; | |
| rpcSchemasCache.set(rpc, entry); | |
| return entry; | |
| }; | |
| /** | |
| * Fiber reference containing headers that are merged into outgoing RPC | |
| * client requests. | |
| * | |
| * **When to use** | |
| * | |
| * Use to set request headers that should be automatically merged into outgoing | |
| * RPC client messages. | |
| * | |
| * @category headers | |
| * @since 4.0.0 | |
| */ | |
| export const CurrentHeaders = /*#__PURE__*/Context.Reference("effect/rpc/RpcClient/CurrentHeaders", { | |
| defaultValue: () => Headers.empty | |
| }); | |
| /** | |
| * Runs an effect with additional RPC client headers, merging them with the | |
| * current `CurrentHeaders` value for outgoing requests. | |
| * | |
| * @category headers | |
| * @since 4.0.0 | |
| */ | |
| export const withHeaders = /*#__PURE__*/dual(2, (effect, headers) => Effect.updateService(effect, CurrentHeaders, Headers.merge(Headers.fromInput(headers)))); | |
| /** | |
| * Defines the service interface for an RPC client transport, responsible for running the | |
| * receive loop and sending encoded client messages. | |
| * | |
| * **When to use** | |
| * | |
| * Use to provide the transport boundary for RPC clients over HTTP, WebSocket, | |
| * workers, sockets, or custom protocols. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export class Protocol extends /*#__PURE__*/Context.Service()("effect/rpc/RpcClient/Protocol") { | |
| /** | |
| * Creates a client protocol service from the supplied RPC request runner. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| static make = withRunClient; | |
| } | |
| /** | |
| * Creates a client `Protocol` that sends each RPC request through the supplied | |
| * `HttpClient` and decodes responses with the current `RpcSerialization`. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const makeProtocolHttp = client => Protocol.make(Effect.fnUntraced(function* (writeResponse) { | |
| const serialization = yield* RpcSerialization.RpcSerialization; | |
| const isFramed = serialization.includesFraming; | |
| const httpClientError = cause => new RpcClientError({ | |
| reason: HttpClientErrorSchema.fromHttpClientError(cause) | |
| }); | |
| const protocolDefect = (message, cause) => new RpcClientError({ | |
| reason: new RpcClientDefect({ | |
| message, | |
| cause | |
| }) | |
| }); | |
| const emptyResponseError = request => protocolDefect("Received empty HTTP response from RPC server", request); | |
| const send = Effect.fnUntraced(function* (clientId, request) { | |
| if (request._tag !== "Request") { | |
| return; | |
| } | |
| const parser = serialization.makeUnsafe(); | |
| const encoded = parser.encode(request); | |
| const body = typeof encoded === "string" ? HttpBody.text(encoded, serialization.contentType) : HttpBody.uint8Array(encoded, serialization.contentType); | |
| const response = yield* client.post("", { | |
| body | |
| }).pipe(Effect.mapError(httpClientError)); | |
| if (!isFramed) { | |
| const text = yield* response.text.pipe(Effect.mapError(httpClientError)); | |
| const responses = yield* Effect.try({ | |
| try: () => parser.decode(text), | |
| catch: cause => protocolDefect("Error decoding HTTP response", cause) | |
| }); | |
| if (!Array.isArray(responses)) { | |
| return yield* protocolDefect("Expected an array of responses", responses); | |
| } | |
| if (responses.length === 0) { | |
| return yield* emptyResponseError(request); | |
| } | |
| let i = 0; | |
| return yield* Effect.whileLoop({ | |
| while: () => i < responses.length, | |
| body: () => writeResponse(clientId, responses[i++]), | |
| step: constVoid | |
| }); | |
| } | |
| let hasResponse = false; | |
| yield* Stream.runForEachArray(response.stream, chunk => Effect.try({ | |
| try: () => chunk.flatMap(parser.decode), | |
| catch: cause => protocolDefect("Error decoding HTTP response", cause) | |
| }).pipe(Effect.flatMap(responses => { | |
| if (responses.length === 0) return Effect.void; | |
| hasResponse = true; | |
| let i = 0; | |
| return Effect.whileLoop({ | |
| while: () => i < responses.length, | |
| body: () => writeResponse(clientId, responses[i++]), | |
| step: constVoid | |
| }); | |
| }))).pipe(Effect.mapError(cause => cause instanceof RpcClientError ? cause : httpClientError(cause))); | |
| if (!hasResponse) { | |
| return yield* emptyResponseError(request); | |
| } | |
| }); | |
| return { | |
| send, | |
| supportsAck: false, | |
| supportsTransferables: false | |
| }; | |
| })); | |
| /** | |
| * Provides a client `Protocol` backed by `HttpClient`, targeting the configured | |
| * URL and optionally transforming the client before use. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const layerProtocolHttp = options => Layer.effect(Protocol)(Effect.flatMap(HttpClient.HttpClient, client => { | |
| client = HttpClient.mapRequest(client, HttpClientRequest.prependUrl(options.url)); | |
| return makeProtocolHttp(options.transformClient ? options.transformClient(client) : client); | |
| })); | |
| /** | |
| * Creates a client `Protocol` over the current `Socket`, using the current | |
| * `RpcSerialization`, connection hooks, ping timeouts, and the configured retry | |
| * policy. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(function* (writeResponse, clientIds) { | |
| const socket = yield* Socket.Socket; | |
| const serialization = yield* RpcSerialization.RpcSerialization; | |
| const hooks = yield* Effect.serviceOption(ConnectionHooks); | |
| const requestClientMap = new Map(); | |
| const write = yield* socket.writer; | |
| let parser = serialization.makeUnsafe(); | |
| const pinger = yield* makePinger(write(parser.encode(constPing))); | |
| let currentError; | |
| const onOpen = Effect.suspend(() => { | |
| currentError = undefined; | |
| return Option.isSome(hooks) ? hooks.value.onConnect : Effect.void; | |
| }); | |
| const broadcast = response => Effect.forEach(clientIds, clientId => writeResponse(clientId, response)); | |
| yield* Effect.suspend(() => { | |
| parser = serialization.makeUnsafe(); | |
| pinger.reset(); | |
| return socket.runRaw(message => { | |
| try { | |
| const responses = parser.decode(message); | |
| if (responses.length === 0) return; | |
| let i = 0; | |
| return Effect.whileLoop({ | |
| while: () => i < responses.length, | |
| body: () => { | |
| const response = responses[i++]; | |
| if (response._tag === "Pong") { | |
| pinger.onPong(); | |
| return Effect.void; | |
| } | |
| if ("requestId" in response) { | |
| const clientId = requestClientMap.get(response.requestId); | |
| if (clientId !== undefined) { | |
| if (response._tag === "Exit") { | |
| requestClientMap.delete(response.requestId); | |
| } | |
| return writeResponse(clientId, response); | |
| } | |
| } | |
| return broadcast(response); | |
| }, | |
| step: constVoid | |
| }); | |
| } catch (defect) { | |
| return broadcast({ | |
| _tag: "ClientProtocolError", | |
| error: new RpcClientError({ | |
| reason: new RpcClientDefect({ | |
| message: "Error decoding message", | |
| cause: defect | |
| }) | |
| }) | |
| }); | |
| } | |
| }, { | |
| onOpen | |
| }).pipe(Effect.raceFirst(Effect.flatMap(pinger.timeout, () => Effect.fail(new Socket.SocketError({ | |
| reason: new Socket.SocketOpenError({ | |
| kind: "Timeout", | |
| cause: new Error("ping timeout") | |
| }) | |
| }))))); | |
| }).pipe(Effect.flatMap(() => Effect.fail(new Socket.SocketError({ | |
| reason: new Socket.SocketCloseError({ | |
| code: 1000 | |
| }) | |
| }))), Option.isSome(hooks) ? Effect.ensuring(hooks.value.onDisconnect) : identity, Effect.tapCause(cause => { | |
| const error = Cause.findError(cause); | |
| const hasError = Result.isSuccess(error); | |
| if (options?.retryTransientErrors && hasError && error.success.reason._tag === "SocketOpenError") { | |
| return Effect.void; | |
| } | |
| currentError = new RpcClientError({ | |
| reason: hasError ? error.success.reason : new RpcClientDefect({ | |
| message: "Unknown socket error", | |
| cause: Cause.squash(cause) | |
| }) | |
| }); | |
| return broadcast({ | |
| _tag: "ClientProtocolError", | |
| error: currentError | |
| }); | |
| }), Effect.retry(options?.retryPolicy ?? defaultRetryPolicy), Effect.annotateLogs({ | |
| module: "RpcClient", | |
| method: "makeProtocolSocket" | |
| }), Effect.forkScoped); | |
| return { | |
| send(clientId, request) { | |
| if (currentError) { | |
| return Effect.fail(currentError); | |
| } | |
| if (request._tag === "Request") { | |
| requestClientMap.set(request.id, clientId); | |
| } | |
| const encoded = parser.encode(request); | |
| if (encoded === undefined) return Effect.void; | |
| return Effect.orDie(write(encoded)); | |
| }, | |
| supportsAck: true, | |
| supportsTransferables: false | |
| }; | |
| })); | |
| const defaultRetryPolicy = /*#__PURE__*/Schedule.exponential(500, 1.5).pipe(/*#__PURE__*/Schedule.either(/*#__PURE__*/Schedule.spaced(5000))); | |
| const makePinger = /*#__PURE__*/Effect.fnUntraced(function* (writePing) { | |
| let recievedPong = true; | |
| const latch = Latch.makeUnsafe(); | |
| const reset = () => { | |
| recievedPong = true; | |
| latch.closeUnsafe(); | |
| }; | |
| const onPong = () => { | |
| recievedPong = true; | |
| }; | |
| yield* Effect.suspend(() => { | |
| if (!recievedPong) return latch.open; | |
| recievedPong = false; | |
| return writePing; | |
| }).pipe(Effect.delay("5 seconds"), Effect.ignore, Effect.forever, Effect.interruptible, Effect.forkScoped); | |
| return { | |
| timeout: latch.await, | |
| reset, | |
| onPong | |
| }; | |
| }); | |
| /** | |
| * Provides a client `Protocol` backed by the current `Socket` and | |
| * `RpcSerialization` services. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const layerProtocolSocket = options => Layer.effect(Protocol)(makeProtocolSocket(options)); | |
| /** | |
| * Creates a client `Protocol` backed by a pool of workers, routing RPC requests | |
| * to workers and supporting transferable values when the platform does. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const makeProtocolWorker = options => Protocol.make(Effect.fnUntraced(function* (writeResponse, clientIds) { | |
| const worker = yield* Worker.WorkerPlatform; | |
| const scope = yield* Effect.scope; | |
| let workerId = 0; | |
| const initialMessage = yield* Effect.serviceOption(RpcWorker.InitialMessage); | |
| const hooks = yield* Effect.serviceOption(ConnectionHooks); | |
| const entries = new Map(); | |
| const broadcast = response => Effect.forEach(clientIds, clientId => writeResponse(clientId, response)); | |
| const acquire = Effect.gen(function* () { | |
| const id = workerId++; | |
| const backing = yield* worker.spawn(id); | |
| yield* backing.run(response => { | |
| if (response._tag === "Exit") { | |
| const entry = entries.get(response.requestId); | |
| if (entry) { | |
| entries.delete(response.requestId); | |
| entry.latch.openUnsafe(); | |
| return writeResponse(entry.clientId, response); | |
| } | |
| } else if (response._tag === "Defect") { | |
| for (const [requestId, entry] of entries) { | |
| entries.delete(requestId); | |
| entry.latch.openUnsafe(); | |
| } | |
| return broadcast(response); | |
| } else if ("requestId" in response) { | |
| const entry = entries.get(response.requestId); | |
| if (entry) { | |
| return writeResponse(entry.clientId, response); | |
| } | |
| } | |
| return broadcast(response); | |
| }, { | |
| onSpawn: Option.isSome(initialMessage) ? Effect.flatMap(initialMessage.value, ([value, transfers]) => Effect.orDie(backing.send({ | |
| _tag: "InitialMessage", | |
| value | |
| }, transfers))) : undefined | |
| }).pipe(Effect.tapCause(cause => { | |
| const error = Cause.findError(cause); | |
| return broadcast({ | |
| _tag: "ClientProtocolError", | |
| error: new RpcClientError({ | |
| reason: Result.isSuccess(error) ? error.success.reason : new RpcClientDefect({ | |
| message: "Error in worker", | |
| cause: Cause.squash(cause) | |
| }) | |
| }) | |
| }); | |
| }), Effect.retry(Schedule.spaced(1000)), Effect.annotateLogs({ | |
| module: "RpcClient", | |
| method: "makeProtocolWorker" | |
| }), Effect.interruptible, Effect.forkScoped); | |
| return backing; | |
| }); | |
| const pool = "minSize" in options ? yield* Pool.makeWithTTL({ | |
| acquire, | |
| min: options.minSize, | |
| max: options.maxSize, | |
| concurrency: options.concurrency, | |
| targetUtilization: options.targetUtilization, | |
| timeToLive: options.timeToLive | |
| }) : yield* Pool.make({ | |
| acquire, | |
| size: options.size, | |
| concurrency: options.concurrency, | |
| targetUtilization: options.targetUtilization | |
| }); | |
| yield* Scope.addFinalizer(scope, Effect.sync(() => { | |
| for (const entry of entries.values()) { | |
| entry.latch.openUnsafe(); | |
| } | |
| entries.clear(); | |
| })); | |
| const send = (clientId, request, transferables) => { | |
| switch (request._tag) { | |
| case "Request": | |
| { | |
| return Pool.get(pool).pipe(Effect.flatMap(worker => { | |
| const latch = Latch.makeUnsafe(false); | |
| entries.set(request.id, { | |
| clientId, | |
| worker, | |
| latch | |
| }); | |
| return Effect.flatMap(worker.send(request, transferables), () => latch.await); | |
| }), Effect.scoped, Effect.orDie); | |
| } | |
| case "Interrupt": | |
| { | |
| const entry = entries.get(request.requestId); | |
| if (!entry) return Effect.void; | |
| entries.delete(request.requestId); | |
| entry.latch.openUnsafe(); | |
| return Effect.orDie(entry.worker.send(request)); | |
| } | |
| case "Ack": | |
| { | |
| const entry = entries.get(request.requestId); | |
| if (!entry) return Effect.void; | |
| return Effect.orDie(entry.worker.send(request)); | |
| } | |
| } | |
| return Effect.void; | |
| }; | |
| yield* Effect.scoped(Pool.get(pool)); | |
| if (Option.isSome(hooks)) yield* hooks.value.onConnect; | |
| return { | |
| send, | |
| supportsAck: true, | |
| supportsTransferables: true | |
| }; | |
| })); | |
| /** | |
| * Provides a client `Protocol` backed by a worker pool using the current worker | |
| * platform and spawner services. | |
| * | |
| * @category protocols | |
| * @since 4.0.0 | |
| */ | |
| export const layerProtocolWorker = /*#__PURE__*/flow(makeProtocolWorker, /*#__PURE__*/Layer.effect(Protocol)); | |
| /** | |
| * Represents optional client protocol hooks that run when a transport connects | |
| * and disconnects. | |
| * | |
| * **When to use** | |
| * | |
| * Use to run setup or cleanup effects when an RPC client transport opens or | |
| * closes. | |
| * | |
| * @category connection hooks | |
| * @since 4.0.0 | |
| */ | |
| export class ConnectionHooks extends /*#__PURE__*/Context.Service()("effect/rpc/RpcClient/ConnectionHooks") {} | |
| // internal | |
| const decodeDefect = /*#__PURE__*/Schema.decodeSync(/*#__PURE__*/Schema.Defect()); | |
| //# sourceMappingURL=RpcClient.js.map |
Xet Storage Details
- Size:
- 29 kB
- Xet hash:
- 9363a7fc03f1de9b8a304a611aa54fec07c8a451d3aa55ca95edcbacae141688
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.