@znight/http 0.1.1 → 0.1.3

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.
Files changed (47) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +83 -32
  3. package/dist/{adapter-CGSceVEN.d.mts → adapter-J0UXFnNM.d.mts} +7 -1
  4. package/dist/compiler.d.mts +1 -1
  5. package/dist/compiler.mjs +82 -13
  6. package/dist/compiler.mjs.map +1 -1
  7. package/dist/express.d.mts +1 -1
  8. package/dist/express.mjs +3 -2
  9. package/dist/express.mjs.map +1 -1
  10. package/dist/fastify.d.mts +1 -1
  11. package/dist/fastify.mjs +254 -42
  12. package/dist/fastify.mjs.map +1 -1
  13. package/dist/hono.d.mts +1 -1
  14. package/dist/hono.mjs +272 -67
  15. package/dist/hono.mjs.map +1 -1
  16. package/dist/index.d.mts +10 -2
  17. package/dist/index.mjs +930 -278
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/{node-BUnkdSsa.mjs → node-DnYv_AMI.mjs} +4 -3
  20. package/dist/{node-BUnkdSsa.mjs.map → node-DnYv_AMI.mjs.map} +1 -1
  21. package/dist/{node-io-BY1ZDLRQ.mjs → node-io-BkdRSQeg.mjs} +151 -86
  22. package/dist/{node-io-BY1ZDLRQ.mjs.map → node-io-BkdRSQeg.mjs.map} +1 -1
  23. package/dist/node.d.mts +1 -1
  24. package/dist/node.mjs +1 -1
  25. package/dist/utils-DIWdzVht.mjs +46 -0
  26. package/dist/utils-DIWdzVht.mjs.map +1 -0
  27. package/package.json +4 -4
  28. package/src/adapter.ts +6 -0
  29. package/src/adapters/express.ts +4 -1
  30. package/src/adapters/fastify.ts +329 -44
  31. package/src/adapters/hono.ts +341 -86
  32. package/src/adapters/node-io.ts +185 -82
  33. package/src/adapters/node.ts +4 -1
  34. package/src/body.ts +31 -2
  35. package/src/coerce.ts +31 -0
  36. package/src/compiler/generate.ts +127 -22
  37. package/src/context.ts +151 -78
  38. package/src/index.ts +1 -1
  39. package/src/pipeline.ts +809 -208
  40. package/src/plugin.ts +10 -4
  41. package/src/query.ts +7 -1
  42. package/src/response.ts +6 -8
  43. package/src/serialize.ts +83 -16
  44. package/src/utils.ts +25 -2
  45. package/src/validate.ts +28 -12
  46. package/dist/utils-DCbu3a4w.mjs +0 -27
  47. package/dist/utils-DCbu3a4w.mjs.map +0 -1
package/src/plugin.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { Container, Plugin, PluginContext, PluginOptions, ProviderKey, Token, ZnightMetadata } from '@znight/core'
2
2
  import type {
3
- AdapterReply,
4
3
  HttpAdapter,
5
4
  HttpServerAddress,
6
5
  InjectRequest,
@@ -49,7 +48,14 @@ import { openInjectSse } from './inject-sse.ts'
49
48
  import { isMediaTypePattern } from './mime.ts'
50
49
  import { invalidOption } from './option-error.ts'
51
50
  import { compareRoutePaths, compareStrings, joinPaths, parseRoutePath, type RoutePath, RoutePathError } from './path.ts'
52
- import { DEFAULT_SSE_SETTINGS, Pipeline, type Resolvable, type RoutePlan, type SseSettings } from './pipeline.ts'
51
+ import {
52
+ DEFAULT_SSE_SETTINGS,
53
+ Pipeline,
54
+ type Resolvable,
55
+ type RoutePlan,
56
+ randomRequestId,
57
+ type SseSettings,
58
+ } from './pipeline.ts'
53
59
  import { collectPluginRoutes } from './plugin-routes.ts'
54
60
  import { resolveTrustProxy } from './proxy.ts'
55
61
  import { RouteTable } from './router.ts'
@@ -512,7 +518,7 @@ export class HttpPlugin implements Plugin {
512
518
  adapter.registerRoute({
513
519
  method: registration.method,
514
520
  path: registration.path,
515
- handler: (request): Promise<AdapterReply> => pipeline.handle(plan, request),
521
+ handler: pipeline.handler(plan),
516
522
  })
517
523
  routes.push(
518
524
  Object.freeze({
@@ -649,7 +655,7 @@ function resolve(options: HttpPluginOptions): Resolved {
649
655
  if (requestIdHeader !== false && (typeof requestIdHeader !== 'string' || !isHeaderName(requestIdHeader))) {
650
656
  throw invalidOption('requestIdHeader', 'a header name or false', requestIdHeader)
651
657
  }
652
- const generateRequestId = options.generateRequestId ?? ((): string => crypto.randomUUID())
658
+ const generateRequestId = options.generateRequestId ?? randomRequestId
653
659
  if (typeof generateRequestId !== 'function') throw invalidOption('generateRequestId', 'a function', generateRequestId)
654
660
  const adapter = options.adapter ?? nodeAdapter()
655
661
  if (
package/src/query.ts CHANGED
@@ -23,8 +23,14 @@ export function parseQuery(query: string): QueryRecord {
23
23
  return result
24
24
  }
25
25
 
26
+ /** A request target split into its raw path and raw query string (without `?`). */
27
+ export interface RequestTarget {
28
+ readonly path: string
29
+ readonly query: string
30
+ }
31
+
26
32
  /** Splits a request target (`/users?page=2`) into its raw path and raw query (without `?`). */
27
- export function splitUrl(url: string): { readonly path: string; readonly query: string } {
33
+ export function splitUrl(url: string): RequestTarget {
28
34
  const index = url.indexOf('?')
29
35
  return index === -1 ? { path: url, query: '' } : { path: url.slice(0, index), query: url.slice(index + 1) }
30
36
  }
package/src/response.ts CHANGED
@@ -229,12 +229,10 @@ function withCookie(cookies: readonly ResponseCookie[], cookie: ResponseCookie):
229
229
 
230
230
  /** Whether `value` is an {@link HttpResponse} (also across duplicated package copies). */
231
231
  export function isHttpResponse(value: unknown): value is HttpResponse {
232
- return (
233
- value instanceof HttpResponse ||
234
- (value !== null &&
235
- typeof value === 'object' &&
236
- (value as { constructor?: { name?: unknown } }).constructor?.name === 'HttpResponse' &&
237
- 'body' in value &&
238
- 'headers' in value)
239
- )
232
+ if (value instanceof HttpResponse) return true
233
+ if (value === null || typeof value !== 'object') return false
234
+ const type = (value as { constructor?: { name?: unknown } }).constructor
235
+ // object literals and arrays (most handler results): reading a function's name is slow
236
+ if (type === Object || type === Array) return false
237
+ return type?.name === 'HttpResponse' && 'body' in value && 'headers' in value
240
238
  }
package/src/serialize.ts CHANGED
@@ -5,6 +5,7 @@ import type { ResolvedFile } from './files.ts'
5
5
  import type { HttpResponse } from './response.ts'
6
6
  import type { SseSession } from './sse.ts'
7
7
 
8
+ import { Buffer } from 'node:buffer'
8
9
  import { Readable } from 'node:stream'
9
10
 
10
11
  import { isFileBody } from './files.ts'
@@ -12,10 +13,16 @@ import { isHttpResponse } from './response.ts'
12
13
  import { isSseBody } from './sse.ts'
13
14
  import { isBodylessStatus } from './status.ts'
14
15
  import { discardStream, isByteStream, toByteStream } from './stream.ts'
15
- import { emptyRecord } from './utils.ts'
16
+ import { fastRecord, isBun } from './utils.ts'
16
17
 
17
18
  const ENCODER = new TextEncoder()
18
19
 
20
+ /**
21
+ * UTF-8 encoding of a text body: a pooled `Buffer` (a `Uint8Array`) is several times cheaper than
22
+ * `TextEncoder` on Node.js, `TextEncoder` is the fastest on Bun.
23
+ */
24
+ const utf8: (text: string) => Uint8Array = isBun() ? text => ENCODER.encode(text) : text => Buffer.from(text, 'utf8')
25
+
19
26
  /** Content types set by the serialiser. */
20
27
  export const ContentType: {
21
28
  readonly json: string
@@ -48,6 +55,10 @@ export interface SerializedReply {
48
55
  readonly status: number
49
56
  readonly headers: Record<string, HeaderValue>
50
57
  readonly body: ReplyBody
58
+ /** The text `body` encodes, for a body serialised from a string (JSON, text). */
59
+ readonly text?: string
60
+ /** Only the serialiser set the headers (content type and length): they are valid by construction. */
61
+ readonly ownHeaders?: boolean
51
62
  /** Cookies of the `HttpResponse` (`withCookie`), applied by the pipeline over those of the context. */
52
63
  readonly cookies?: readonly ResponseCookie[]
53
64
  /** The event stream of the body (`@Sse`, `HttpResponse.sse()`). */
@@ -85,12 +96,30 @@ export function serializeResult(result: unknown, options: SerializeOptions): Ser
85
96
  cookies = cookiesOf(result)
86
97
  }
87
98
  const finalStatus = options.status ?? status ?? defaultStatus(options.routeMethod, payload)
88
- const headers = emptyRecord<HeaderValue>()
89
- const body = encode(payload, headers)
90
- if (options.headers !== undefined) assignHeaders(headers, options.headers)
91
- if (options.context !== undefined) assignHeaders(headers, options.context)
92
- if (extra !== undefined) assignHeaders(headers, extra)
93
- return withCookies(finish(finalStatus, headers, body, options.method), cookies)
99
+ const headers = fastRecord<HeaderValue>()
100
+ const encoded = encode(payload, headers)
101
+ let body: ReplyBody
102
+ let text: string | undefined
103
+ if (typeof encoded === 'string') {
104
+ text = encoded
105
+ body = encodeText(encoded, headers, sendsBody(finalStatus, options.method))
106
+ } else {
107
+ body = encoded
108
+ }
109
+ let own = true
110
+ if (options.headers !== undefined) {
111
+ assignHeaders(headers, options.headers)
112
+ own = false
113
+ }
114
+ if (options.context !== undefined) {
115
+ assignHeaders(headers, options.context)
116
+ own = false
117
+ }
118
+ if (extra !== undefined) {
119
+ assignHeaders(headers, extra)
120
+ own = false
121
+ }
122
+ return withCookies(finish(finalStatus, headers, body, options.method, text, own), cookies)
94
123
  }
95
124
 
96
125
  /**
@@ -106,7 +135,7 @@ export function serializeSseReply(
106
135
  options: SerializeOptions,
107
136
  ): SerializedReply {
108
137
  const status = options.status ?? response?.status ?? 200
109
- const headers = emptyRecord<HeaderValue>()
138
+ const headers = fastRecord<HeaderValue>()
110
139
  headers['cache-control'] = 'no-cache, no-transform'
111
140
  headers['x-accel-buffering'] = 'no'
112
141
  if (options.headers !== undefined) assignHeaders(headers, options.headers)
@@ -140,7 +169,7 @@ export function serializeFileReply(
140
169
  options: SerializeOptions,
141
170
  ): SerializedReply {
142
171
  const status = file.status === 200 ? (options.status ?? response.status ?? 200) : file.status
143
- const headers = emptyRecord<HeaderValue>()
172
+ const headers = fastRecord<HeaderValue>()
144
173
  assignHeaders(headers, file.headers)
145
174
  if (options.headers !== undefined) assignHeaders(headers, options.headers)
146
175
  if (options.context !== undefined) assignHeaders(headers, options.context)
@@ -156,9 +185,10 @@ function defaultStatus(routeMethod: string, payload: unknown): number {
156
185
 
157
186
  /** Serialises an error body as JSON. */
158
187
  export function serializeError(status: number, body: HttpErrorBody | unknown, method: string): SerializedReply {
159
- const headers = emptyRecord<HeaderValue>()
188
+ const headers = fastRecord<HeaderValue>()
160
189
  const encoded = encode(body, headers)
161
- return finish(status, headers, encoded, method)
190
+ if (typeof encoded !== 'string') return finish(status, headers, encoded, method)
191
+ return finish(status, headers, encodeText(encoded, headers, sendsBody(status, method)), method, encoded)
162
192
  }
163
193
 
164
194
  /** Copies headers with lower-case names (later values win). */
@@ -184,11 +214,17 @@ export function addMissingHeaders(
184
214
  }
185
215
  }
186
216
 
217
+ /**
218
+ * The reply of a body: `text` is the string `body` encodes (see {@link encodeText}). `own`: only
219
+ * {@link encode} set `headers`, which are then valid for a text body.
220
+ */
187
221
  function finish(
188
222
  status: number,
189
223
  headers: Record<string, HeaderValue>,
190
224
  body: ReplyBody,
191
225
  method: string,
226
+ text?: string,
227
+ own = false,
192
228
  ): SerializedReply {
193
229
  if (isBodylessStatus(status)) {
194
230
  discard(body)
@@ -200,16 +236,42 @@ function finish(
200
236
  discard(body)
201
237
  return { status, headers, body: null }
202
238
  }
203
- return { status, headers, body }
239
+ if (text === undefined) return { status, headers, body }
240
+ return own ? { status, headers, body, text, ownHeaders: true } : { status, headers, body, text }
204
241
  }
205
242
 
206
243
  function discard(body: ReplyBody): void {
207
244
  if (body instanceof ReadableStream) discardStream(body)
208
245
  }
209
246
 
210
- function encode(payload: unknown, headers: Record<string, HeaderValue>): ReplyBody {
247
+ /** Whether a reply with this status, to this method, carries its body (see {@link finish}). */
248
+ function sendsBody(status: number, method: string): boolean {
249
+ return method !== 'HEAD' && !isBodylessStatus(status)
250
+ }
251
+
252
+ /**
253
+ * Sets the `content-length` of a text body (right after its `content-type`, before the route
254
+ * headers) and returns its UTF-8 bytes: their length is the header, a lone surrogate counting as
255
+ * U+FFFD. `null` when the reply sends no body (`HEAD`, bodyless status): nothing is encoded then,
256
+ * `Buffer.byteLength` counts the same bytes.
257
+ */
258
+ function encodeText(text: string, headers: Record<string, HeaderValue>, sent: boolean): Uint8Array | null {
259
+ if (!sent) {
260
+ headers['content-length'] = String(Buffer.byteLength(text, 'utf8'))
261
+ return null
262
+ }
263
+ const bytes = utf8(text)
264
+ headers['content-length'] = String(bytes.byteLength)
265
+ return bytes
266
+ }
267
+
268
+ /**
269
+ * The body of a payload, its `content-type` and, for bytes, its `content-length` set in `headers`.
270
+ * Text (a string, JSON) is returned as a string: {@link encodeText} sets its length and encodes it.
271
+ */
272
+ function encode(payload: unknown, headers: Record<string, HeaderValue>): ReplyBody | string {
211
273
  if (payload === undefined) return null
212
- if (typeof payload === 'string') return bytes(ENCODER.encode(payload), headers, ContentType.text)
274
+ if (typeof payload === 'string') return text(payload, headers, ContentType.text)
213
275
  if (payload instanceof Uint8Array) return bytes(payload, headers, ContentType.bytes)
214
276
  if (payload instanceof ArrayBuffer) return bytes(new Uint8Array(payload), headers, ContentType.bytes)
215
277
  if (ArrayBuffer.isView(payload)) {
@@ -233,7 +295,7 @@ function encode(payload: unknown, headers: Record<string, HeaderValue>): ReplyBo
233
295
  if (isSseBody(payload)) throw new TypeError('An event stream (HttpResponse.sse()) cannot be serialised here')
234
296
  const json = JSON.stringify(payload)
235
297
  if (json === undefined) throw new TypeError(`Cannot serialise a ${typeof payload} as a response body`)
236
- return bytes(ENCODER.encode(json), headers, ContentType.json)
298
+ return text(json, headers, ContentType.json)
237
299
  }
238
300
 
239
301
  function bytes(data: Uint8Array, headers: Record<string, HeaderValue>, type: string): Uint8Array {
@@ -242,6 +304,11 @@ function bytes(data: Uint8Array, headers: Record<string, HeaderValue>, type: str
242
304
  return data
243
305
  }
244
306
 
307
+ function text(data: string, headers: Record<string, HeaderValue>, type: string): string {
308
+ headers['content-type'] = type
309
+ return data
310
+ }
311
+
245
312
  function isNodeReadable(value: unknown): value is Readable {
246
313
  return (
247
314
  value instanceof Readable ||
@@ -254,7 +321,7 @@ function isNodeReadable(value: unknown): value is Readable {
254
321
  }
255
322
 
256
323
  function fromWebResponse(response: Response, method: string): SerializedReply {
257
- const headers = emptyRecord<HeaderValue>()
324
+ const headers = fastRecord<HeaderValue>()
258
325
  for (const [name, value] of response.headers) {
259
326
  if (name !== 'set-cookie') headers[name] = value
260
327
  }
package/src/utils.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  /** Internal helpers (not part of the public API). */
2
2
 
3
- /** Whether `value` is a promise-like object (has a callable `then`). */
4
- export function isThenable(value: unknown): value is PromiseLike<unknown> {
3
+ /**
4
+ * Whether `value` is a promise-like object (has a callable `then`). The guard keeps the thenable
5
+ * part of the value's type (`Promise<R>` of `R | Promise<R>`, even when `R` is `object` or `any`), so
6
+ * that generated code awaiting it gets `R`.
7
+ */
8
+ export function isThenable<T>(
9
+ value: T,
10
+ ): value is unknown extends T ? T & PromiseLike<unknown> : Extract<T, PromiseLike<unknown>> {
5
11
  return (
6
12
  value !== null &&
7
13
  (typeof value === 'object' || typeof value === 'function') &&
@@ -25,6 +31,23 @@ export function emptyRecord<V>(): Record<string, V> {
25
31
  return Object.create(null) as Record<string, V>
26
32
  }
27
33
 
34
+ /**
35
+ * Constructor of {@link fastRecord}: its prototype is an empty, frozen object without prototype, so
36
+ * nothing is inherited (`__proto__`, `constructor`, `toString`... are plain own keys).
37
+ */
38
+ const FastRecord = function FastRecord() {} as unknown as new () => object
39
+ FastRecord.prototype = Object.freeze(Object.create(null))
40
+
41
+ /**
42
+ * A `Record` that inherits nothing, like {@link emptyRecord}, but that V8 keeps in fast mode: an
43
+ * `Object.create(null)` object is a dictionary, about twice as slow to fill and enumerate (response
44
+ * headers: built, completed and enumerated on every request). Its prototype is not `null` (an empty
45
+ * frozen object): use {@link emptyRecord} for records handed to user code (query, fields...).
46
+ */
47
+ export function fastRecord<V>(): Record<string, V> {
48
+ return new FastRecord() as Record<string, V>
49
+ }
50
+
28
51
  /** Whether the code runs on Bun. */
29
52
  export function isBun(): boolean {
30
53
  return typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined'
package/src/validate.ts CHANGED
@@ -1,20 +1,28 @@
1
- import { ValidationException } from './exceptions.ts'
2
- import {
3
- type InferSchemaOutput,
4
- isStandardSchema,
5
- type StandardSchemaV1,
6
- type StandardSchemaV1Issue,
7
- type StandardSchemaV1Result,
1
+ import type {
2
+ InferSchemaOutput,
3
+ StandardSchemaV1,
4
+ StandardSchemaV1Issue,
5
+ StandardSchemaV1Props,
6
+ StandardSchemaV1Result,
8
7
  } from './standard-schema.ts'
8
+
9
+ import { ValidationException } from './exceptions.ts'
9
10
  import { isThenable } from './utils.ts'
10
11
 
12
+ /**
13
+ * Whether `value` is a promise-like object (has a callable `then`). Called by the generated
14
+ * `invoke` functions on the result of {@link validate} (README, "Compiler contract").
15
+ */
16
+ export { isThenable } from './utils.ts'
17
+
11
18
  /**
12
19
  * Validates `value` with a [Standard Schema](https://standardschema.dev) (Zod, Valibot, ArkType...)
13
20
  * and returns the schema **output** (defaults applied, transforms run, unknown keys stripped when
14
21
  * the schema strips them).
15
22
  *
16
23
  * Returns synchronously when the schema validates synchronously, a promise otherwise (async
17
- * refinements): generated code always `await`s it.
24
+ * refinements): generated code calls the handler directly with the value, and `await`s the value
25
+ * when it is a thenable (`isThenable`).
18
26
  *
19
27
  * @param location what is validated, used in the error message: `body`, `query`, `query "page"`,
20
28
  * `param "id"`, `headers`, `cookie "sid"`, `cookies`...
@@ -28,12 +36,20 @@ export function validate<S extends StandardSchemaV1>(
28
36
  value: unknown,
29
37
  location: string,
30
38
  ): InferSchemaOutput<S> | Promise<InferSchemaOutput<S>> {
31
- if (!isStandardSchema(schema)) {
39
+ // `~standard` is read once (the same checks as `isStandardSchema`)
40
+ const props =
41
+ schema !== null && (typeof schema === 'object' || typeof schema === 'function')
42
+ ? (schema as { readonly '~standard'?: unknown })['~standard']
43
+ : undefined
44
+ if (
45
+ props === null ||
46
+ typeof props !== 'object' ||
47
+ (props as { readonly version?: unknown }).version !== 1 ||
48
+ typeof (props as { readonly validate?: unknown }).validate !== 'function'
49
+ ) {
32
50
  throw new TypeError(`The schema of ${location} is not a Standard Schema v1 (missing "~standard.validate")`)
33
51
  }
34
- const result = schema['~standard'].validate(value) as
35
- | StandardSchemaV1Result<InferSchemaOutput<S>>
36
- | Promise<StandardSchemaV1Result<InferSchemaOutput<S>>>
52
+ const result = (props as StandardSchemaV1Props<unknown, InferSchemaOutput<S>>).validate(value)
37
53
  if (isThenable(result)) return Promise.resolve(result).then(settled => unwrap(settled, location))
38
54
  return unwrap(result, location)
39
55
  }
@@ -1,27 +0,0 @@
1
- // src/utils.ts
2
- /** Internal helpers (not part of the public API). */
3
- /** Whether `value` is a promise-like object (has a callable `then`). */
4
- function isThenable(value) {
5
- return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
6
- }
7
- /** Message of any thrown value. */
8
- function errorMessage(error) {
9
- if (error instanceof Error) return error.message;
10
- if (typeof error === "string") return error;
11
- try {
12
- return String(error);
13
- } catch {
14
- return Object.prototype.toString.call(error);
15
- }
16
- }
17
- /** A `Record` without prototype (safe for user-controlled keys such as `__proto__`). */
18
- function emptyRecord() {
19
- return Object.create(null);
20
- }
21
- /** Whether the code runs on Bun. */
22
- function isBun() {
23
- return typeof globalThis.Bun !== "undefined";
24
- }
25
- export { isThenable as i, errorMessage as n, isBun as r, emptyRecord as t };
26
-
27
- //# sourceMappingURL=utils-DCbu3a4w.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"utils-DCbu3a4w.mjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["/** Internal helpers (not part of the public API). */\n\n/** Whether `value` is a promise-like object (has a callable `then`). */\nexport function isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n value !== null &&\n (typeof value === 'object' || typeof value === 'function') &&\n typeof (value as { then?: unknown }).then === 'function'\n )\n}\n\n/** Message of any thrown value. */\nexport function errorMessage(error: unknown): string {\n if (error instanceof Error) return error.message\n if (typeof error === 'string') return error\n try {\n return String(error)\n } catch {\n return Object.prototype.toString.call(error)\n }\n}\n\n/** A `Record` without prototype (safe for user-controlled keys such as `__proto__`). */\nexport function emptyRecord<V>(): Record<string, V> {\n return Object.create(null) as Record<string, V>\n}\n\n/** Whether the code runs on Bun. */\nexport function isBun(): boolean {\n return typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined'\n}\n"],"mappings":";;;AAGA,SAAgB,WAAW,OAA+C;CACxE,OACE,UAAU,SACT,OAAO,UAAU,YAAY,OAAO,UAAU,eAC/C,OAAQ,MAA6B,SAAS;AAElD;;AAGA,SAAgB,aAAa,OAAwB;CACnD,IAAI,iBAAiB,OAAO,OAAO,MAAM;CACzC,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI;EACF,OAAO,OAAO,KAAK;CACrB,QAAQ;EACN,OAAO,OAAO,UAAU,SAAS,KAAK,KAAK;CAC7C;AACF;;AAGA,SAAgB,cAAoC;CAClD,OAAO,OAAO,OAAO,IAAI;AAC3B;;AAGA,SAAgB,QAAiB;CAC/B,OAAO,OAAQ,WAAiC,QAAQ;AAC1D"}