/** * @since 1.0.0 */ import * as Rpc from "@effect/rpc/Rpc" import type * as RpcSchema from "@effect/rpc/RpcSchema" import type { NonEmptyReadonlyArray } from "effect/Array" import * as Context from "effect/Context" import * as Data from "effect/Data" import * as Effect from "effect/Effect" import * as Exit from "effect/Exit" import * as FiberId from "effect/FiberId" import * as FiberRef from "effect/FiberRef" import { identity } from "effect/Function" import type * as Option from "effect/Option" import { hasProperty } from "effect/Predicate" import * as Schema from "effect/Schema" import { MalformedMessage } from "./ClusterError.js" import type { OutgoingRequest } from "./Message.js" import { Snowflake, SnowflakeFromString } from "./Snowflake.js" /** * @since 1.0.0 * @category type ids */ export const TypeId: unique symbol = Symbol.for("@effect/cluster/Reply") /** * @since 1.0.0 * @category type ids */ export type TypeId = typeof TypeId /** * @since 1.0.0 * @category guards */ export const isReply = (u: unknown): u is Reply => hasProperty(u, TypeId) /** * @since 1.0.0 * @category models */ export type Reply = WithExit | Chunk /** * @since 1.0.0 * @category models */ export class ReplyWithContext extends Data.TaggedClass("ReplyWithContext")<{ readonly reply: Reply readonly context: Context.Context> readonly rpc: R }> { /** * @since 1.0.0 */ static fromDefect(options: { readonly id: Snowflake readonly requestId: Snowflake readonly defect: unknown }): ReplyWithContext { return new ReplyWithContext({ reply: new WithExit({ requestId: options.requestId, id: options.id, exit: Exit.die(Schema.encodeSync(Schema.Defect)(options.defect)) }), context: Context.empty() as any, rpc: neverRpc }) } /** * @since 1.0.0 */ static interrupt(options: { readonly id: Snowflake readonly requestId: Snowflake }): ReplyWithContext { return new ReplyWithContext({ reply: new WithExit({ requestId: options.requestId, id: options.id, exit: Exit.interrupt(FiberId.none) }), context: Context.empty() as any, rpc: neverRpc }) } } const neverRpc = Rpc.make("Never", { success: Schema.Never as any, error: Schema.Never, payload: {} }) /** * @since 1.0.0 * @category models */ export type ReplyEncoded = WithExitEncoded | ChunkEncoded /** * @since 1.0.0 * @category models */ export interface WithExitEncoded { readonly _tag: "WithExit" readonly requestId: string readonly id: string readonly exit: Rpc.ExitEncoded } /** * @since 1.0.0 * @category models */ export interface ChunkEncoded { readonly _tag: "Chunk" readonly requestId: string readonly id: string readonly sequence: number readonly values: NonEmptyReadonlyArray> } const schemaCache = new WeakMap, ReplyEncoded, Rpc.Context>>() /** * @since 1.0.0 * @category schemas */ export const Reply = (rpc: R): Schema.Schema< Reply, ReplyEncoded, Rpc.Context > => { if (schemaCache.has(rpc)) { return schemaCache.get(rpc) as any } const schema = Schema.Union(WithExit.schema(rpc), Chunk.schema(rpc)) schemaCache.set(rpc, schema) return schema } /** * @since 1.0.0 * @category schemas */ export const Encoded = Schema.Union( Schema.Struct({ _tag: Schema.Literal("WithExit"), requestId: Schema.String, id: Schema.String, exit: Schema.Unknown }), Schema.Struct({ _tag: Schema.Literal("Chunk"), requestId: Schema.String, id: Schema.String, sequence: Schema.Number, values: Schema.Array(Schema.Unknown) }) ) /** * @since 1.0.0 * @category models */ export class Chunk extends Data.TaggedClass("Chunk")<{ readonly requestId: Snowflake readonly id: Snowflake readonly sequence: number readonly values: NonEmptyReadonlyArray> }> { /** * @since 1.0.0 */ readonly [TypeId] = TypeId /** * @since 1.0.0 */ static emptyFrom(requestId: Snowflake) { return new Chunk({ requestId, id: Snowflake(BigInt(0)), sequence: 0, values: [undefined] }) } /** * @since 1.0.0 */ static readonly schemaFromSelf: Schema.Schema> = Schema.declare( (u): u is Chunk => isReply(u) && u._tag === "Chunk", { typeConstructor: { _tag: "effect/cluster/Reply.Chunk" } } ) /** * @since 1.0.0 */ static schema(rpc: R): Schema.Schema< Chunk, ChunkEncoded, Rpc.Context > { const successSchema = ((rpc as any as Rpc.AnyWithProps).successSchema as RpcSchema.Stream).success if (!successSchema) { return Schema.Never as any } return Schema.transform( Schema.Struct({ _tag: Schema.Literal("Chunk"), requestId: SnowflakeFromString, id: SnowflakeFromString, sequence: Schema.Number, values: Schema.NonEmptyArray(successSchema) }), Chunk.schemaFromSelf, { decode: (encoded) => new Chunk(encoded as any), encode: identity } ) as any } /** * @since 1.0.0 */ withRequestId(requestId: Snowflake): Chunk { return new Chunk({ ...this, requestId }) } } /** * @since 1.0.0 * @category models */ export class WithExit extends Data.TaggedClass("WithExit")<{ readonly requestId: Snowflake readonly id: Snowflake readonly exit: Rpc.Exit }> { /** * @since 1.0.0 */ readonly [TypeId] = TypeId /** * @since 1.0.0 */ static schema(rpc: R): Schema.Schema< WithExit, WithExitEncoded, Rpc.Context > { return Schema.transform( Schema.Struct({ _tag: Schema.Literal("WithExit"), requestId: SnowflakeFromString, id: SnowflakeFromString, exit: Rpc.exitSchema(rpc) }), Schema.declare((u): u is WithExit => isReply(u) && u._tag === "WithExit"), { decode: (encoded) => new WithExit(encoded), encode: identity } ) as any } /** * @since 1.0.0 */ withRequestId(requestId: Snowflake): WithExit { return new WithExit({ ...this, requestId }) } } /** * @since 1.0.0 * @category serialization / deserialization */ export const serialize = ( self: ReplyWithContext ): Effect.Effect, MalformedMessage> => { const schema = Reply(self.rpc) return MalformedMessage.refail( Effect.locally(Schema.encode(schema)(self.reply), FiberRef.currentContext, self.context) ) } /** * @since 1.0.0 * @category serialization / deserialization */ export const serializeLastReceived = ( self: OutgoingRequest ): Effect.Effect>, MalformedMessage> => { if (self.lastReceivedReply._tag === "None") { return Effect.succeedNone } const schema = Reply(self.rpc) return Effect.asSome(MalformedMessage.refail( Effect.locally(Schema.encode(schema)(self.lastReceivedReply.value), FiberRef.currentContext, self.context) )) }