UNPKG

the-moby-effect

Version:
309 lines (282 loc) 12.6 kB
import type * as HttpClientResponse from "@effect/platform/HttpClientResponse"; import type * as ParseResult from "effect/ParseResult"; import type * as Scope from "effect/Scope"; import type * as MobyDemux from "../../MobyDemux.js"; import * as Socket from "@effect/platform/Socket"; import * as Channel from "effect/Channel"; import * as Chunk from "effect/Chunk"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Function from "effect/Function"; import * as Pipeable from "effect/Pipeable"; import * as Predicate from "effect/Predicate"; import * as Schedule from "effect/Schedule"; import * as Schema from "effect/Schema"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import * as Tuple from "effect/Tuple"; import * as internalCompressed from "./compressed.js"; /** @internal */ export const MultiplexedContentType = "application/vnd.docker.multiplexed-stream" as const; /** @internal */ export const MultiplexedSocketTypeId: MobyDemux.MultiplexedSocketTypeId = Symbol.for( "the-moby-effect/demux/MultiplexedSocket" ) as MobyDemux.MultiplexedSocketTypeId; /** @internal */ export const MultiplexedChannelTypeId: MobyDemux.MultiplexedChannelTypeId = Symbol.for( "the-moby-effect/demux/MultiplexedChannel" ) as MobyDemux.MultiplexedChannelTypeId; /** @internal */ export const makeMultiplexedSocket = (underlying: Socket.Socket): MobyDemux.MultiplexedSocket => ({ underlying, "content-type": MultiplexedContentType, [MultiplexedSocketTypeId]: MultiplexedSocketTypeId, pipe() { return Pipeable.pipeArguments(this, arguments); }, }); /** @internal */ export const makeMultiplexedChannel = <IE = unknown, OE = Socket.SocketError, R = never>( underlying: Channel.Channel< Chunk.Chunk<Uint8Array>, Chunk.Chunk<string | Uint8Array | Socket.CloseEvent>, OE, IE, void, unknown, R > ): MobyDemux.MultiplexedChannel<IE, IE | OE, R> => ({ underlying, "content-type": MultiplexedContentType, [MultiplexedChannelTypeId]: MultiplexedChannelTypeId, pipe() { return Pipeable.pipeArguments(this, arguments); }, }); /** @internal */ export const isMultiplexedSocket = (u: unknown): u is MobyDemux.MultiplexedSocket => Predicate.hasProperty(u, MultiplexedSocketTypeId); /** @internal */ export const isMultiplexedChannel = <IE = unknown, OE = Socket.SocketError, R = never>( u: unknown ): u is MobyDemux.MultiplexedChannel<IE, IE | OE, R> => Predicate.hasProperty(u, MultiplexedChannelTypeId); /** @internal */ export const responseIsMultiplexedResponse = (response: HttpClientResponse.HttpClientResponse): boolean => response.headers["content-type"] === MultiplexedContentType; /** @internal */ export enum MultiplexedHeaderType { Stdin = 0, Stdout = 1, Stderr = 2, } /** @internal */ export type MultiplexedAccumulator = { headerBytesRead: number; messageBytesRead: number; headerBuffer: Chunk.Chunk<number>; messageBuffer: Chunk.Chunk<number>; messageSize: number | undefined; messageType: number | undefined; }; /** @internal */ export interface $MultiplexedSchema extends Schema.Tuple< [Schema.Enums<typeof MultiplexedHeaderType>, Schema.Schema<Uint8Array, ReadonlyArray<number>, never>] > {} /** @internal */ export const MultiplexedSchema: $MultiplexedSchema = Schema.Tuple( Schema.Enums(MultiplexedHeaderType), Schema.Uint8Array ); /** @internal */ export const asMultiplexedChannel = <IE = never, OE = Socket.SocketError, R = never>( input: MobyDemux.EitherMultiplexedInput<IE, OE, R> ): MobyDemux.MultiplexedChannel<IE, OE, R> => isMultiplexedSocket(input) ? (makeMultiplexedChannel(Socket.toChannel<IE>(input.underlying)) as unknown as MobyDemux.MultiplexedChannel< IE, OE, R >) : (input as MobyDemux.MultiplexedChannel<IE, OE, R>); /** @internal */ export const multiplexedToStream = <IE = never, OE = Socket.SocketError, R = never>( input: MobyDemux.EitherMultiplexedInput<IE, OE, R> ): Stream.Stream<Uint8Array, IE | OE, R> => Channel.toStream( asMultiplexedChannel(input).underlying as Channel.Channel< Chunk.Chunk<Uint8Array>, unknown, IE | OE, unknown, void, unknown, R > ); /** @internal */ export const multiplexedToSink = <IE = never, OE = Socket.SocketError, R = never>( input: MobyDemux.EitherMultiplexedInput<IE, OE, R> ): Sink.Sink<void, string | Uint8Array | Socket.CloseEvent, Uint8Array, IE | OE, R> => Channel.toSink(asMultiplexedChannel(input).underlying); /** @internal */ export const multiplexedFromStreamWith = <IE>() => <E, R>(input: Stream.Stream<Uint8Array, IE | E, R>): MobyDemux.MultiplexedChannel<IE, IE | E, R> => makeMultiplexedChannel<IE, IE | E, R>(Stream.toChannel(input)); /** @internal */ export const multiplexedFromStream = <E, R>( input: Stream.Stream<Uint8Array, E, R> ): MobyDemux.MultiplexedChannel<never, E, R> => multiplexedFromStreamWith<never>()(input); /** @internal */ export const multiplexedFromSink = <E, R>( input: Sink.Sink<void, string | Uint8Array | Socket.CloseEvent, Uint8Array, E, R> ): MobyDemux.MultiplexedChannel<never, E, R> => makeMultiplexedChannel<never, E, R>(Sink.toChannel(input)); /** @internal */ export const demuxMultiplexedFolderSink: Sink.Sink<MultiplexedAccumulator, number, number, never, never> = Sink.fold( { headerBytesRead: 0, messageBytesRead: 0, headerBuffer: Chunk.empty<number>(), messageBuffer: Chunk.empty<number>(), messageSize: undefined as number | undefined, messageType: undefined as number | undefined, }, ({ messageBytesRead, messageSize }) => messageSize === undefined || messageBytesRead < messageSize, (accumulator, input: number) => { // If we have not read the entire header yet if (accumulator.headerBytesRead < 8) { return { ...accumulator, headerBytesRead: accumulator.headerBytesRead + 1, headerBuffer: Chunk.append(accumulator.headerBuffer, input), }; } // Lazily evaluate the header and message size const uint8Array = () => new Uint8Array(Chunk.toReadonlyArray(accumulator.headerBuffer)); const type = () => accumulator.messageType ?? new DataView(uint8Array().buffer).getUint8(0); const size = () => accumulator.messageSize ?? new DataView(uint8Array().buffer).getUint32(4); // We can parse the header when we've read all 8 bytes and haven't parsed it yet const needToParseHeader = accumulator.headerBytesRead === 8 && Predicate.isUndefined(accumulator.messageType) && Predicate.isUndefined(accumulator.messageSize); // Otherwise, if we have read the entire header but not parsed it yet const parsedHeader = needToParseHeader ? { messageType: type(), messageSize: size() } : {}; return { ...accumulator, ...parsedHeader, messageBytesRead: accumulator.messageBytesRead + 1, messageBuffer: Chunk.append(accumulator.messageBuffer, input), }; } ); /** @internal */ export const aggregate = <E, R>( multiplexedStream: Stream.Stream<Uint8Array, E, R> ): Stream.Stream<readonly [MultiplexedHeaderType, Uint8Array], ParseResult.ParseError | E, R> => Function.pipe( multiplexedStream, Stream.mapConcat(Function.identity), Stream.aggregateWithin(demuxMultiplexedFolderSink, Schedule.fixed(Duration.infinity)), Stream.map(({ messageBuffer, messageType }) => Tuple.make(messageType, Chunk.toReadonlyArray(messageBuffer))), Stream.flatMap(Schema.decodeUnknown(MultiplexedSchema)) ); /** @internal */ export const demuxMultiplexedToSingleSink = Function.dual< // One sink, data-last signature. <A1, L1, E1, E2, R1, R2>( source: Stream.Stream<string | Uint8Array | Socket.CloseEvent, E1, R1>, sink: Sink.Sink<A1, readonly [MultiplexedHeaderType, string], L1, E2, R2>, options?: { encoding?: string | undefined } | undefined ) => <IE = never, OE = Socket.SocketError, R3 = never>( socket: MobyDemux.EitherMultiplexedInput<E1 | IE, OE, R3> ) => Effect.Effect< A1, E1 | E2 | IE | OE | ParseResult.ParseError, Exclude<R1, Scope.Scope> | Exclude<R2, Scope.Scope> | Exclude<R3, Scope.Scope> >, // One sink, data-first signature. <A1, L1, E1, E2, R1, R2, IE = never, OE = Socket.SocketError, R3 = never>( socket: MobyDemux.EitherMultiplexedInput<E1 | IE, OE, R3>, source: Stream.Stream<string | Uint8Array | Socket.CloseEvent, E1, R1>, sink: Sink.Sink<A1, readonly [MultiplexedHeaderType, string], L1, E2, R2>, options?: { encoding?: string | undefined } | undefined ) => Effect.Effect< A1, E1 | E2 | IE | OE | ParseResult.ParseError, Exclude<R1, Scope.Scope> | Exclude<R2, Scope.Scope> | Exclude<R3, Scope.Scope> > >( (arguments_) => isMultiplexedSocket(arguments_[0]) || isMultiplexedChannel(arguments_[0]), (socket, source, sink, options) => { const { underlying } = asMultiplexedChannel(socket); const textDecoder = new TextDecoder(options?.encoding); const toStrings = Tuple.mapSecond((bytes: Uint8Array) => textDecoder.decode(bytes, { stream: true })); return Function.pipe( source, Stream.pipeThroughChannelOrFail(underlying), aggregate, Stream.map(toStrings), Stream.run(sink) ); } ); /** @internal */ export const demuxMultiplexedToSeparateSinks = Function.dual< // Two sinks, data-last signature. <A1, A2, L1, L2, E1, E2, E3, R1, R2, R3>( source: Stream.Stream<string | Uint8Array | Socket.CloseEvent, E1, R1>, sink1: Sink.Sink<A1, string, L1, E2, R2>, sink2: Sink.Sink<A2, string, L2, E3, R3>, options?: { bufferSize?: number | undefined; encoding?: string | undefined } | undefined ) => <IE = never, OE = Socket.SocketError, R4 = never>( socket: MobyDemux.EitherMultiplexedInput<E1 | IE, OE, R4> ) => Effect.Effect< MobyDemux.CompressedDemuxOutput<A1, A2>, E1 | E2 | E3 | IE | OE | ParseResult.ParseError, Exclude<R1, Scope.Scope> | Exclude<R2, Scope.Scope> | Exclude<R3, Scope.Scope> | Exclude<R4, Scope.Scope> >, // Two sinks, data-first signature. <A1, A2, L1, L2, E1, E2, E3, R1, R2, R3, IE = never, OE = Socket.SocketError, R4 = never>( socket: MobyDemux.EitherMultiplexedInput<E1 | IE, OE, R4>, source: Stream.Stream<string | Uint8Array | Socket.CloseEvent, E1, R1>, sink1: Sink.Sink<A1, string, L1, E2, R2>, sink2: Sink.Sink<A2, string, L2, E3, R3>, options?: { bufferSize?: number | undefined; encoding?: string | undefined } | undefined ) => Effect.Effect< MobyDemux.CompressedDemuxOutput<A1, A2>, E1 | E2 | E3 | IE | OE | ParseResult.ParseError, Exclude<R1, Scope.Scope> | Exclude<R2, Scope.Scope> | Exclude<R3, Scope.Scope> | Exclude<R4, Scope.Scope> > >( (arguments_) => isMultiplexedSocket(arguments_[0]) || isMultiplexedChannel(arguments_[0]), (socket, source, sink1, sink2, options) => Function.pipe( source, Stream.pipeThroughChannelOrFail(asMultiplexedChannel(socket).underlying), aggregate, Stream.partition(([messageType]) => messageType === MultiplexedHeaderType.Stderr, { bufferSize: options?.bufferSize, }), Effect.map( Tuple.mapBoth({ onFirst: Function.flow( Stream.map(Tuple.getSecond), Stream.decodeText(options?.encoding), Stream.run(sink1) ), onSecond: Function.flow( Stream.map(Tuple.getSecond), Stream.decodeText(options?.encoding), Stream.run(sink2) ), }) ), Effect.map(Effect.allWith({ concurrency: 2 })), Effect.flatten, Effect.map(internalCompressed.compressDemuxOutput), Effect.scoped ) );