@motiadev/core 0.15.6-beta.174 → 0.16.0-beta.175
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracer.mjs","names":["traceStream: MotiaStream<Trace>","traceGroupStream: MotiaStream<TraceGroup>","traceGroup: TraceGroup","traceStreamAdapter: MotiaStream<Trace>","traceGroupStreamAdapter: MotiaStream<TraceGroup>"],"sources":["../../../src/observability/tracer.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"tracer.mjs","names":["traceStream: MotiaStream<Trace>","traceGroupStream: MotiaStream<TraceGroup>","traceGroup: TraceGroup","traceStreamAdapter: MotiaStream<Trace>","traceGroupStreamAdapter: MotiaStream<TraceGroup>"],"sources":["../../../src/observability/tracer.ts"],"sourcesContent":["import type { LockedData } from '../locked-data'\nimport type { Logger } from '../logger'\nimport type { Step } from '../types'\nimport type { MotiaStream } from '../types-stream'\nimport { createTrace } from './create-trace'\nimport type { TracerFactory } from './index'\nimport { RedisTraceStreamAdapter } from './redis-trace-stream-adapter'\nimport { StreamTracer } from './stream-tracer'\nimport { TraceManager } from './trace-manager'\nimport type { Trace, TraceGroup } from './types'\n\nexport class BaseTracerFactory implements TracerFactory {\n constructor(\n private readonly traceStream: MotiaStream<Trace>,\n private readonly traceGroupStream: MotiaStream<TraceGroup>,\n ) {}\n\n private async getAllGroups() {\n return await this.traceGroupStream.getGroup('default')\n }\n\n private async deleteGroup(group: TraceGroup) {\n const traces = await this.traceStream.getGroup(group.id)\n\n for (const trace of traces) {\n await this.traceStream.delete(group.id, trace.id)\n }\n await this.traceGroupStream.delete('default', group.id)\n }\n\n async clear() {\n const groups = await this.getAllGroups()\n\n for (const group of groups) {\n await this.deleteGroup(group)\n }\n }\n\n async createTracer(traceId: string, step: Step, logger: Logger) {\n const traceGroup: TraceGroup = {\n id: traceId,\n name: step.config.name,\n lastActivity: Date.now(),\n metadata: {\n completedSteps: 0,\n activeSteps: 0,\n totalSteps: 0,\n },\n correlationId: undefined,\n status: 'running',\n startTime: Date.now(),\n }\n\n const trace = createTrace(traceGroup, step)\n const manager = new TraceManager(this.traceStream, this.traceGroupStream, traceGroup, trace)\n\n return new StreamTracer(manager, traceGroup, trace, logger)\n }\n\n async attachToTrace(traceId: string, step: Step, logger: Logger) {\n const existingGroup = await this.traceGroupStream.get('default', traceId)\n\n if (!existingGroup) {\n return this.createTracer(traceId, step, logger)\n }\n\n const trace = createTrace(existingGroup, step)\n const manager = new TraceManager(this.traceStream, this.traceGroupStream, existingGroup, trace)\n\n return new StreamTracer(manager, existingGroup, trace, logger)\n }\n}\n\nexport const createTracerFactory = (lockedData: LockedData): TracerFactory => {\n if (!lockedData.redisClient) {\n throw new Error(\n 'Redis client is required for tracer factory. Please provide a redisClient when creating LockedData.',\n )\n }\n\n const traceStreamName = 'motia-trace'\n const traceStreamAdapter: MotiaStream<Trace> = new RedisTraceStreamAdapter(traceStreamName, lockedData.redisClient)\n\n const traceStream = lockedData.createStream<Trace>({\n filePath: traceStreamName,\n hidden: true,\n config: {\n name: traceStreamName,\n baseConfig: { storageType: 'custom', factory: () => traceStreamAdapter },\n schema: null as never,\n },\n })()\n\n const traceGroupName = 'motia-trace-group'\n const traceGroupStreamAdapter: MotiaStream<TraceGroup> = new RedisTraceStreamAdapter(\n traceGroupName,\n lockedData.redisClient,\n )\n\n const traceGroupStream = lockedData.createStream<TraceGroup>({\n filePath: traceGroupName,\n hidden: true,\n config: {\n name: traceGroupName,\n baseConfig: { storageType: 'custom', factory: () => traceGroupStreamAdapter },\n schema: null as never,\n },\n })()\n\n return new BaseTracerFactory(traceStream, traceGroupStream)\n}\n"],"mappings":";;;;;;AAWA,IAAa,oBAAb,MAAwD;CACtD,YACE,AAAiBA,aACjB,AAAiBC,kBACjB;EAFiB;EACA;;CAGnB,MAAc,eAAe;AAC3B,SAAO,MAAM,KAAK,iBAAiB,SAAS,UAAU;;CAGxD,MAAc,YAAY,OAAmB;EAC3C,MAAM,SAAS,MAAM,KAAK,YAAY,SAAS,MAAM,GAAG;AAExD,OAAK,MAAM,SAAS,OAClB,OAAM,KAAK,YAAY,OAAO,MAAM,IAAI,MAAM,GAAG;AAEnD,QAAM,KAAK,iBAAiB,OAAO,WAAW,MAAM,GAAG;;CAGzD,MAAM,QAAQ;EACZ,MAAM,SAAS,MAAM,KAAK,cAAc;AAExC,OAAK,MAAM,SAAS,OAClB,OAAM,KAAK,YAAY,MAAM;;CAIjC,MAAM,aAAa,SAAiB,MAAY,QAAgB;EAC9D,MAAMC,aAAyB;GAC7B,IAAI;GACJ,MAAM,KAAK,OAAO;GAClB,cAAc,KAAK,KAAK;GACxB,UAAU;IACR,gBAAgB;IAChB,aAAa;IACb,YAAY;IACb;GACD,eAAe;GACf,QAAQ;GACR,WAAW,KAAK,KAAK;GACtB;EAED,MAAM,QAAQ,YAAY,YAAY,KAAK;AAG3C,SAAO,IAAI,aAFK,IAAI,aAAa,KAAK,aAAa,KAAK,kBAAkB,YAAY,MAAM,EAE3D,YAAY,OAAO,OAAO;;CAG7D,MAAM,cAAc,SAAiB,MAAY,QAAgB;EAC/D,MAAM,gBAAgB,MAAM,KAAK,iBAAiB,IAAI,WAAW,QAAQ;AAEzE,MAAI,CAAC,cACH,QAAO,KAAK,aAAa,SAAS,MAAM,OAAO;EAGjD,MAAM,QAAQ,YAAY,eAAe,KAAK;AAG9C,SAAO,IAAI,aAFK,IAAI,aAAa,KAAK,aAAa,KAAK,kBAAkB,eAAe,MAAM,EAE9D,eAAe,OAAO,OAAO;;;AAIlE,MAAa,uBAAuB,eAA0C;AAC5E,KAAI,CAAC,WAAW,YACd,OAAM,IAAI,MACR,sGACD;CAGH,MAAM,kBAAkB;CACxB,MAAMC,qBAAyC,IAAI,wBAAwB,iBAAiB,WAAW,YAAY;CAEnH,MAAM,cAAc,WAAW,aAAoB;EACjD,UAAU;EACV,QAAQ;EACR,QAAQ;GACN,MAAM;GACN,YAAY;IAAE,aAAa;IAAU,eAAe;IAAoB;GACxE,QAAQ;GACT;EACF,CAAC,EAAE;CAEJ,MAAM,iBAAiB;CACvB,MAAMC,0BAAmD,IAAI,wBAC3D,gBACA,WAAW,YACZ;AAYD,QAAO,IAAI,kBAAkB,aAVJ,WAAW,aAAyB;EAC3D,UAAU;EACV,QAAQ;EACR,QAAQ;GACN,MAAM;GACN,YAAY;IAAE,aAAa;IAAU,eAAe;IAAyB;GAC7E,QAAQ;GACT;EACF,CAAC,EAAE,CAEuD"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { globalLogger } from "./logger.mjs";
|
|
2
2
|
import { getRoom, sendAccessDenied, sendError } from "./socket-server/helpers.mjs";
|
|
3
|
-
import { WebSocketServer } from "ws";
|
|
3
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
4
4
|
|
|
5
5
|
//#region src/socket-server.ts
|
|
6
6
|
const AUTH_ERROR_CODE = 401;
|
|
@@ -37,9 +37,9 @@ const createSocketServer = ({ server, onJoin, onJoinGroup, authenticate, authori
|
|
|
37
37
|
};
|
|
38
38
|
socketServer.on("connection", async (socket, request) => {
|
|
39
39
|
authContexts.set(socket, request.authContext);
|
|
40
|
-
subscriptions.set(socket, /* @__PURE__ */ new
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
subscriptions.set(socket, /* @__PURE__ */ new Map());
|
|
41
|
+
let messageQueue = Promise.resolve();
|
|
42
|
+
const processMessage = async (message) => {
|
|
43
43
|
if (message.type === "join") {
|
|
44
44
|
if (!await isAuthorized(socket, message.data)) {
|
|
45
45
|
sendAccessDenied(socket, message.data);
|
|
@@ -78,14 +78,28 @@ const createSocketServer = ({ server, onJoin, onJoinGroup, authenticate, authori
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
rooms[room].set(message.data.subscriptionId, socket);
|
|
81
|
-
subscriptions.get(socket)?.
|
|
81
|
+
subscriptions.get(socket)?.set(message.data.subscriptionId, room);
|
|
82
82
|
} else if (message.type === "leave") {
|
|
83
|
+
if (!message.data.subscriptionId) {
|
|
84
|
+
globalLogger.error("[Socket Server] Subscription ID is required for leave message");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
83
87
|
const room = getRoom(message.data);
|
|
84
|
-
if (rooms[room])
|
|
88
|
+
if (rooms[room]) {
|
|
89
|
+
rooms[room].delete(message.data.subscriptionId);
|
|
90
|
+
if (rooms[room].size === 0) delete rooms[room];
|
|
91
|
+
}
|
|
92
|
+
subscriptions.get(socket)?.delete(message.data.subscriptionId);
|
|
85
93
|
}
|
|
94
|
+
};
|
|
95
|
+
socket.on("message", (payload) => {
|
|
96
|
+
const message = JSON.parse(payload.toString());
|
|
97
|
+
messageQueue = messageQueue.then(() => processMessage(message)).catch((error) => {
|
|
98
|
+
globalLogger.error("[Socket Server] Error processing message", error);
|
|
99
|
+
});
|
|
86
100
|
});
|
|
87
101
|
socket.on("close", () => {
|
|
88
|
-
subscriptions.get(socket)?.forEach((
|
|
102
|
+
subscriptions.get(socket)?.forEach((room, subscriptionId) => {
|
|
89
103
|
rooms[room]?.delete(subscriptionId);
|
|
90
104
|
if (rooms[room]?.size === 0) delete rooms[room];
|
|
91
105
|
});
|
|
@@ -103,18 +117,21 @@ const createSocketServer = ({ server, onJoin, onJoinGroup, authenticate, authori
|
|
|
103
117
|
timestamp: Date.now(),
|
|
104
118
|
...message
|
|
105
119
|
});
|
|
106
|
-
|
|
107
|
-
socket.
|
|
108
|
-
|
|
120
|
+
const safeSend = (socket) => {
|
|
121
|
+
if (socket.readyState === WebSocket.OPEN) try {
|
|
122
|
+
socket.send(eventMessage);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
globalLogger.debug("[Socket Server] Failed to send message to socket", error);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
if (rooms[groupRoom]) rooms[groupRoom].forEach(safeSend);
|
|
109
128
|
if (id) {
|
|
110
129
|
const itemRoom = getRoom({
|
|
111
130
|
groupId,
|
|
112
131
|
streamName,
|
|
113
132
|
id
|
|
114
133
|
});
|
|
115
|
-
if (rooms[itemRoom]) rooms[itemRoom].forEach(
|
|
116
|
-
socket.send(eventMessage);
|
|
117
|
-
});
|
|
134
|
+
if (rooms[itemRoom]) rooms[itemRoom].forEach(safeSend);
|
|
118
135
|
}
|
|
119
136
|
};
|
|
120
137
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket-server.mjs","names":["authRequest: StreamAuthRequest","rooms: Record<string, Map<string, WebSocket>>","subscriptions: Map<WebSocket,
|
|
1
|
+
{"version":3,"file":"socket-server.mjs","names":["authRequest: StreamAuthRequest","rooms: Record<string, Map<string, WebSocket>>","subscriptions: Map<WebSocket, Map<string, string>>","authContexts: Map<WebSocket, unknown>","messageQueue: Promise<void>","resultMessage: EventMessage<typeof item>","resultMessage: EventMessage<typeof items>","message: Message"],"sources":["../../src/socket-server.ts"],"sourcesContent":["import type { Server } from 'http'\nimport { WebSocket, WebSocketServer } from 'ws'\nimport { globalLogger } from './logger'\nimport {\n type BaseMessage,\n type EventMessage,\n getRoom,\n type JoinMessage,\n sendAccessDenied,\n sendError,\n} from './socket-server/helpers'\nimport type { StreamAuthRequest } from './types/app-config-types'\n\ntype Message = { type: 'join' | 'leave'; data: JoinMessage }\n\ntype Props = {\n server: Server\n onJoin: <TData>(streamName: string, groupId: string, id: string) => Promise<TData>\n onJoinGroup: <TData>(streamName: string, groupId: string) => Promise<TData[] | undefined>\n authenticate?: (request: StreamAuthRequest) => Promise<unknown | null> | unknown | null\n authorize?: (\n subscription: { streamName: string; groupId: string; id?: string },\n authContext?: unknown,\n ) => Promise<boolean> | boolean\n}\n\nconst AUTH_ERROR_CODE = 401\nexport const createSocketServer = ({ server, onJoin, onJoinGroup, authenticate, authorize }: Props) => {\n const socketServer = new WebSocketServer({\n server,\n verifyClient: async (info, callback) => {\n if (authenticate) {\n try {\n const authRequest: StreamAuthRequest = {\n headers: info.req.headers,\n url: info.req.url,\n }\n info.req.authContext = await authenticate(authRequest)\n callback(true)\n } catch {\n globalLogger.debug('[Socket Server] Authentication failed')\n callback(false, AUTH_ERROR_CODE, 'Authentication failed')\n }\n } else {\n callback(true)\n }\n },\n })\n const rooms: Record<string, Map<string, WebSocket>> = {}\n const subscriptions: Map<WebSocket, Map<string, string>> = new Map()\n const authContexts: Map<WebSocket, unknown> = new Map()\n\n const isAuthorized = async (socket: WebSocket, data: BaseMessage): Promise<boolean> => {\n if (!authorize) {\n return true\n }\n\n try {\n const authContext = authContexts.get(socket)\n const result = await authorize(data, authContext)\n return result !== false\n } catch (error) {\n sendError(socket, data, error as Error)\n globalLogger.error('[Socket Server] Failed to authorize stream subscription')\n return false\n }\n }\n\n socketServer.on('connection', async (socket, request) => {\n authContexts.set(socket, request.authContext)\n\n subscriptions.set(socket, new Map())\n\n // Message queue to ensure messages are processed in order\n // This prevents race conditions where async join handlers allow leave to overtake join\n let messageQueue: Promise<void> = Promise.resolve()\n\n const processMessage = async (message: Message) => {\n if (message.type === 'join') {\n const authorized = await isAuthorized(socket, message.data)\n\n if (!authorized) {\n sendAccessDenied(socket, message.data)\n return\n }\n\n const room = getRoom(message.data)\n\n if (!rooms[room]) {\n rooms[room] = new Map()\n }\n\n if (message.data.id) {\n const item = await onJoin(message.data.streamName, message.data.groupId, message.data.id)\n\n if (item) {\n const resultMessage: EventMessage<typeof item> = {\n timestamp: Date.now(),\n streamName: message.data.streamName,\n groupId: message.data.groupId,\n id: message.data.id,\n event: { type: 'sync', data: item },\n }\n\n socket.send(JSON.stringify(resultMessage))\n }\n } else {\n const items = await onJoinGroup(message.data.streamName, message.data.groupId)\n\n if (items) {\n const resultMessage: EventMessage<typeof items> = {\n timestamp: Date.now(),\n streamName: message.data.streamName,\n groupId: message.data.groupId,\n event: { type: 'sync', data: items },\n }\n\n socket.send(JSON.stringify(resultMessage))\n }\n }\n\n rooms[room].set(message.data.subscriptionId, socket)\n subscriptions.get(socket)?.set(message.data.subscriptionId, room)\n } else if (message.type === 'leave') {\n if (!message.data.subscriptionId) {\n globalLogger.error('[Socket Server] Subscription ID is required for leave message')\n return\n }\n\n const room = getRoom(message.data)\n if (rooms[room]) {\n rooms[room].delete(message.data.subscriptionId)\n if (rooms[room].size === 0) {\n delete rooms[room]\n }\n }\n\n subscriptions.get(socket)?.delete(message.data.subscriptionId)\n }\n }\n\n socket.on('message', (payload: Buffer) => {\n const message: Message = JSON.parse(payload.toString())\n // Chain messages to ensure they are processed in order\n messageQueue = messageQueue\n .then(() => processMessage(message))\n .catch((error) => {\n globalLogger.error('[Socket Server] Error processing message', error)\n })\n })\n\n socket.on('close', () => {\n subscriptions.get(socket)?.forEach((room, subscriptionId) => {\n rooms[room]?.delete(subscriptionId)\n\n if (rooms[room]?.size === 0) {\n delete rooms[room]\n }\n })\n subscriptions.delete(socket)\n authContexts.delete(socket)\n })\n })\n\n const pushEvent = <TData>(message: Omit<EventMessage<TData>, 'timestamp'>) => {\n const { groupId, streamName, id } = message\n const groupRoom = getRoom({ streamName, groupId })\n const eventMessage = JSON.stringify({ timestamp: Date.now(), ...message })\n\n const safeSend = (socket: WebSocket) => {\n if (socket.readyState === WebSocket.OPEN) {\n try {\n socket.send(eventMessage)\n } catch (error) {\n globalLogger.debug('[Socket Server] Failed to send message to socket', error)\n }\n }\n }\n\n if (rooms[groupRoom]) {\n rooms[groupRoom].forEach(safeSend)\n }\n\n if (id) {\n const itemRoom = getRoom({ groupId, streamName, id })\n\n if (rooms[itemRoom]) {\n rooms[itemRoom].forEach(safeSend)\n }\n }\n }\n\n return { pushEvent, socketServer }\n}\n"],"mappings":";;;;;AA0BA,MAAM,kBAAkB;AACxB,MAAa,sBAAsB,EAAE,QAAQ,QAAQ,aAAa,cAAc,gBAAuB;CACrG,MAAM,eAAe,IAAI,gBAAgB;EACvC;EACA,cAAc,OAAO,MAAM,aAAa;AACtC,OAAI,aACF,KAAI;IACF,MAAMA,cAAiC;KACrC,SAAS,KAAK,IAAI;KAClB,KAAK,KAAK,IAAI;KACf;AACD,SAAK,IAAI,cAAc,MAAM,aAAa,YAAY;AACtD,aAAS,KAAK;WACR;AACN,iBAAa,MAAM,wCAAwC;AAC3D,aAAS,OAAO,iBAAiB,wBAAwB;;OAG3D,UAAS,KAAK;;EAGnB,CAAC;CACF,MAAMC,QAAgD,EAAE;CACxD,MAAMC,gCAAqD,IAAI,KAAK;CACpE,MAAMC,+BAAwC,IAAI,KAAK;CAEvD,MAAM,eAAe,OAAO,QAAmB,SAAwC;AACrF,MAAI,CAAC,UACH,QAAO;AAGT,MAAI;AAGF,UADe,MAAM,UAAU,MADX,aAAa,IAAI,OAAO,CACK,KAC/B;WACX,OAAO;AACd,aAAU,QAAQ,MAAM,MAAe;AACvC,gBAAa,MAAM,0DAA0D;AAC7E,UAAO;;;AAIX,cAAa,GAAG,cAAc,OAAO,QAAQ,YAAY;AACvD,eAAa,IAAI,QAAQ,QAAQ,YAAY;AAE7C,gBAAc,IAAI,wBAAQ,IAAI,KAAK,CAAC;EAIpC,IAAIC,eAA8B,QAAQ,SAAS;EAEnD,MAAM,iBAAiB,OAAO,YAAqB;AACjD,OAAI,QAAQ,SAAS,QAAQ;AAG3B,QAAI,CAFe,MAAM,aAAa,QAAQ,QAAQ,KAAK,EAE1C;AACf,sBAAiB,QAAQ,QAAQ,KAAK;AACtC;;IAGF,MAAM,OAAO,QAAQ,QAAQ,KAAK;AAElC,QAAI,CAAC,MAAM,MACT,OAAM,wBAAQ,IAAI,KAAK;AAGzB,QAAI,QAAQ,KAAK,IAAI;KACnB,MAAM,OAAO,MAAM,OAAO,QAAQ,KAAK,YAAY,QAAQ,KAAK,SAAS,QAAQ,KAAK,GAAG;AAEzF,SAAI,MAAM;MACR,MAAMC,gBAA2C;OAC/C,WAAW,KAAK,KAAK;OACrB,YAAY,QAAQ,KAAK;OACzB,SAAS,QAAQ,KAAK;OACtB,IAAI,QAAQ,KAAK;OACjB,OAAO;QAAE,MAAM;QAAQ,MAAM;QAAM;OACpC;AAED,aAAO,KAAK,KAAK,UAAU,cAAc,CAAC;;WAEvC;KACL,MAAM,QAAQ,MAAM,YAAY,QAAQ,KAAK,YAAY,QAAQ,KAAK,QAAQ;AAE9E,SAAI,OAAO;MACT,MAAMC,gBAA4C;OAChD,WAAW,KAAK,KAAK;OACrB,YAAY,QAAQ,KAAK;OACzB,SAAS,QAAQ,KAAK;OACtB,OAAO;QAAE,MAAM;QAAQ,MAAM;QAAO;OACrC;AAED,aAAO,KAAK,KAAK,UAAU,cAAc,CAAC;;;AAI9C,UAAM,MAAM,IAAI,QAAQ,KAAK,gBAAgB,OAAO;AACpD,kBAAc,IAAI,OAAO,EAAE,IAAI,QAAQ,KAAK,gBAAgB,KAAK;cACxD,QAAQ,SAAS,SAAS;AACnC,QAAI,CAAC,QAAQ,KAAK,gBAAgB;AAChC,kBAAa,MAAM,gEAAgE;AACnF;;IAGF,MAAM,OAAO,QAAQ,QAAQ,KAAK;AAClC,QAAI,MAAM,OAAO;AACf,WAAM,MAAM,OAAO,QAAQ,KAAK,eAAe;AAC/C,SAAI,MAAM,MAAM,SAAS,EACvB,QAAO,MAAM;;AAIjB,kBAAc,IAAI,OAAO,EAAE,OAAO,QAAQ,KAAK,eAAe;;;AAIlE,SAAO,GAAG,YAAY,YAAoB;GACxC,MAAMC,UAAmB,KAAK,MAAM,QAAQ,UAAU,CAAC;AAEvD,kBAAe,aACZ,WAAW,eAAe,QAAQ,CAAC,CACnC,OAAO,UAAU;AAChB,iBAAa,MAAM,4CAA4C,MAAM;KACrE;IACJ;AAEF,SAAO,GAAG,eAAe;AACvB,iBAAc,IAAI,OAAO,EAAE,SAAS,MAAM,mBAAmB;AAC3D,UAAM,OAAO,OAAO,eAAe;AAEnC,QAAI,MAAM,OAAO,SAAS,EACxB,QAAO,MAAM;KAEf;AACF,iBAAc,OAAO,OAAO;AAC5B,gBAAa,OAAO,OAAO;IAC3B;GACF;CAEF,MAAM,aAAoB,YAAoD;EAC5E,MAAM,EAAE,SAAS,YAAY,OAAO;EACpC,MAAM,YAAY,QAAQ;GAAE;GAAY;GAAS,CAAC;EAClD,MAAM,eAAe,KAAK,UAAU;GAAE,WAAW,KAAK,KAAK;GAAE,GAAG;GAAS,CAAC;EAE1E,MAAM,YAAY,WAAsB;AACtC,OAAI,OAAO,eAAe,UAAU,KAClC,KAAI;AACF,WAAO,KAAK,aAAa;YAClB,OAAO;AACd,iBAAa,MAAM,oDAAoD,MAAM;;;AAKnF,MAAI,MAAM,WACR,OAAM,WAAW,QAAQ,SAAS;AAGpC,MAAI,IAAI;GACN,MAAM,WAAW,QAAQ;IAAE;IAAS;IAAY;IAAI,CAAC;AAErD,OAAI,MAAM,UACR,OAAM,UAAU,QAAQ,SAAS;;;AAKvC,QAAO;EAAE;EAAW;EAAc"}
|