@atproto/xrpc-server 0.11.5 → 0.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,125 +0,0 @@
1
- import { decodeAll, encode } from '@atproto/lex-cbor'
2
- import { LexValue, isPlainObject } from '@atproto/lex-data'
3
- import { XRPCError } from '../errors.js'
4
- import {
5
- ErrorFrameBody,
6
- ErrorFrameHeader,
7
- FrameHeader,
8
- FrameType,
9
- MessageFrameHeader,
10
- errorFrameBody,
11
- frameHeader,
12
- } from './types.js'
13
-
14
- export abstract class Frame<T extends LexValue = LexValue> {
15
- abstract header: FrameHeader
16
- abstract body: T
17
-
18
- get op(): FrameType {
19
- return this.header.op
20
- }
21
- toBytes(): Uint8Array {
22
- return Buffer.concat([encode(this.header), encode(this.body)])
23
- }
24
- isMessage(): this is MessageFrame {
25
- return this.op === FrameType.Message
26
- }
27
- isError(): this is ErrorFrame {
28
- return this.op === FrameType.Error
29
- }
30
- static fromBytes(bytes: Uint8Array) {
31
- const [header, body, ...rest] = decodeAll(bytes)
32
- if (rest.length) {
33
- throw new Error('Too many CBOR data items in frame')
34
- } else if (body === undefined) {
35
- throw new Error('Missing frame body')
36
- }
37
-
38
- const parsedHeader = frameHeader.safeParse(header)
39
- if (!parsedHeader.success) {
40
- throw new Error(`Invalid frame header: ${parsedHeader.reason.message}`)
41
- }
42
- const frameOp = parsedHeader.value.op
43
- if (frameOp === FrameType.Message) {
44
- return new MessageFrame(body, {
45
- type: parsedHeader.value.t,
46
- })
47
- } else if (frameOp === FrameType.Error) {
48
- const parsedBody = errorFrameBody.safeParse(body)
49
- if (!parsedBody.success) {
50
- throw new Error(
51
- `Invalid error frame body: ${parsedBody.reason.message}`,
52
- )
53
- }
54
- return new ErrorFrame(parsedBody.value)
55
- } else {
56
- const exhaustiveCheck: never = frameOp
57
- throw new Error(`Unknown frame op: ${exhaustiveCheck}`)
58
- }
59
- }
60
- }
61
-
62
- export class MessageFrame<T extends LexValue = LexValue> extends Frame<T> {
63
- header: MessageFrameHeader
64
- body: T
65
-
66
- constructor(body: T, opts?: { type?: string }) {
67
- super()
68
- this.header =
69
- opts?.type !== undefined
70
- ? { op: FrameType.Message, t: opts?.type }
71
- : { op: FrameType.Message }
72
- this.body = body
73
- }
74
- get type() {
75
- return this.header.t
76
- }
77
-
78
- static fromLexValue(data: LexValue, nsid: string) {
79
- if (!isPlainObject(data)) {
80
- return new MessageFrame(data)
81
- }
82
-
83
- const $type = data?.['$type']
84
- if (typeof $type !== 'string') {
85
- return new MessageFrame(data)
86
- }
87
-
88
- let type: string
89
-
90
- const split = $type.split('#')
91
- if (split.length === 2 && (split[0] === '' || split[0] === nsid)) {
92
- type = `#${split[1]}`
93
- } else {
94
- type = $type
95
- }
96
-
97
- const { $type: _, ...clone } = data
98
- return new MessageFrame(clone, { type })
99
- }
100
- }
101
-
102
- export class ErrorFrame<T extends string = string> extends Frame<
103
- ErrorFrameBody<T>
104
- > {
105
- header: ErrorFrameHeader
106
- body: ErrorFrameBody<T>
107
-
108
- constructor(body: ErrorFrameBody<T>) {
109
- super()
110
- this.header = { op: FrameType.Error }
111
- this.body = body
112
- }
113
- get code() {
114
- return this.body.error
115
- }
116
- get message() {
117
- return this.body.message
118
- }
119
-
120
- static fromError(err: unknown): ErrorFrame {
121
- if (err instanceof ErrorFrame) return err
122
- const { error = 'Unknown', message } = XRPCError.fromError(err).payload
123
- return new ErrorFrame({ error, message })
124
- }
125
- }
@@ -1,5 +0,0 @@
1
- export * from './types.js'
2
- export * from './frames.js'
3
- export * from './stream.js'
4
- export * from './subscription.js'
5
- export * from './server.js'
@@ -1,6 +0,0 @@
1
- import { subsystemLogger } from '@atproto/common'
2
-
3
- export const logger: ReturnType<typeof subsystemLogger> =
4
- subsystemLogger('xrpc-stream')
5
-
6
- export default logger
@@ -1,66 +0,0 @@
1
- import { IncomingMessage } from 'node:http'
2
- import type { ServerOptions } from 'ws'
3
- import { WebSocket, WebSocketServer } from 'ws'
4
- import { CloseCode, DisconnectError } from '@atproto/ws-client'
5
- import { ErrorFrame, Frame } from './frames.js'
6
- import { logger } from './logger.js'
7
-
8
- export class XrpcStreamServer {
9
- wss: WebSocketServer
10
- constructor(opts: ServerOptions & { handler: Handler }) {
11
- const { handler, ...serverOpts } = opts
12
- this.wss = new WebSocketServer(serverOpts)
13
- this.wss.on('connection', async (socket, req) => {
14
- socket.on('error', (err) => logger.error(err, 'websocket error'))
15
- try {
16
- const ac = new AbortController()
17
- const iterator = unwrapIterator(handler(req, ac.signal, socket, this))
18
- socket.once('close', () => {
19
- iterator.return?.()
20
- ac.abort()
21
- })
22
- const safeFrames = wrapIterator(iterator)
23
- for await (const frame of safeFrames) {
24
- await new Promise((res, rej) => {
25
- socket.send(frame.toBytes(), { binary: true }, (err) => {
26
- // @TODO this callback may give more aggressive on backpressure than
27
- // we ultimately want, but trying it out for the time being.
28
- if (err) return rej(err)
29
- res(undefined)
30
- })
31
- })
32
- if (frame instanceof ErrorFrame) {
33
- throw new DisconnectError(CloseCode.Policy, frame.body.error)
34
- }
35
- }
36
- } catch (err) {
37
- if (err instanceof DisconnectError) {
38
- return socket.close(err.wsCode, err.xrpcCode)
39
- } else {
40
- logger.error({ err }, 'websocket server error')
41
- return socket.terminate()
42
- }
43
- }
44
- socket.close(CloseCode.Normal)
45
- })
46
- }
47
- }
48
-
49
- export type Handler = (
50
- req: IncomingMessage,
51
- signal: AbortSignal,
52
- socket: WebSocket,
53
- server: XrpcStreamServer,
54
- ) => AsyncIterable<Frame>
55
-
56
- function unwrapIterator<T>(iterable: AsyncIterable<T>): AsyncIterator<T> {
57
- return iterable[Symbol.asyncIterator]()
58
- }
59
-
60
- function wrapIterator<T>(iterator: AsyncIterator<T>): AsyncIterable<T> {
61
- return {
62
- [Symbol.asyncIterator]() {
63
- return iterator
64
- },
65
- }
66
- }
@@ -1,39 +0,0 @@
1
- import { DuplexOptions } from 'node:stream'
2
- import { WebSocket, createWebSocketStream } from 'ws'
3
- import { ResponseType, XRPCError } from '@atproto/xrpc'
4
- import { Frame, MessageFrame } from './frames.js'
5
-
6
- export function streamByteChunks(ws: WebSocket, options?: DuplexOptions) {
7
- return createWebSocketStream(ws, {
8
- ...options,
9
- readableObjectMode: true, // Ensures frame bytes don't get buffered/combined together
10
- })
11
- }
12
-
13
- export async function* byFrame(ws: WebSocket, options?: DuplexOptions) {
14
- const wsStream = streamByteChunks(ws, options)
15
- for await (const chunk of wsStream) {
16
- yield Frame.fromBytes(chunk)
17
- }
18
- }
19
-
20
- export async function* byMessage(ws: WebSocket, options?: DuplexOptions) {
21
- const wsStream = streamByteChunks(ws, options)
22
- for await (const chunk of wsStream) {
23
- const msg = ensureChunkIsMessage(chunk)
24
- yield msg
25
- }
26
- }
27
-
28
- export function ensureChunkIsMessage(chunk: Uint8Array): MessageFrame {
29
- const frame = Frame.fromBytes(chunk)
30
- if (frame.isMessage()) {
31
- return frame
32
- } else if (frame.isError()) {
33
- // @TODO work -1 error code into XRPCError
34
- // @ts-ignore
35
- throw new XRPCError(-1, frame.code, frame.message)
36
- } else {
37
- throw new XRPCError(ResponseType.Unknown, undefined, 'Unknown frame type')
38
- }
39
- }
@@ -1,96 +0,0 @@
1
- import type { ClientOptions } from 'ws'
2
- import { isPlainObject } from '@atproto/lex-data'
3
- import { WebSocketKeepAlive } from '@atproto/ws-client'
4
- import { ensureChunkIsMessage } from './stream.js'
5
-
6
- export class Subscription<T = unknown> {
7
- constructor(
8
- public opts: ClientOptions & {
9
- service: string
10
- method: string
11
- maxReconnectSeconds?: number
12
- heartbeatIntervalMs?: number
13
- signal?: AbortSignal
14
- validate: (obj: unknown) => T | undefined
15
- onReconnectError?: (
16
- error: unknown,
17
- n: number,
18
- initialSetup: boolean,
19
- ) => void
20
- getParams?: () =>
21
- | Record<string, unknown>
22
- | Promise<Record<string, unknown> | undefined>
23
- | undefined
24
- },
25
- ) {}
26
-
27
- async *[Symbol.asyncIterator](): AsyncGenerator<T> {
28
- const ws = new WebSocketKeepAlive({
29
- ...this.opts,
30
- getUrl: async () => {
31
- const params = (await this.opts.getParams?.()) ?? {}
32
- const query = encodeQueryParams(params)
33
- return `${this.opts.service}/xrpc/${this.opts.method}?${query}`
34
- },
35
- })
36
- for await (const chunk of ws) {
37
- const message = ensureChunkIsMessage(chunk)
38
- const t = message.header.t
39
-
40
- const typedBody = isPlainObject(message.body)
41
- ? t !== undefined
42
- ? {
43
- ...message.body,
44
- $type: t.startsWith('#') ? this.opts.method + t : t,
45
- }
46
- : message.body
47
- : undefined
48
-
49
- const result = this.opts.validate(typedBody)
50
- if (result !== undefined) {
51
- yield result
52
- }
53
- }
54
- }
55
- }
56
-
57
- export default Subscription
58
-
59
- function encodeQueryParams(obj: Record<string, unknown>): string {
60
- const params = new URLSearchParams()
61
- Object.entries(obj).forEach(([key, value]) => {
62
- const encoded = encodeQueryParam(value)
63
- if (Array.isArray(encoded)) {
64
- encoded.forEach((enc) => params.append(key, enc))
65
- } else {
66
- params.set(key, encoded)
67
- }
68
- })
69
- return params.toString()
70
- }
71
-
72
- // Adapted from xrpc, but without any lex-specific knowledge
73
- function encodeQueryParam(value: unknown): string | string[] {
74
- if (typeof value === 'string') {
75
- return value
76
- }
77
- if (typeof value === 'number') {
78
- return value.toString()
79
- }
80
- if (typeof value === 'boolean') {
81
- return value ? 'true' : 'false'
82
- }
83
- if (typeof value === 'undefined') {
84
- return ''
85
- }
86
- if (typeof value === 'object') {
87
- if (value instanceof Date) {
88
- return value.toISOString()
89
- } else if (Array.isArray(value)) {
90
- return value.flatMap(encodeQueryParam)
91
- } else if (!value) {
92
- return ''
93
- }
94
- }
95
- throw new Error(`Cannot encode ${typeof value}s into query params`)
96
- }
@@ -1,27 +0,0 @@
1
- import { l } from '@atproto/lex-schema'
2
-
3
- export enum FrameType {
4
- Message = 1,
5
- Error = -1,
6
- }
7
-
8
- export const messageFrameHeader = l.object({
9
- op: l.literal(FrameType.Message), // Frame op
10
- t: l.optional(l.string()), // Message body type discriminator
11
- })
12
- export type MessageFrameHeader = l.Infer<typeof messageFrameHeader>
13
-
14
- export const errorFrameHeader = l.object({
15
- op: l.literal(FrameType.Error),
16
- })
17
- export const errorFrameBody = l.object({
18
- error: l.string(), // Error code
19
- message: l.optional(l.string()), // Error message
20
- })
21
- export type ErrorFrameHeader = l.Infer<typeof errorFrameHeader>
22
- export type ErrorFrameBody<T extends string = string> = { error: T } & l.Infer<
23
- typeof errorFrameBody
24
- >
25
-
26
- export const frameHeader = l.union([messageFrameHeader, errorFrameHeader])
27
- export type FrameHeader = l.Infer<typeof frameHeader>
package/src/types.ts DELETED
@@ -1,330 +0,0 @@
1
- import { IncomingMessage } from 'node:http'
2
- import { Readable } from 'node:stream'
3
- import { NextFunction, Request, Response } from 'express'
4
- import { l } from '@atproto/lex-schema'
5
- import { ErrorResult, XRPCError } from './errors.js'
6
- import { CalcKeyFn, CalcPointsFn, RateLimiterI } from './rate-limiter.js'
7
-
8
- export type Awaitable<T> = T | Promise<T>
9
-
10
- export type CatchallHandler = (
11
- req: Request,
12
- res: Response,
13
- next: NextFunction,
14
- ) => unknown
15
-
16
- export type Options = {
17
- validateResponse?: boolean
18
- catchall?: CatchallHandler
19
- payload?: RouteOptions
20
- rateLimits?: {
21
- creator: RateLimiterCreator<HandlerContext>
22
- global?: ServerRateLimitDescription<HandlerContext>[]
23
- shared?: ServerRateLimitDescription<HandlerContext>[]
24
- bypass?: (ctx: HandlerContext) => boolean
25
- }
26
- /**
27
- * By default, errors are converted to {@link XRPCError} using
28
- * {@link XRPCError.fromError} before being rendered. If method handlers throw
29
- * error objects that are not properly rendered in the HTTP response, this
30
- * function can be used to properly convert them to {@link XRPCError}. The
31
- * provided function will typically fallback to the default error conversion
32
- * (`return XRPCError.fromError(err)`) if the error is not recognized.
33
- *
34
- * @note This function should not throw errors.
35
- */
36
- errorParser?: (err: unknown) => XRPCError
37
- }
38
-
39
- export type UndecodedParams = Request['query']
40
-
41
- export type Primitive = string | number | boolean
42
- export type Params = { [P in string]?: undefined | Primitive | Primitive[] }
43
-
44
- export type HandlerInput = {
45
- encoding: string
46
- body: unknown
47
- }
48
-
49
- export type AuthResult = {
50
- credentials: unknown
51
- artifacts?: unknown
52
- }
53
-
54
- export const headersSchema = l.dict(l.string(), l.string())
55
-
56
- export type Headers = l.Infer<typeof headersSchema>
57
-
58
- export const handlerSuccess = l.object({
59
- encoding: l.string(),
60
- body: l.unknown(),
61
- headers: l.optional(headersSchema),
62
- })
63
-
64
- export type HandlerSuccess = l.Infer<typeof handlerSuccess>
65
-
66
- export const handlerPipeThroughBuffer = l.object({
67
- encoding: l.string(),
68
- buffer: l.custom(
69
- (v): v is Buffer => v instanceof Buffer,
70
- 'Expected a Buffer',
71
- ),
72
- headers: l.optional(headersSchema),
73
- })
74
-
75
- export type HandlerPipeThroughBuffer = l.Infer<typeof handlerPipeThroughBuffer>
76
-
77
- export const handlerPipeThroughStream = l.object({
78
- encoding: l.string(),
79
- stream: l.custom(
80
- (v): v is Readable => v instanceof Readable,
81
- 'Expected a Readable stream',
82
- ),
83
- headers: l.optional(headersSchema),
84
- })
85
-
86
- export type HandlerPipeThroughStream = l.Infer<typeof handlerPipeThroughStream>
87
-
88
- export const handlerPipeThrough = l.union([
89
- handlerPipeThroughBuffer,
90
- handlerPipeThroughStream,
91
- ])
92
-
93
- export type HandlerPipeThrough = l.Infer<typeof handlerPipeThrough>
94
-
95
- export type Auth = void | AuthResult
96
- export type Input = void | HandlerInput
97
- export type Output = void | HandlerSuccess | HandlerPipeThrough | ErrorResult
98
-
99
- export type AuthVerifier<C, A extends AuthResult = AuthResult> =
100
- | ((ctx: C) => Awaitable<A | ErrorResult>)
101
- | ((ctx: C) => Awaitable<A>)
102
-
103
- export type MethodAuthContext<P extends Params = Params> = {
104
- params: P
105
- req: Request
106
- res: Response
107
- }
108
-
109
- export type MethodAuthVerifier<
110
- A extends AuthResult = AuthResult,
111
- P extends Params = Params,
112
- > = AuthVerifier<MethodAuthContext<P>, A>
113
-
114
- export type HandlerContext<
115
- A extends Auth = Auth,
116
- P extends Params = Params,
117
- I extends Input = Input,
118
- > = MethodAuthContext<P> & {
119
- auth: A
120
- input: I
121
- resetRouteRateLimits: () => Promise<void>
122
- }
123
-
124
- export type MethodHandler<
125
- A extends Auth = Auth,
126
- P extends Params = Params,
127
- I extends Input = Input,
128
- O extends Output = Output,
129
- > = (ctx: HandlerContext<A, P, I>) => Awaitable<O | HandlerPipeThrough>
130
-
131
- export type RateLimiterCreator<T extends HandlerContext = HandlerContext> = <
132
- C extends T = T,
133
- >(opts: {
134
- keyPrefix: string
135
- durationMs: number
136
- points: number
137
- calcKey: CalcKeyFn<C>
138
- calcPoints: CalcPointsFn<C>
139
- failClosed?: boolean
140
- }) => RateLimiterI<C>
141
-
142
- export type ServerRateLimitDescription<
143
- C extends HandlerContext = HandlerContext,
144
- > = {
145
- name: string
146
- durationMs: number
147
- points: number
148
- calcKey?: CalcKeyFn<C>
149
- calcPoints?: CalcPointsFn<C>
150
- failClosed?: boolean
151
- }
152
-
153
- export type SharedRateLimitOpts<C extends HandlerContext = HandlerContext> = {
154
- name: string
155
- calcKey?: CalcKeyFn<C>
156
- calcPoints?: CalcPointsFn<C>
157
- }
158
-
159
- export type RouteRateLimitOpts<C extends HandlerContext = HandlerContext> = {
160
- durationMs: number
161
- points: number
162
- calcKey?: CalcKeyFn<C>
163
- calcPoints?: CalcPointsFn<C>
164
- }
165
-
166
- export type RateLimitOpts<C extends HandlerContext = HandlerContext> =
167
- | SharedRateLimitOpts<C>
168
- | RouteRateLimitOpts<C>
169
-
170
- export function isSharedRateLimitOpts<
171
- C extends HandlerContext = HandlerContext,
172
- >(opts: RateLimitOpts<C>): opts is SharedRateLimitOpts<C> {
173
- return typeof opts['name'] === 'string'
174
- }
175
-
176
- export type RouteOptions = {
177
- blobLimit?: number
178
- jsonLimit?: number
179
- textLimit?: number
180
- paramsParseLoose?: boolean
181
- }
182
-
183
- export type MethodAuth<
184
- A extends Auth = Auth,
185
- P extends Params = Params,
186
- > = MethodAuthVerifier<Extract<A, AuthResult>, P>
187
-
188
- export type MethodRateLimit<
189
- A extends Auth = Auth,
190
- P extends Params = Params,
191
- I extends Input = Input,
192
- > =
193
- | RateLimitOpts<HandlerContext<A, P, I>>
194
- | RateLimitOpts<HandlerContext<A, P, I>>[]
195
-
196
- export type MethodConfig<
197
- A extends Auth = Auth,
198
- P extends Params = Params,
199
- I extends Input = Input,
200
- O extends Output = Output,
201
- > = {
202
- handler: MethodHandler<A, P, I, O>
203
- auth?: MethodAuth<A, P>
204
- opts?: RouteOptions
205
- rateLimit?: MethodRateLimit<A, P, I>
206
- }
207
-
208
- export type MethodConfigWithAuth<
209
- A extends Auth = Auth,
210
- P extends Params = Params,
211
- I extends Input = Input,
212
- O extends Output = Output,
213
- > = {
214
- handler: MethodHandler<A, P, I, O>
215
- auth: MethodAuth<A, P>
216
- opts?: RouteOptions
217
- rateLimit?: MethodRateLimit<A, P, I>
218
- }
219
-
220
- export type MethodConfigOrHandler<
221
- A extends Auth = Auth,
222
- P extends Params = Params,
223
- I extends Input = Input,
224
- O extends Output = Output,
225
- > = MethodHandler<A, P, I, O> | MethodConfig<A, P, I, O>
226
-
227
- export type StreamAuthContext<P extends Params = Params> = {
228
- params: P
229
- req: IncomingMessage
230
- }
231
-
232
- export type LexMethodParams<M extends l.Procedure | l.Query | l.Subscription> =
233
- l.InferMethodParams<M>
234
-
235
- export type LexMethodInput<M extends l.Procedure | l.Query> =
236
- l.InferMethodInput<M, Readable>
237
-
238
- export type LexMethodOutput<M extends l.Procedure | l.Query> =
239
- l.InferMethodOutput<M, Readable> extends undefined
240
- ? l.InferMethodOutput<M, Uint8Array | Readable> | void
241
- : l.InferMethodOutput<M, Uint8Array | Readable>
242
-
243
- export type LexMethodMessage<M extends l.Subscription> = l.InferMethodMessage<M>
244
-
245
- export type LexMethodHandler<
246
- M extends l.Procedure | l.Query,
247
- A extends Auth = Auth,
248
- > = MethodHandler<A, LexMethodParams<M>, LexMethodInput<M>, LexMethodOutput<M>>
249
-
250
- export type LexMethodConfig<
251
- M extends l.Procedure | l.Query,
252
- A extends Auth = Auth,
253
- > = MethodConfig<A, LexMethodParams<M>, LexMethodInput<M>, LexMethodOutput<M>>
254
-
255
- export type LexSubscriptionHandler<
256
- M extends l.Subscription,
257
- A extends Auth = Auth,
258
- > = StreamHandler<
259
- Extract<A, AuthResult>,
260
- LexMethodParams<M>,
261
- LexMethodMessage<M>
262
- >
263
-
264
- export type LexSubscriptionConfig<
265
- M extends l.Subscription,
266
- A extends Auth = Auth,
267
- > = StreamConfig<A, LexMethodParams<M>, LexMethodMessage<M>>
268
-
269
- export type StreamAuthVerifier<
270
- A extends AuthResult = AuthResult,
271
- P extends Params = Params,
272
- > = AuthVerifier<StreamAuthContext<P>, A>
273
-
274
- export type StreamContext<
275
- A extends Auth = Auth,
276
- P extends Params = Params,
277
- > = StreamAuthContext<P> & {
278
- auth: A
279
- signal: AbortSignal
280
- }
281
-
282
- export type StreamHandler<
283
- A extends Auth = Auth,
284
- P extends Params = Params,
285
- O = unknown,
286
- > = (ctx: StreamContext<A, P>) => AsyncIterable<O>
287
-
288
- export type StreamConfig<
289
- A extends Auth = Auth,
290
- P extends Params = Params,
291
- O = unknown,
292
- > = {
293
- auth?: StreamAuthVerifier<Extract<A, AuthResult>, P>
294
- handler: StreamHandler<A, P, O>
295
- }
296
-
297
- export type StreamConfigOrHandler<
298
- A extends Auth = Auth,
299
- P extends Params = Params,
300
- O = unknown,
301
- > = StreamHandler<A, P, O> | StreamConfig<A, P, O>
302
-
303
- export function isHandlerSuccess(output: Output): output is HandlerSuccess {
304
- // We only need to discriminate between possible Output values
305
- return (
306
- output != null &&
307
- 'body' in output && // body is non optional (contrary to what type inference may suggest)
308
- 'encoding' in output &&
309
- // Allows using objects that extends HandlerSuccess with a "status" field as
310
- // output, as long as the status is < 400, in order to avoid being confused
311
- // with ErrorResult objects.
312
- (!('status' in output) ||
313
- output.status == null ||
314
- Number(output.status) < 400)
315
- )
316
- }
317
-
318
- export function isHandlerPipeThroughBuffer(
319
- output: Output,
320
- ): output is HandlerPipeThroughBuffer {
321
- // We only need to discriminate between possible Output values
322
- return output != null && 'buffer' in output && output['buffer'] !== undefined
323
- }
324
-
325
- export function isHandlerPipeThroughStream(
326
- output: Output,
327
- ): output is HandlerPipeThroughStream {
328
- // We only need to discriminate between possible Output values
329
- return output != null && 'stream' in output && output['stream'] !== undefined
330
- }