@torkbot/sledge 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Geoff Goodman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,251 @@
1
+ # @torkbot/sledge
2
+
3
+ A SQLite-backed event and work engine for building durable, restart-safe workflows.
4
+
5
+ If you need to reliably turn events into background work (without losing consistency during crashes/retries), this package gives you the core runtime.
6
+
7
+ ## Who this is for
8
+
9
+ Use `@torkbot/sledge` when you want:
10
+
11
+ - durable event append + background work orchestration,
12
+ - retries and restart recovery by default,
13
+ - strong runtime validation at I/O boundaries,
14
+ - a small API surface you can adapt to your own schema and storage layout.
15
+
16
+ ## What you get
17
+
18
+ - **Event log** (`events` table)
19
+ - **Durable work queue** (`work` table)
20
+ - **Transactional flow:** append event -> project -> materialize work in one transaction
21
+ - **Lease-based execution** for queue handlers
22
+ - **Idempotent producer retries** via `dedupeKey`
23
+ - **Configurable contention behavior** (`maxBusyRetries`, `maxBusyRetryDelayMs`)
24
+
25
+ ## Example use-cases
26
+
27
+ - **Webhook ingestion** with producer idempotency (`dedupeKey`) and reliable downstream processing
28
+ - **Notification pipelines** (email/push/slack) with retries and dead-letter outcomes
29
+ - **Long-running tool/API jobs** that survive worker restarts
30
+ - **Outbox-style orchestration** without split-brain between writes and job enqueue
31
+
32
+ ---
33
+
34
+ ## Quick start (copy/paste)
35
+
36
+ ```ts
37
+ import Database from "better-sqlite3";
38
+ import { Type } from "@sinclair/typebox";
39
+
40
+ import { defineLedgerModel } from "@torkbot/sledge/ledger";
41
+ import { createBetterSqliteLedger } from "@torkbot/sledge/better-sqlite3-ledger";
42
+ import {
43
+ NodeRuntimeScheduler,
44
+ SystemRuntimeClock,
45
+ } from "@torkbot/sledge/runtime/node-runtime";
46
+
47
+ const model = defineLedgerModel({
48
+ events: {
49
+ "user.created": Type.Object({
50
+ userId: Type.String(),
51
+ email: Type.String(),
52
+ }),
53
+ },
54
+
55
+ queues: {
56
+ "welcome-email.send": Type.Object({
57
+ userId: Type.String(),
58
+ email: Type.String(),
59
+ }),
60
+ },
61
+
62
+ indexers: {
63
+ upsertUser: Type.Object({
64
+ userId: Type.String(),
65
+ email: Type.String(),
66
+ }),
67
+ },
68
+
69
+ queries: {
70
+ userById: {
71
+ params: Type.Object({ userId: Type.String() }),
72
+ result: Type.Union([
73
+ Type.Null(),
74
+ Type.Object({
75
+ userId: Type.String(),
76
+ email: Type.String(),
77
+ }),
78
+ ]),
79
+ },
80
+ },
81
+
82
+ register(builder) {
83
+ // Event -> projection
84
+ builder.project("user.created", async ({ event, actions }) => {
85
+ await actions.index("upsertUser", {
86
+ userId: event.payload.userId,
87
+ email: event.payload.email,
88
+ });
89
+ });
90
+
91
+ // Event -> queued work
92
+ builder.materialize("user.created", ({ event, actions }) => {
93
+ actions.enqueue("welcome-email.send", {
94
+ userId: event.payload.userId,
95
+ email: event.payload.email,
96
+ });
97
+ });
98
+
99
+ // Queue handler
100
+ builder.handle("welcome-email.send", async ({ work }) => {
101
+ // call provider here
102
+ console.log("sending welcome email", work.payload.email);
103
+
104
+ return { outcome: "ack" } as const;
105
+ });
106
+ },
107
+ });
108
+
109
+ const db = new Database("./app.sqlite");
110
+ const clock = new SystemRuntimeClock();
111
+ const scheduler = new NodeRuntimeScheduler();
112
+
113
+ const ledger = createBetterSqliteLedger({
114
+ database: db,
115
+ boundModel: model.bind({
116
+ indexers: {
117
+ upsertUser: async (input) => {
118
+ // Write to your own projection table(s)
119
+ },
120
+ },
121
+ queries: {
122
+ userById: async () => {
123
+ // Read from your own projection table(s)
124
+ return null;
125
+ },
126
+ },
127
+ }),
128
+ timing: {
129
+ clock,
130
+ scheduler,
131
+ },
132
+ });
133
+
134
+ await ledger.emit("user.created", {
135
+ userId: "u_123",
136
+ email: "alice@example.com",
137
+ });
138
+
139
+ await ledger.close();
140
+ ```
141
+
142
+ ---
143
+
144
+ ## How to think about the API
145
+
146
+ ### 1) `defineLedgerModel(...)`
147
+
148
+ You define contracts, not implementation details:
149
+
150
+ - `events`: facts appended to the event stream
151
+ - `queues`: durable work payloads
152
+ - `indexers`: projection write contracts
153
+ - `queries`: projection read contracts
154
+ - `register(builder)`: orchestration glue
155
+
156
+ ### 2) `model.bind(...)`
157
+
158
+ You provide concrete implementations for indexers and queries.
159
+
160
+ ### 3) `create*Ledger(...)`
161
+
162
+ You choose backend adapter and start the runtime:
163
+
164
+ - `createBetterSqliteLedger(...)`
165
+ - `createTursoLedger(...)`
166
+
167
+ The runtime exposes:
168
+
169
+ - `emit(eventName, payload, options?)`
170
+ - `query(queryName, params)`
171
+ - `close()`
172
+
173
+ ---
174
+
175
+ ## Handler outcomes
176
+
177
+ Queue handlers must return one of:
178
+
179
+ - `{ outcome: "ack" }`
180
+ - `{ outcome: "retry", error, retryAtMs? }`
181
+ - `{ outcome: "dead_letter", error }`
182
+
183
+ If a handler throws, the runtime treats it as a retry.
184
+
185
+ ## Dedupe and idempotency
186
+
187
+ Use `dedupeKey` in `emit(...)` for producer retries.
188
+
189
+ ```ts
190
+ await ledger.emit(
191
+ "user.created",
192
+ { userId: "u_123", email: "alice@example.com" },
193
+ { dedupeKey: "provider-event:abc-123" },
194
+ );
195
+ ```
196
+
197
+ Same key => same durable event winner, no duplicate downstream materialization.
198
+
199
+ ## Long-running handlers
200
+
201
+ For long operations, keep the lease alive while working:
202
+
203
+ ```ts
204
+ builder.handle("some.queue", async ({ lease }) => {
205
+ await using hold = lease.hold();
206
+
207
+ // long-running async work
208
+
209
+ return { outcome: "ack" } as const;
210
+ });
211
+ ```
212
+
213
+ ## Runtime tuning knobs
214
+
215
+ Available options when creating a ledger:
216
+
217
+ - `leaseMs`
218
+ - `defaultRetryDelayMs`
219
+ - `maxInFlight`
220
+ - `maxBusyRetries`
221
+ - `maxBusyRetryDelayMs`
222
+
223
+ Start simple; tune only when you observe contention/throughput issues.
224
+
225
+ ---
226
+
227
+ ## Package exports
228
+
229
+ - `@torkbot/sledge/ledger`
230
+ - `@torkbot/sledge/database-ledger-engine`
231
+ - `@torkbot/sledge/better-sqlite3-ledger`
232
+ - `@torkbot/sledge/turso-ledger`
233
+ - `@torkbot/sledge/runtime/contracts`
234
+ - `@torkbot/sledge/runtime/node-runtime`
235
+ - `@torkbot/sledge/runtime/virtual-runtime`
236
+
237
+ ## Development
238
+
239
+ ```bash
240
+ node --run typecheck
241
+ node --run test
242
+ node --run build
243
+ node --run lint
244
+ ```
245
+
246
+ ## Publishing notes
247
+
248
+ - The package is published as compiled JavaScript in `dist/` (with `.d.ts` types).
249
+ - Source remains strict TypeScript in `src/`.
250
+ - `prepublishOnly` runs `node --run build` automatically.
251
+ - Node version is pinned via `engines.node` because runtime code uses explicit resource management (`using` / `await using`).
@@ -0,0 +1,18 @@
1
+ import type Database from "better-sqlite3";
2
+ import type { TSchema } from "@sinclair/typebox";
3
+ import type { Ledger, BoundLedgerModel, LedgerTiming, QuerySchema } from "./ledger.ts";
4
+ type AnyIndexerDef = TSchema;
5
+ type AnyQueryDef = QuerySchema<TSchema, TSchema>;
6
+ type CreateBetterSqliteLedgerInput<TEvents extends Record<string, TSchema>, TQueues extends Record<string, TSchema>, TIndexers extends Record<string, AnyIndexerDef>, TQueries extends Record<string, AnyQueryDef>> = {
7
+ readonly database: Database.Database;
8
+ readonly boundModel: BoundLedgerModel<TEvents, TQueues, TIndexers, TQueries>;
9
+ readonly timing: LedgerTiming;
10
+ readonly leaseMs?: number;
11
+ readonly defaultRetryDelayMs?: number;
12
+ readonly maxInFlight?: number;
13
+ readonly maxBusyRetries?: number;
14
+ readonly maxBusyRetryDelayMs?: number;
15
+ };
16
+ export declare function createBetterSqliteLedger<const TEvents extends Record<string, TSchema>, const TQueues extends Record<string, TSchema>, const TIndexers extends Record<string, AnyIndexerDef>, const TQueries extends Record<string, AnyQueryDef>>(input: CreateBetterSqliteLedgerInput<TEvents, TQueues, TIndexers, TQueries>): Ledger<TEvents, TQueries>;
17
+ export {};
18
+ //# sourceMappingURL=better-sqlite3-ledger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"better-sqlite3-ledger.d.ts","sourceRoot":"","sources":["../../src/ledger/better-sqlite3-ledger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,KAAK,EACV,MAAM,EACN,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACZ,MAAM,aAAa,CAAC;AAOrB,KAAK,aAAa,GAAG,OAAO,CAAC;AAC7B,KAAK,WAAW,GAAG,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAEjD,KAAK,6BAA6B,CAChC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,EAC/C,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,IAC1C;IACF,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC;IACrC,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC7E,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACvC,CAAC;AAEF,wBAAgB,wBAAwB,CACtC,KAAK,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7C,KAAK,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7C,KAAK,CAAC,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,EACrD,KAAK,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EAElD,KAAK,EAAE,6BAA6B,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC,GAC1E,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,CAkB3B"}
@@ -0,0 +1,50 @@
1
+ import { createDatabaseLedger, } from "./database-ledger-engine.js";
2
+ export function createBetterSqliteLedger(input) {
3
+ const sharedInput = {
4
+ database: wrapBetterSqliteDatabase(input.database),
5
+ boundModel: input.boundModel,
6
+ timing: input.timing,
7
+ leaseMs: input.leaseMs,
8
+ defaultRetryDelayMs: input.defaultRetryDelayMs,
9
+ maxInFlight: input.maxInFlight,
10
+ maxBusyRetries: input.maxBusyRetries,
11
+ maxBusyRetryDelayMs: input.maxBusyRetryDelayMs,
12
+ };
13
+ return createDatabaseLedger(sharedInput);
14
+ }
15
+ function wrapBetterSqliteDatabase(database) {
16
+ return {
17
+ exec: async (sql) => {
18
+ database.exec(sql);
19
+ },
20
+ prepare: (sql) => {
21
+ const statement = database.prepare(sql);
22
+ return {
23
+ run: async (...params) => statement.run(...params),
24
+ get: async (...params) => {
25
+ const row = statement.get(...params);
26
+ if (row === undefined) {
27
+ return undefined;
28
+ }
29
+ if (typeof row !== "object" || row === null || Array.isArray(row)) {
30
+ throw new Error("expected row object from better-sqlite statement.get");
31
+ }
32
+ return row;
33
+ },
34
+ all: async (...params) => {
35
+ const rows = statement.all(...params);
36
+ return rows.map((row) => {
37
+ if (typeof row !== "object" || row === null || Array.isArray(row)) {
38
+ throw new Error("expected row object from better-sqlite statement.all");
39
+ }
40
+ return row;
41
+ });
42
+ },
43
+ };
44
+ },
45
+ close: async () => {
46
+ database.close();
47
+ },
48
+ };
49
+ }
50
+ //# sourceMappingURL=better-sqlite3-ledger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"better-sqlite3-ledger.js","sourceRoot":"","sources":["../../src/ledger/better-sqlite3-ledger.ts"],"names":[],"mappings":"AASA,OAAO,EACL,oBAAoB,GAGrB,MAAM,6BAA6B,CAAC;AAqBrC,MAAM,UAAU,wBAAwB,CAMtC,KAA2E;IAE3E,MAAM,WAAW,GAKb;QACF,QAAQ,EAAE,wBAAwB,CAAC,KAAK,CAAC,QAAQ,CAAC;QAClD,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,mBAAmB,EAAE,KAAK,CAAC,mBAAmB;QAC9C,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,cAAc,EAAE,KAAK,CAAC,cAAc;QACpC,mBAAmB,EAAE,KAAK,CAAC,mBAAmB;KAC/C,CAAC;IAEF,OAAO,oBAAoB,CAAC,WAAW,CAAC,CAAC;AAC3C,CAAC;AAED,SAAS,wBAAwB,CAC/B,QAA2B;IAE3B,OAAO;QACL,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YAClB,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC;QACD,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACf,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAExC,OAAO;gBACL,GAAG,EAAE,KAAK,EAAE,GAAG,MAAM,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC;gBAClD,GAAG,EAAE,KAAK,EAAE,GAAG,MAAM,EAAE,EAAE;oBACvB,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC;oBAErC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;wBACtB,OAAO,SAAS,CAAC;oBACnB,CAAC;oBAED,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;wBAClE,MAAM,IAAI,KAAK,CACb,sDAAsD,CACvD,CAAC;oBACJ,CAAC;oBAED,OAAO,GAA8B,CAAC;gBACxC,CAAC;gBACD,GAAG,EAAE,KAAK,EAAE,GAAG,MAAM,EAAE,EAAE;oBACvB,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC;oBAEtC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;wBACtB,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;4BAClE,MAAM,IAAI,KAAK,CACb,sDAAsD,CACvD,CAAC;wBACJ,CAAC;wBAED,OAAO,GAA8B,CAAC;oBACxC,CAAC,CAAC,CAAC;gBACL,CAAC;aACF,CAAC;QACJ,CAAC;QACD,KAAK,EAAE,KAAK,IAAI,EAAE;YAChB,QAAQ,CAAC,KAAK,EAAE,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { TSchema } from "@sinclair/typebox";
2
+ import type { BoundLedgerModel, Ledger, LedgerTiming, QuerySchema } from "./ledger.ts";
3
+ type AnyIndexerDef = TSchema;
4
+ type AnyQueryDef = QuerySchema<TSchema, TSchema>;
5
+ type StorageRow = Record<string, unknown>;
6
+ export interface StorageStatement {
7
+ run(...params: unknown[]): Promise<{
8
+ readonly changes: number;
9
+ readonly lastInsertRowid: number | bigint;
10
+ }>;
11
+ get(...params: unknown[]): Promise<StorageRow | undefined>;
12
+ all(...params: unknown[]): Promise<readonly StorageRow[]>;
13
+ }
14
+ export interface StorageDatabase {
15
+ exec(sql: string): Promise<void>;
16
+ prepare(sql: string): StorageStatement;
17
+ close(): Promise<void>;
18
+ }
19
+ export type CreateDatabaseLedgerInput<TEvents extends Record<string, TSchema>, TQueues extends Record<string, TSchema>, TIndexers extends Record<string, AnyIndexerDef>, TQueries extends Record<string, AnyQueryDef>> = {
20
+ readonly database: StorageDatabase;
21
+ readonly boundModel: BoundLedgerModel<TEvents, TQueues, TIndexers, TQueries>;
22
+ readonly timing: LedgerTiming;
23
+ readonly leaseMs?: number;
24
+ readonly defaultRetryDelayMs?: number;
25
+ readonly maxInFlight?: number;
26
+ readonly maxBusyRetries?: number;
27
+ readonly maxBusyRetryDelayMs?: number;
28
+ };
29
+ export declare function createDatabaseLedger<const TEvents extends Record<string, TSchema>, const TQueues extends Record<string, TSchema>, const TIndexers extends Record<string, AnyIndexerDef>, const TQueries extends Record<string, AnyQueryDef>>(input: CreateDatabaseLedgerInput<TEvents, TQueues, TIndexers, TQueries>): Ledger<TEvents, TQueries>;
30
+ export {};
31
+ //# sourceMappingURL=database-ledger-engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database-ledger-engine.d.ts","sourceRoot":"","sources":["../../src/ledger/database-ledger-engine.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAU,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,KAAK,EACV,gBAAgB,EAEhB,MAAM,EAEN,YAAY,EAGZ,WAAW,EAOZ,MAAM,aAAa,CAAC;AAErB,KAAK,aAAa,GAAG,OAAO,CAAC;AAC7B,KAAK,WAAW,GAAG,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAqBjD,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE1C,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAAC;KAC3C,CAAC,CAAC;IAEH,GAAG,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC;IAE3D,GAAG,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,SAAS,UAAU,EAAE,CAAC,CAAC;CAC3D;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAAC;IAEvC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAkBD,MAAM,MAAM,yBAAyB,CACnC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,EAC/C,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,IAC1C;IACF,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC7E,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACvC,CAAC;AAEF,wBAAgB,oBAAoB,CAClC,KAAK,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7C,KAAK,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7C,KAAK,CAAC,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,EACrD,KAAK,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EAElD,KAAK,EAAE,yBAAyB,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC,GACtE,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,CAW3B"}