shokupan 0.7.0 → 0.9.0

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 (40) hide show
  1. package/README.md +53 -0
  2. package/dist/context.d.ts +50 -15
  3. package/dist/{http-server-DFhwlK8e.cjs → http-server-BEMPIs33.cjs} +4 -2
  4. package/dist/http-server-BEMPIs33.cjs.map +1 -0
  5. package/dist/{http-server-0xH174zz.js → http-server-CCeagTyU.js} +4 -2
  6. package/dist/http-server-CCeagTyU.js.map +1 -0
  7. package/dist/index.cjs +998 -136
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.js +996 -135
  11. package/dist/index.js.map +1 -1
  12. package/dist/plugins/application/dashboard/metrics-collector.d.ts +12 -0
  13. package/dist/plugins/application/dashboard/plugin.d.ts +14 -8
  14. package/dist/plugins/application/dashboard/static/charts.js +328 -0
  15. package/dist/plugins/application/dashboard/static/failures.js +85 -0
  16. package/dist/plugins/application/dashboard/static/graph.mjs +523 -0
  17. package/dist/plugins/application/dashboard/static/poll.js +146 -0
  18. package/dist/plugins/application/dashboard/static/reactflow.css +18 -0
  19. package/dist/plugins/application/dashboard/static/registry.css +131 -0
  20. package/dist/plugins/application/dashboard/static/registry.js +269 -0
  21. package/dist/plugins/application/dashboard/static/requests.js +118 -0
  22. package/dist/plugins/application/dashboard/static/scrollbar.css +24 -0
  23. package/dist/plugins/application/dashboard/static/styles.css +175 -0
  24. package/dist/plugins/application/dashboard/static/tables.js +92 -0
  25. package/dist/plugins/application/dashboard/static/tabs.js +113 -0
  26. package/dist/plugins/application/dashboard/static/tabulator.css +66 -0
  27. package/dist/plugins/application/dashboard/template.eta +246 -0
  28. package/dist/plugins/application/socket-io.d.ts +14 -0
  29. package/dist/router.d.ts +12 -0
  30. package/dist/shokupan.d.ts +21 -1
  31. package/dist/util/datastore.d.ts +4 -3
  32. package/dist/util/decorators.d.ts +5 -0
  33. package/dist/util/http-error.d.ts +38 -0
  34. package/dist/util/http-status.d.ts +30 -0
  35. package/dist/util/request.d.ts +1 -1
  36. package/dist/util/symbol.d.ts +19 -0
  37. package/dist/util/types.d.ts +30 -1
  38. package/package.json +6 -3
  39. package/dist/http-server-0xH174zz.js.map +0 -1
  40. package/dist/http-server-DFhwlK8e.cjs.map +0 -1
package/README.md CHANGED
@@ -243,6 +243,59 @@ app.mount('/api', UserController);
243
243
  - `@Ctx()` - Access full context
244
244
  - `@Req()` - Access request object
245
245
 
246
+ ### WebSockets
247
+
248
+ > Notice: The WebSocket API is currently in an experimental stage and subject to change.
249
+
250
+ Shokupan provides native WebSocket handling and an HTTP Bridge feature.
251
+
252
+ #### Event Decorator
253
+
254
+ Handle WebSocket events directly in your controllers:
255
+
256
+ ```typescript
257
+ import { Controller, Event, ShokupanContext } from 'shokupan';
258
+
259
+ @Controller('/chat')
260
+ export class ChatController {
261
+
262
+ @Event('message')
263
+ async onMessage(ctx: ShokupanContext) {
264
+ const payload = await ctx.body();
265
+ console.log('Received:', payload);
266
+
267
+ // Reply using underlying socket
268
+ // Native Bun: ctx.socket is ServerWebSocket
269
+ // Socket.IO: ctx.socket is Socket
270
+ ctx.emit('ack', 'received');
271
+ }
272
+ }
273
+ ```
274
+
275
+ #### Socket.IO Support
276
+
277
+ Easily integrate Socket.IO and wire up your event decorators via the built-in helper:
278
+
279
+ ```typescript
280
+ import { Shokupan } from 'shokupan';
281
+ import { Server } from 'socket.io';
282
+ import { attachSocketIOBridge } from 'shokupan';
283
+
284
+ const app = new Shokupan({ enableHttpBridge: false });
285
+ const server = await app.listen(3000);
286
+
287
+ // Attach Socket.IO
288
+ const io = new Server(server.nodeServer); // For Node.js
289
+ attachSocketIOBridge(io, app);
290
+ ```
291
+
292
+ #### HTTP Bridge
293
+
294
+ Expose your HTTP API over WebSockets (set `enableHttpBridge: true` in config).
295
+
296
+ Client sends: `{ type: "HTTP", method: "GET", path: "/api/users", ... }`
297
+ Server responds: `{ type: "RESPONSE", status: 200, body: ... }`
298
+
246
299
  ### Middleware
247
300
 
248
301
  Middleware functions have access to the context and can control request flow:
package/dist/context.d.ts CHANGED
@@ -1,7 +1,9 @@
1
- import { BodyInit, Server } from 'bun';
1
+ import { BodyInit, Server, ServerWebSocket } from 'bun';
2
+ import { Socket, Server as SocketServer } from 'socket.io';
2
3
  import { Shokupan } from './shokupan';
3
4
  import { ShokupanRequest } from './util/request';
4
5
  import { ShokupanResponse } from './util/response';
6
+ import { $bodyParsed, $bodyParseError, $bodyType, $cachedBody, $cachedHost, $cachedHostname, $cachedOrigin, $cachedProtocol, $cachedQuery, $debug, $finalResponse, $io, $rawBody, $requestId, $routeMatched, $socket, $url, $ws } from './util/symbol';
5
7
  import { CookieOptions, HeadersInit, JSXRenderer } from './util/types';
6
8
  export interface HandlerStackItem {
7
9
  name: string;
@@ -87,25 +89,30 @@ export declare class ShokupanContext<State extends Record<string, any> = Record<
87
89
  state: State;
88
90
  handlerStack: HandlerStackItem[];
89
91
  readonly response: ShokupanResponse;
90
- _debug?: DebugCollector;
91
- _finalResponse?: Response;
92
- _rawBody?: string | ArrayBuffer | Uint8Array;
93
- private _url?;
94
- private _cachedBody?;
95
- private _bodyType?;
96
- private _bodyParsed;
97
- _bodyParseError?: Error;
98
- _routeMatched: boolean;
99
- private _cachedHostname?;
100
- private _cachedProtocol?;
101
- private _cachedHost?;
102
- private _cachedOrigin?;
103
- private _cachedQuery?;
92
+ [$debug]?: DebugCollector;
93
+ [$finalResponse]?: Response;
94
+ [$rawBody]?: string | ArrayBuffer | Uint8Array;
95
+ private [$url]?;
96
+ private [$cachedBody]?;
97
+ private [$bodyType]?;
98
+ private [$bodyParsed];
99
+ private [$bodyParseError]?;
100
+ [$routeMatched]: boolean;
101
+ private [$cachedHostname]?;
102
+ private [$cachedProtocol]?;
103
+ private [$cachedHost]?;
104
+ private [$cachedOrigin]?;
105
+ private [$cachedQuery]?;
106
+ private [$ws]?;
107
+ private [$socket]?;
108
+ private [$io]?;
104
109
  /**
105
110
  * JSX Rendering Function
106
111
  */
107
112
  private renderer?;
108
113
  setRenderer(renderer: JSXRenderer): void;
114
+ private [$requestId];
115
+ get requestId(): string;
109
116
  constructor(request: ShokupanRequest<any>, server?: Server, state?: State, app?: Shokupan, signal?: AbortSignal, // Optional as it might not be provided in tests or simple creates
110
117
  enableMiddlewareTracking?: boolean);
111
118
  get url(): URL;
@@ -162,12 +169,34 @@ export declare class ShokupanContext<State extends Record<string, any> = Record<
162
169
  * Base response object
163
170
  */
164
171
  get res(): ShokupanResponse;
172
+ /**
173
+ * Raw WebSocket connection
174
+ */
175
+ get ws(): ServerWebSocket<undefined>;
176
+ /**
177
+ * Socket.io socket
178
+ */
179
+ get socket(): Socket<import('socket.io').DefaultEventsMap, import('socket.io').DefaultEventsMap, import('socket.io').DefaultEventsMap, any>;
180
+ /**
181
+ * Socket.io server
182
+ */
183
+ get io(): SocketServer<import('socket.io').DefaultEventsMap, import('socket.io').DefaultEventsMap, import('socket.io').DefaultEventsMap, any>;
165
184
  /**
166
185
  * Helper to set a header on the response
167
186
  * @param key Header key
168
187
  * @param value Header value
169
188
  */
170
189
  set(key: string, value: string): this;
190
+ isUpgraded: boolean;
191
+ /**
192
+ * Upgrades the request to a WebSocket connection.
193
+ * @param options Upgrade options
194
+ * @returns true if upgraded, false otherwise
195
+ */
196
+ upgrade(options?: {
197
+ data?: any;
198
+ headers?: HeadersInit;
199
+ }): boolean;
171
200
  /**
172
201
  * Set a cookie
173
202
  * @param name Cookie name
@@ -200,6 +229,12 @@ export declare class ShokupanContext<State extends Record<string, any> = Record<
200
229
  * @returns Response
201
230
  */
202
231
  send(body?: BodyInit, options?: ResponseInit): Response;
232
+ /**
233
+ * Emit an event to the client (WebSocket only)
234
+ * @param event Event name
235
+ * @param data Event data (Must be JSON serializable)
236
+ */
237
+ emit(event: string, data?: any): void;
203
238
  /**
204
239
  * Respond with a JSON object
205
240
  */
@@ -70,7 +70,9 @@ function createHttpServer() {
70
70
  requestIP: (req) => null,
71
71
  publish: () => 0,
72
72
  subscriberCount: () => 0,
73
- url: new URL(`http://${options.hostname}:${options.port}`)
73
+ url: new URL(`http://${options.hostname}:${options.port}`),
74
+ // Expose the raw Node.js server for generic socket/websocket support (e.g. Socket.IO)
75
+ nodeServer: server
74
76
  };
75
77
  return new Promise((resolve) => {
76
78
  server.listen(options.port, options.hostname, () => {
@@ -80,4 +82,4 @@ function createHttpServer() {
80
82
  };
81
83
  }
82
84
  exports.createHttpServer = createHttpServer;
83
- //# sourceMappingURL=http-server-DFhwlK8e.cjs.map
85
+ //# sourceMappingURL=http-server-BEMPIs33.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-server-BEMPIs33.cjs","sources":["../src/plugins/application/http-server.ts"],"sourcesContent":["import type { Server } from \"bun\";\nimport * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport type { ServerFactory } from \"../../util/types\";\n\n/**\n * Creates a server factory that uses the standard Node.js `http` module.\n * @returns A ServerFactory compatible with Shokupan.\n */\nexport function createHttpServer(): ServerFactory {\n return async (options: any): Promise<Server> => {\n const server = http.createServer(async (req, res) => {\n const url = new URL(req.url!, `http://${req.headers.host}`);\n const request = new Request(url.toString(), {\n method: req.method,\n headers: req.headers as any,\n body: ['GET', 'HEAD'].includes(req.method!) ? undefined : new ReadableStream({\n start(controller) {\n req.on('data', chunk => controller.enqueue(chunk));\n req.on('end', () => controller.close());\n req.on('error', err => controller.error(err));\n }\n }) as any,\n // Required for Node.js undici when sending a body\n duplex: 'half'\n } as any);\n\n const response = await options.fetch(request, fauxServer);\n\n res.statusCode = response.status;\n response.headers.forEach((v, k) => res.setHeader(k, v));\n\n if (response.body) {\n // Optimize: Use arrayBuffer for direct conversion instead of async iteration\n const buffer = await response.arrayBuffer();\n res.end(Buffer.from(buffer));\n } else {\n res.end();\n }\n });\n\n const fauxServer: Server = {\n stop: () => {\n server.close();\n return Promise.resolve(); // Bun.Server stop usually returns void but in type definition it might vary.\n },\n upgrade(req, options) {\n return false;\n },\n reload(options) {\n return fauxServer as any;\n },\n get port() {\n const addr = server.address();\n if (typeof addr === 'object' && addr !== null) {\n return addr.port;\n }\n return options.port;\n },\n hostname: options.hostname,\n development: options.development,\n pendingRequests: 0,\n requestIP: (req) => null,\n publish: () => 0,\n subscriberCount: () => 0,\n url: new URL(`http://${options.hostname}:${options.port}`),\n // Expose the raw Node.js server for generic socket/websocket support (e.g. Socket.IO)\n nodeServer: server\n } as unknown as Server;\n\n return new Promise((resolve) => {\n server.listen(options.port, options.hostname, () => {\n resolve(fauxServer);\n });\n });\n };\n}\n\n/**\n * Creates a server factory that uses the standard Node.js `https` module.\n * @param sslOptions - Node.js HTTPS options (key, cert, etc.)\n * @returns A ServerFactory compatible with Shokupan.\n */\nexport function createHttpsServer(sslOptions: https.ServerOptions): ServerFactory {\n return async (options: any): Promise<Server> => {\n const server = https.createServer(sslOptions, async (req, res) => {\n const url = new URL(req.url!, `https://${req.headers.host}`);\n const request = new Request(url.toString(), {\n method: req.method,\n headers: req.headers as any,\n body: ['GET', 'HEAD'].includes(req.method!) ? undefined : new ReadableStream({\n start(controller) {\n req.on('data', chunk => controller.enqueue(chunk));\n req.on('end', () => controller.close());\n req.on('error', err => controller.error(err));\n }\n }) as any,\n // Required for Node.js undici when sending a body\n duplex: 'half'\n } as any);\n\n const response = await options.fetch(request, fauxServer);\n\n res.statusCode = response.status;\n response.headers.forEach((v, k) => res.setHeader(k, v));\n\n if (response.body) {\n // Optimize: Use arrayBuffer for direct conversion instead of async iteration\n const buffer = await response.arrayBuffer();\n res.end(Buffer.from(buffer));\n } else {\n res.end();\n }\n });\n\n const fauxServer: Server = {\n stop: () => {\n server.close();\n },\n upgrade(req, options) {\n return false;\n },\n reload(options) {\n return fauxServer as any;\n },\n get port() {\n const addr = server.address();\n if (typeof addr === 'object' && addr !== null) {\n return addr.port;\n }\n return options.port;\n },\n hostname: options.hostname,\n development: options.development,\n pendingRequests: 0,\n requestIP: (req) => null,\n publish: () => 0,\n subscriberCount: () => 0,\n url: new URL(`https://${options.hostname}:${options.port}`),\n // Expose the raw Node.js server for generic socket/websocket support\n nodeServer: server\n } as unknown as Server;\n\n return new Promise((resolve) => {\n server.listen(options.port, options.hostname, () => {\n resolve(fauxServer);\n });\n });\n };\n}\n"],"names":["http","options"],"mappings":";;;;;;;;;;;;;;;;;;;;;AASO,SAAS,mBAAkC;AAC9C,SAAO,OAAO,YAAkC;AAC5C,UAAM,SAASA,gBAAK,aAAa,OAAO,KAAK,QAAQ;AACjD,YAAM,MAAM,IAAI,IAAI,IAAI,KAAM,UAAU,IAAI,QAAQ,IAAI,EAAE;AAC1D,YAAM,UAAU,IAAI,QAAQ,IAAI,YAAY;AAAA,QACxC,QAAQ,IAAI;AAAA,QACZ,SAAS,IAAI;AAAA,QACb,MAAM,CAAC,OAAO,MAAM,EAAE,SAAS,IAAI,MAAO,IAAI,SAAY,IAAI,eAAe;AAAA,UACzE,MAAM,YAAY;AACd,gBAAI,GAAG,QAAQ,CAAA,UAAS,WAAW,QAAQ,KAAK,CAAC;AACjD,gBAAI,GAAG,OAAO,MAAM,WAAW,OAAO;AACtC,gBAAI,GAAG,SAAS,CAAA,QAAO,WAAW,MAAM,GAAG,CAAC;AAAA,UAChD;AAAA,QAAA,CACH;AAAA;AAAA,QAED,QAAQ;AAAA,MAAA,CACJ;AAER,YAAM,WAAW,MAAM,QAAQ,MAAM,SAAS,UAAU;AAExD,UAAI,aAAa,SAAS;AAC1B,eAAS,QAAQ,QAAQ,CAAC,GAAG,MAAM,IAAI,UAAU,GAAG,CAAC,CAAC;AAEtD,UAAI,SAAS,MAAM;AAEf,cAAM,SAAS,MAAM,SAAS,YAAA;AAC9B,YAAI,IAAI,OAAO,KAAK,MAAM,CAAC;AAAA,MAC/B,OAAO;AACH,YAAI,IAAA;AAAA,MACR;AAAA,IACJ,CAAC;AAED,UAAM,aAAqB;AAAA,MACvB,MAAM,MAAM;AACR,eAAO,MAAA;AACP,eAAO,QAAQ,QAAA;AAAA,MACnB;AAAA,MACA,QAAQ,KAAKC,UAAS;AAClB,eAAO;AAAA,MACX;AAAA,MACA,OAAOA,UAAS;AACZ,eAAO;AAAA,MACX;AAAA,MACA,IAAI,OAAO;AACP,cAAM,OAAO,OAAO,QAAA;AACpB,YAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAC3C,iBAAO,KAAK;AAAA,QAChB;AACA,eAAO,QAAQ;AAAA,MACnB;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB,aAAa,QAAQ;AAAA,MACrB,iBAAiB;AAAA,MACjB,WAAW,CAAC,QAAQ;AAAA,MACpB,SAAS,MAAM;AAAA,MACf,iBAAiB,MAAM;AAAA,MACvB,KAAK,IAAI,IAAI,UAAU,QAAQ,QAAQ,IAAI,QAAQ,IAAI,EAAE;AAAA;AAAA,MAEzD,YAAY;AAAA,IAAA;AAGhB,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC5B,aAAO,OAAO,QAAQ,MAAM,QAAQ,UAAU,MAAM;AAChD,gBAAQ,UAAU;AAAA,MACtB,CAAC;AAAA,IACL,CAAC;AAAA,EACL;AACJ;;"}
@@ -51,7 +51,9 @@ function createHttpServer() {
51
51
  requestIP: (req) => null,
52
52
  publish: () => 0,
53
53
  subscriberCount: () => 0,
54
- url: new URL(`http://${options.hostname}:${options.port}`)
54
+ url: new URL(`http://${options.hostname}:${options.port}`),
55
+ // Expose the raw Node.js server for generic socket/websocket support (e.g. Socket.IO)
56
+ nodeServer: server
55
57
  };
56
58
  return new Promise((resolve) => {
57
59
  server.listen(options.port, options.hostname, () => {
@@ -63,4 +65,4 @@ function createHttpServer() {
63
65
  export {
64
66
  createHttpServer
65
67
  };
66
- //# sourceMappingURL=http-server-0xH174zz.js.map
68
+ //# sourceMappingURL=http-server-CCeagTyU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-server-CCeagTyU.js","sources":["../src/plugins/application/http-server.ts"],"sourcesContent":["import type { Server } from \"bun\";\nimport * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport type { ServerFactory } from \"../../util/types\";\n\n/**\n * Creates a server factory that uses the standard Node.js `http` module.\n * @returns A ServerFactory compatible with Shokupan.\n */\nexport function createHttpServer(): ServerFactory {\n return async (options: any): Promise<Server> => {\n const server = http.createServer(async (req, res) => {\n const url = new URL(req.url!, `http://${req.headers.host}`);\n const request = new Request(url.toString(), {\n method: req.method,\n headers: req.headers as any,\n body: ['GET', 'HEAD'].includes(req.method!) ? undefined : new ReadableStream({\n start(controller) {\n req.on('data', chunk => controller.enqueue(chunk));\n req.on('end', () => controller.close());\n req.on('error', err => controller.error(err));\n }\n }) as any,\n // Required for Node.js undici when sending a body\n duplex: 'half'\n } as any);\n\n const response = await options.fetch(request, fauxServer);\n\n res.statusCode = response.status;\n response.headers.forEach((v, k) => res.setHeader(k, v));\n\n if (response.body) {\n // Optimize: Use arrayBuffer for direct conversion instead of async iteration\n const buffer = await response.arrayBuffer();\n res.end(Buffer.from(buffer));\n } else {\n res.end();\n }\n });\n\n const fauxServer: Server = {\n stop: () => {\n server.close();\n return Promise.resolve(); // Bun.Server stop usually returns void but in type definition it might vary.\n },\n upgrade(req, options) {\n return false;\n },\n reload(options) {\n return fauxServer as any;\n },\n get port() {\n const addr = server.address();\n if (typeof addr === 'object' && addr !== null) {\n return addr.port;\n }\n return options.port;\n },\n hostname: options.hostname,\n development: options.development,\n pendingRequests: 0,\n requestIP: (req) => null,\n publish: () => 0,\n subscriberCount: () => 0,\n url: new URL(`http://${options.hostname}:${options.port}`),\n // Expose the raw Node.js server for generic socket/websocket support (e.g. Socket.IO)\n nodeServer: server\n } as unknown as Server;\n\n return new Promise((resolve) => {\n server.listen(options.port, options.hostname, () => {\n resolve(fauxServer);\n });\n });\n };\n}\n\n/**\n * Creates a server factory that uses the standard Node.js `https` module.\n * @param sslOptions - Node.js HTTPS options (key, cert, etc.)\n * @returns A ServerFactory compatible with Shokupan.\n */\nexport function createHttpsServer(sslOptions: https.ServerOptions): ServerFactory {\n return async (options: any): Promise<Server> => {\n const server = https.createServer(sslOptions, async (req, res) => {\n const url = new URL(req.url!, `https://${req.headers.host}`);\n const request = new Request(url.toString(), {\n method: req.method,\n headers: req.headers as any,\n body: ['GET', 'HEAD'].includes(req.method!) ? undefined : new ReadableStream({\n start(controller) {\n req.on('data', chunk => controller.enqueue(chunk));\n req.on('end', () => controller.close());\n req.on('error', err => controller.error(err));\n }\n }) as any,\n // Required for Node.js undici when sending a body\n duplex: 'half'\n } as any);\n\n const response = await options.fetch(request, fauxServer);\n\n res.statusCode = response.status;\n response.headers.forEach((v, k) => res.setHeader(k, v));\n\n if (response.body) {\n // Optimize: Use arrayBuffer for direct conversion instead of async iteration\n const buffer = await response.arrayBuffer();\n res.end(Buffer.from(buffer));\n } else {\n res.end();\n }\n });\n\n const fauxServer: Server = {\n stop: () => {\n server.close();\n },\n upgrade(req, options) {\n return false;\n },\n reload(options) {\n return fauxServer as any;\n },\n get port() {\n const addr = server.address();\n if (typeof addr === 'object' && addr !== null) {\n return addr.port;\n }\n return options.port;\n },\n hostname: options.hostname,\n development: options.development,\n pendingRequests: 0,\n requestIP: (req) => null,\n publish: () => 0,\n subscriberCount: () => 0,\n url: new URL(`https://${options.hostname}:${options.port}`),\n // Expose the raw Node.js server for generic socket/websocket support\n nodeServer: server\n } as unknown as Server;\n\n return new Promise((resolve) => {\n server.listen(options.port, options.hostname, () => {\n resolve(fauxServer);\n });\n });\n };\n}\n"],"names":["options"],"mappings":";;AASO,SAAS,mBAAkC;AAC9C,SAAO,OAAO,YAAkC;AAC5C,UAAM,SAAS,KAAK,aAAa,OAAO,KAAK,QAAQ;AACjD,YAAM,MAAM,IAAI,IAAI,IAAI,KAAM,UAAU,IAAI,QAAQ,IAAI,EAAE;AAC1D,YAAM,UAAU,IAAI,QAAQ,IAAI,YAAY;AAAA,QACxC,QAAQ,IAAI;AAAA,QACZ,SAAS,IAAI;AAAA,QACb,MAAM,CAAC,OAAO,MAAM,EAAE,SAAS,IAAI,MAAO,IAAI,SAAY,IAAI,eAAe;AAAA,UACzE,MAAM,YAAY;AACd,gBAAI,GAAG,QAAQ,CAAA,UAAS,WAAW,QAAQ,KAAK,CAAC;AACjD,gBAAI,GAAG,OAAO,MAAM,WAAW,OAAO;AACtC,gBAAI,GAAG,SAAS,CAAA,QAAO,WAAW,MAAM,GAAG,CAAC;AAAA,UAChD;AAAA,QAAA,CACH;AAAA;AAAA,QAED,QAAQ;AAAA,MAAA,CACJ;AAER,YAAM,WAAW,MAAM,QAAQ,MAAM,SAAS,UAAU;AAExD,UAAI,aAAa,SAAS;AAC1B,eAAS,QAAQ,QAAQ,CAAC,GAAG,MAAM,IAAI,UAAU,GAAG,CAAC,CAAC;AAEtD,UAAI,SAAS,MAAM;AAEf,cAAM,SAAS,MAAM,SAAS,YAAA;AAC9B,YAAI,IAAI,OAAO,KAAK,MAAM,CAAC;AAAA,MAC/B,OAAO;AACH,YAAI,IAAA;AAAA,MACR;AAAA,IACJ,CAAC;AAED,UAAM,aAAqB;AAAA,MACvB,MAAM,MAAM;AACR,eAAO,MAAA;AACP,eAAO,QAAQ,QAAA;AAAA,MACnB;AAAA,MACA,QAAQ,KAAKA,UAAS;AAClB,eAAO;AAAA,MACX;AAAA,MACA,OAAOA,UAAS;AACZ,eAAO;AAAA,MACX;AAAA,MACA,IAAI,OAAO;AACP,cAAM,OAAO,OAAO,QAAA;AACpB,YAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAC3C,iBAAO,KAAK;AAAA,QAChB;AACA,eAAO,QAAQ;AAAA,MACnB;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB,aAAa,QAAQ;AAAA,MACrB,iBAAiB;AAAA,MACjB,WAAW,CAAC,QAAQ;AAAA,MACpB,SAAS,MAAM;AAAA,MACf,iBAAiB,MAAM;AAAA,MACvB,KAAK,IAAI,IAAI,UAAU,QAAQ,QAAQ,IAAI,QAAQ,IAAI,EAAE;AAAA;AAAA,MAEzD,YAAY;AAAA,IAAA;AAGhB,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC5B,aAAO,OAAO,QAAQ,MAAM,QAAQ,UAAU,MAAM;AAChD,gBAAQ,UAAU;AAAA,MACtB,CAAC;AAAA,IACL,CAAC;AAAA,EACL;AACJ;"}