@nxtedition/cache 2.1.13 → 2.1.15

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/README.md CHANGED
@@ -1,13 +1,14 @@
1
1
  # @nxtedition/cache
2
2
 
3
- A two-tier async cache with SQLite persistence, in-memory pseudo-LRU, stale-while-revalidate, cross-process/thread deduplication, and automatic request coalescing.
3
+ A two-tier async cache with SQLite persistence, in-memory pseudo-LRU, stale-while-revalidate, cross-thread deduplication via `SharedArrayBuffer`, and automatic request coalescing.
4
4
 
5
5
  ## Features
6
6
 
7
7
  - **Two-tier storage** — In-memory cache backed by SQLite on disk
8
+ - **File-sharded SQLite** — Keys are hash-routed across N independent SQLite files, bypassing SQLite's per-file writer serialization. ~40% higher throughput under multi-thread write contention (see [Benchmarks](#benchmarks)).
8
9
  - **Stale-while-revalidate** — Serve stale data synchronously while refreshing in the background
9
- - **Request coalescing** — Concurrent fetches for the same key share a single in-flight request
10
- - **Cross-process/thread locking** — SQLite-based distributed locks with `Atomics`-based wakeup prevent redundant work across processes and worker threads
10
+ - **Request coalescing** — Concurrent fetches for the same key share a single in-flight `Promise`
11
+ - **Cross-thread locking** — `SharedArrayBuffer` + `Atomics.compareExchange` / `Atomics.waitAsync` prevent redundant `valueSelector` calls across worker threads in the same process that share the same `location`
11
12
  - **Async value resolution** — Transparently fetches missing values via a user-defined `valueSelector`
12
13
  - **Binary support** — Store and retrieve `Buffer` / `Uint8Array` alongside JSON values
13
14
  - **Size-bounded storage** — Configurable max database size with automatic eviction of oldest entries
@@ -67,14 +68,14 @@ if (result.async) {
67
68
 
68
69
  #### `CacheOptions`
69
70
 
70
- | Option | Type | Default | Description |
71
- | ------------ | ---------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------ |
72
- | `ttl` | `number \| (value, key) => number` | `MAX_SAFE_INTEGER` | Time-to-live in milliseconds. After this, the entry is stale. |
73
- | `stale` | `number \| (value, key) => number` | `MAX_SAFE_INTEGER` | Stale-while-revalidate window in ms. After `ttl + stale`, the entry is purged. |
74
- | `memory` | `MemoryOptions \| false \| null` | `{ maxSize: 16MB, maxCount: 16384 }` | In-memory cache config, or `false`/`null` to disable. |
75
- | `database` | `DatabaseOptions \| false \| null` | `{ timeout: 20, maxSize: 128MB }` | SQLite config, or `false`/`null` to disable persistence. |
76
- | `lock` | `LockOptions \| false \| null` | `{ minTimeout: 1, maxTimeout: 1000 }` | Cross-process/thread lock config, or `false`/`null` to disable. |
77
- | `serializer` | `Serializer<V>` | JSON + ArrayBufferView passthrough | Custom `{ serialize, deserialize }` for value encoding. |
71
+ | Option | Type | Default | Description |
72
+ | ------------ | ---------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
73
+ | `ttl` | `number \| (value, key) => number` | `MAX_SAFE_INTEGER` | Time-to-live in milliseconds. After this, the entry is stale. |
74
+ | `stale` | `number \| (value, key) => number` | `MAX_SAFE_INTEGER` | Stale-while-revalidate window in ms. After `ttl + stale`, the entry is purged. |
75
+ | `memory` | `MemoryOptions \| false \| null` | `{ maxSize: 16MB, maxCount: 16384 }` | In-memory cache config, or `false`/`null` to disable. |
76
+ | `database` | `DatabaseOptions \| false \| null` | `{ timeout: 20, maxSize: 128MB }` | SQLite config, or `false`/`null` to disable persistence. |
77
+ | `lock` | `false \| null` | enabled | Pass `false`/`null` to disable cross-thread SAB locking (see [Cross-Thread Locking](#cross-thread-locking)). Always off for `':memory:'`. |
78
+ | `serializer` | `Serializer<V>` | JSON + ArrayBufferView passthrough | Custom `{ serialize, deserialize }` for value encoding. |
78
79
 
79
80
  #### `MemoryOptions`
80
81
 
@@ -87,19 +88,11 @@ The in-memory tier uses a random-two-choice eviction strategy: when the cache is
87
88
 
88
89
  #### `DatabaseOptions`
89
90
 
90
- | Option | Type | Default | Description |
91
- | --------- | -------- | ---------------------------- | ----------------------------------------------------------------- |
92
- | `timeout` | `number` | `20` | SQLite busy timeout in milliseconds. |
93
- | `maxSize` | `number` | `128 * 1024 * 1024` (128 MB) | Maximum database file size. Oldest entries are evicted when full. |
94
-
95
- #### `LockOptions`
96
-
97
- Cross-process/thread locking prevents multiple processes or worker threads from computing the same value simultaneously. The lock timeout is adaptive — it uses an exponential moving average (EMA) of `valueSelector` durations to estimate how long to wait before taking over a lock.
98
-
99
- | Option | Type | Default | Description |
100
- | ------------ | -------- | ------- | -------------------------------------------------------------------------- |
101
- | `minTimeout` | `number` | `1` | Minimum lock timeout in ms. Also the starting timeout before EMA warms up. |
102
- | `maxTimeout` | `number` | `1000` | Maximum lock timeout in ms. Caps the EMA-derived timeout. |
91
+ | Option | Type | Default | Description |
92
+ | --------- | -------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
93
+ | `timeout` | `number` | `20` | SQLite busy timeout in milliseconds. |
94
+ | `maxSize` | `number` | `128 * 1024 * 1024` (128 MB) | Maximum total database size across all shards. Divided evenly per shard. Oldest entries are evicted when a shard is full. |
95
+ | `shards` | `number` | `4` | Number of SQLite files (shards) the cache is spread across. See [File Sharding](#file-sharding). Use `1` for single-file. |
103
96
 
104
97
  #### `Serializer<V>`
105
98
 
@@ -149,7 +142,7 @@ Remove an entry from both memory and SQLite. Also cancels any in-flight deduplic
149
142
 
150
143
  #### `cache.purgeStale(): void`
151
144
 
152
- Remove all expired entries (past `ttl + stale`) from both the in-memory cache and SQLite. Also cleans up stale lock rows older than 1 hour and runs `PRAGMA wal_checkpoint(TRUNCATE)` + `PRAGMA optimize`.
145
+ Remove all expired entries (past `ttl + stale`) from both the in-memory cache and SQLite, and run `PRAGMA wal_checkpoint(TRUNCATE)` + `PRAGMA optimize`.
153
146
 
154
147
  #### `cache.close(): void`
155
148
 
@@ -163,7 +156,6 @@ Returns runtime statistics:
163
156
 
164
157
  ```ts
165
158
  {
166
- lock: { timeout, mean, stddev } | undefined,
167
159
  dedupe: { size },
168
160
  memory: { size, maxSize, count, maxCount } | undefined,
169
161
  database: { location, size } | undefined,
@@ -193,34 +185,130 @@ Once the stale window expires, the entry is purged entirely and the next `get()`
193
185
 
194
186
  ```
195
187
  |--- ttl ---|--- stale ---|
196
- fresh stale expired
197
- ↓ ↓
198
- sync hit sync hit async miss
199
- + bg refresh
188
+ fresh stale expired
189
+ ↓ ↓
190
+ sync hit sync hit async miss
191
+ + bg refresh
200
192
  ```
201
193
 
202
- ## Cross-Process/Thread Locking
194
+ ## Cross-Thread Locking
195
+
196
+ Worker threads in the same process that pass the same `location` to `new Cache(...)` share a `SharedArrayBuffer` (acquired via `@nxtedition/shared`'s `getOrCreate` registry). That buffer is treated as a hash table of per-key `Int32` mutex slots:
197
+
198
+ - Acquire is `Atomics.compareExchange(slot, 0, 1)`. The winner runs `valueSelector`; losers `Atomics.waitAsync` for a notify (with a 1 s timeout that re-enters `#load` — a guard against missed notifies and holder crashes).
199
+ - On completion the holder does `Atomics.sub(slot, 1)` + `Atomics.notify`, waking any waiters which then re-enter `get()` and read the freshly-cached value.
200
+ - Exception paths (sync `valueSelector` throw, async rejection, serializer/ttl/stale throwing inside `#set`) all run the release under a `try/finally`, so a buggy user callback can never wedge a slot.
201
+
202
+ When the `location` is `':memory:'`, the cache is inherently per-instance (the SQLite DB isn't shared), so the SAB is skipped entirely and all coordination falls back to the instance-local `#dedupe` Map.
203
+
204
+ Pass `{ lock: false }` (or `lock: null`) to opt out of the SAB lock on any cache — the instance-local `#dedupe` Map still coalesces concurrent `get()` calls on the same key, but sibling threads hitting the same DB file will each call `valueSelector` independently.
205
+
206
+ There is **no cross-process coordination** — two separate Node processes pointed at the same SQLite file may both run `valueSelector` for the same key. If you need cross-process dedup, do it at a layer above (e.g. a request-coalescing service).
207
+
208
+ ### When to disable the lock
203
209
 
204
- When multiple processes or worker threads share the same SQLite database, the lock mechanism prevents redundant `valueSelector` calls. Process A acquires a lock, computes the value, and writes it. Process B sees the lock, waits for the estimated completion time (EMA-based), then reads the value from SQLite.
210
+ - `':memory:'` already disabled for you; nothing to configure.
211
+ - Single-threaded app that doesn't spawn workers — disable to skip the cheap SAB round-trip on every `get()` cache miss.
212
+ - Workload where each worker uses a disjoint keyspace — the SAB adds overhead without any dedupe benefit.
205
213
 
206
- For worker threads sharing the same process, a `SharedArrayBuffer` + `Atomics.waitAsync` mechanism provides efficient cross-thread wakeup — the waiting thread is notified immediately when the lock holder writes the value, rather than polling.
214
+ ### Counter overflow
207
215
 
208
- If the lock holder crashes, the lock becomes stale after `3 * lockTimeout` and another process steals it.
216
+ Each slot is an `Int32`. `refresh()` uses `Atomics.add(slot, 1)` rather than a CAS (so concurrent refreshes stack), with an overflow guard at `0xfffffff` — if the counter ever reaches that value the call throws `Error: lock counter overflow` and rolls the increment back, so a wedged slot can't silently wrap. In practice this is unreachable: `0xfffffff` = ~268 M in-flight refreshes, which would OOM long before hitting the guard.
217
+
218
+ ## File Sharding
219
+
220
+ The cache partitions its SQLite storage across N physical files (default `shards: 4`). Each key is hash-routed (via xxhash32) to one shard, and reads/writes for that key only touch that shard's connection. This is intended to reduce the SQLite writer-serialization ceiling: SQLite allows only one writer at a time per database file, even in WAL mode, so a single file caps concurrent-writer throughput at roughly one thread's worth of work regardless of how many threads the process spawns.
221
+
222
+ **On-disk layout:**
223
+
224
+ | `shards` | Files |
225
+ | ----------- | -------------------------------------------------------------------- |
226
+ | `1` | `<location>` (single file, legacy layout — compatible with `stat()`) |
227
+ | `N` (N ≥ 2) | `<location>.0`, `<location>.1`, …, `<location>.{N-1}` |
228
+
229
+ With `shards: 1` the cache is a single file at the given `location`, identical to the pre-sharding layout — useful for backup scripts or observability tools that expect one file.
230
+
231
+ **Cross-instance consistency:** hash routing is deterministic (xxhash32 is pure), so two Cache instances pointing at the same `location` with the same `shards` count will route the same keys to the same shards. Data persists across instance restarts.
232
+
233
+ **Changing `shards` invalidates data.** If you open a cache with `shards: 2` after previously using `shards: 4`, keys hash-route to different shard files than they were written to. The old data is still on disk but effectively unreachable until you open with the original shard count.
234
+
235
+ **Per-shard state:** `maxSize` divides evenly across shards (`maxSize / shards` per shard), eviction runs per-shard on `SQLITE_FULL`, and `stats.database.size` is summed across all shards.
209
236
 
210
237
  ## Batched Writes
211
238
 
212
- SQLite writes are batched using `setImmediate` — multiple `set()` calls within the same microtask turn are coalesced into a single `BEGIN`/`COMMIT` transaction. While a write batch is pending, the in-memory cache is corked (eviction deferred) to avoid dropping entries before they reach disk. If a batch exceeds 512 items, it is flushed immediately.
239
+ SQLite writes are batched using `setImmediate` — multiple `set()` calls within the same microtask turn are coalesced into a single `BEGIN`/`COMMIT` transaction per shard. While a write batch is pending, the in-memory cache is corked (eviction deferred) to avoid dropping entries before they reach disk. If a per-shard batch exceeds 512 items, it is flushed immediately.
240
+
241
+ Flushes iterate shards starting at a random index on each call, so no single shard is starved when the 10 ms per-flush time budget is exhausted mid-pass — the next flush picks a different starting shard.
213
242
 
214
- If the database is full (`SQLITE_FULL`), the cache evicts the 256 oldest entries and retries up to 3 times. On other errors, the rolled-back items are dropped from the batch and the error is emitted.
243
+ If a shard's database is full (`SQLITE_FULL`), the cache evicts the 256 oldest entries in that shard and retries up to 3 times. On other errors, the entire pending batch for that shard is dropped (items remain in the in-memory cache until natural eviction/TTL) and the error is emitted — this prevents error floods when the underlying failure is persistent (e.g. a read-only DB).
215
244
 
216
245
  ## Error Handling
217
246
 
218
- `Cache` extends `EventEmitter`. Non-fatal errors (SQLite failures, lock contention errors, stale revalidation failures) are emitted as `'error'` events when a listener is attached. If no `'error'` listener is registered, errors are surfaced via `process.emitWarning()` instead, avoiding unhandled crashes.
247
+ `Cache` extends `EventEmitter`. Non-fatal errors (SQLite failures, stale revalidation failures, background-refresh rejections from the SWR fire-and-forget path) are emitted as `'error'` events when a listener is attached. If no `'error'` listener is registered, errors are surfaced via `process.emitWarning()` instead, avoiding unhandled crashes.
219
248
 
220
249
  ## Off-Peak Purge
221
250
 
222
251
  All cache instances listen on the `nxt:offPeak` `BroadcastChannel`. When a message is received, `purgeStale()` is called on every active instance, enabling coordinated cleanup during low-traffic periods.
223
252
 
253
+ ## Benchmarks
254
+
255
+ Measured on Apple M3 Pro (12 CPUs), Node 25.6.1. Throughput is `ops/sec`; latency is `ns` (median).
256
+
257
+ ### Single-thread hot paths
258
+
259
+ | Operation | ops/sec | p50 | p99 |
260
+ | ------------------------------------------- | ------- | ------ | ------ |
261
+ | `get()` memory hit (sequential keys) | 6.87 M | 125 ns | 333 ns |
262
+ | `get()` memory hit (random keys) | 7.09 M | 125 ns | 250 ns |
263
+ | `peek()` memory hit | 5.74 M | 125 ns | 625 ns |
264
+ | `get()` memory miss, DB hit (sequential) | 386 K | 2.3 µs | 6.9 µs |
265
+ | `get()` memory miss, DB hit (random) | 345 K | 2.3 µs | 9.4 µs |
266
+ | `get()` cold (sync `valueSelector`) | 284 K | 2.0 µs | 3.4 µs |
267
+ | `get()` memory-only hit (no DB) | 8.37 M | 125 ns | 209 ns |
268
+ | `get()` memory-only cold (no DB) | 1.73 M | 500 ns | 1.1 µs |
269
+ | `get()` eviction pressure (`maxCount=1000`) | 1.43 M | 583 ns | 1.8 µs |
270
+ | `delete()` existing keys | 73 K | 12 µs | 32 µs |
271
+ | `purgeStale()` 10 K expired entries | 7.4 ms | — | — |
272
+
273
+ ### Shard-count comparison
274
+
275
+ Single-thread overhead of sharding on the hot paths is ≤1 % in either direction. The pay-off is under multi-thread write contention:
276
+
277
+ **4 threads, partitioned cold writes** (each worker writes unique keys, stressing concurrent writers):
278
+
279
+ | `shards` | Aggregate throughput | Scaling vs. 1 thread | Δ vs. `shards: 1` |
280
+ | -------- | -------------------- | -------------------- | ----------------- |
281
+ | 1 | 383 K ops/s | 1.47× | baseline |
282
+ | 2 | 491 K ops/s | 1.85× | **+28 %** |
283
+ | 4 | 551 K ops/s | 2.15× | **+44 %** |
284
+
285
+ **4 threads, shared-keys hot-hit** (mostly memory-resident; sharding shouldn't help much here):
286
+
287
+ | `shards` | Aggregate throughput |
288
+ | -------- | -------------------- |
289
+ | 1 | 25.3 M ops/s |
290
+ | 2 | 24.5 M ops/s |
291
+ | 4 | 24.6 M ops/s |
292
+
293
+ So `shards: 4` (the default) is a large win for write-heavy multi-threaded workloads and roughly break-even otherwise. If you run single-threaded or strictly read-heavy, `shards: 1` removes all sharding overhead and restores the single-file on-disk layout.
294
+
295
+ ### Lock option overhead (single-thread)
296
+
297
+ | Path | lock enabled | lock disabled |
298
+ | --------------------------------- | ------------ | ------------- |
299
+ | `get()` cold (sync valueSelector) | 361 K ops/s | 388 K ops/s |
300
+ | `get()` memory hit | 9.23 M ops/s | 8.93 M ops/s |
301
+
302
+ Cross-thread SAB locking adds ~7 % on the cold path and is negligible (or slightly faster) on memory hits. It is worth leaving on unless the workload is strictly single-threaded or partitioned across workers.
303
+
304
+ ### Reproducing
305
+
306
+ ```sh
307
+ yarn build
308
+ node scripts/bench.mjs # full bench suite
309
+ node scripts/bench-shards.mjs # shard-count comparison (1, 2, 4 shards)
310
+ ```
311
+
224
312
  ## Scripts
225
313
 
226
314
  ```sh
package/lib/index.d.ts CHANGED
@@ -3,12 +3,9 @@ import { type MemoryOptions } from './memory.ts';
3
3
  export type { MemoryOptions } from './memory.ts';
4
4
  export interface DatabaseOptions {
5
5
  timeout?: number;
6
+ shards?: number;
6
7
  maxSize?: number;
7
8
  }
8
- export interface LockOptions {
9
- minTimeout?: number;
10
- maxTimeout?: number;
11
- }
12
9
  export interface Serializer<V> {
13
10
  serialize: (value: V) => Buffer | Uint8Array | string;
14
11
  deserialize: (data: Buffer | string) => V;
@@ -16,8 +13,8 @@ export interface Serializer<V> {
16
13
  export interface CacheOptions<V> {
17
14
  ttl?: number | ((value: V, key: string) => number);
18
15
  stale?: number | ((value: V, key: string) => number);
19
- lock?: LockOptions | false | null;
20
16
  memory?: MemoryOptions | false | null;
17
+ lock?: false | null;
21
18
  database?: DatabaseOptions | false | null;
22
19
  serializer?: Serializer<V>;
23
20
  }
@@ -37,11 +34,6 @@ export declare class Cache<V = unknown, A extends unknown[] = [string]> extends
37
34
  #private;
38
35
  constructor(location: string, valueSelector?: (...args: A) => V | PromiseLike<V>, keySelector?: (...args: A) => string, opts?: CacheOptions<V>);
39
36
  get stats(): {
40
- lock: {
41
- timeout: number;
42
- mean: number;
43
- stddev: number;
44
- } | undefined;
45
37
  dedupe: {
46
38
  size: number;
47
39
  };
@@ -68,3 +60,4 @@ export declare class Cache<V = unknown, A extends unknown[] = [string]> extends
68
60
  export declare const AsyncCache: typeof Cache;
69
61
  /** @deprecated Use `CacheOptions` instead. */
70
62
  export type AsyncCacheOptions<V> = CacheOptions<V>;
63
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAO1C,OAAO,EAAsC,KAAK,aAAa,EAAE,MAAM,aAAa,CAAA;AAEpF,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AA2BhD,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,SAAS,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,GAAG,UAAU,GAAG,MAAM,CAAA;IACrD,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,KAAK,CAAC,CAAA;CAC1C;AAaD,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,CAAA;IAClD,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,CAAA;IACpD,MAAM,CAAC,EAAE,aAAa,GAAG,KAAK,GAAG,IAAI,CAAA;IACrC,IAAI,CAAC,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,QAAQ,CAAC,EAAE,eAAe,GAAG,KAAK,GAAG,IAAI,CAAA;IACzC,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;CAC3B;AAED,MAAM,MAAM,WAAW,CAAC,CAAC,IACrB;IAAE,KAAK,EAAE,CAAC,GAAG,SAAS,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GACtC;IAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAAC,KAAK,EAAE,IAAI,CAAA;CAAE,CAAA;AAEtC,OAAO,CAAC,MAAM,CAAC;IAEb,IAAI,WAAW,EAAE,OAAO,CAAC;QAAE,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC,EAAE,CAAA;CACtD;AAyFD,qBAAa,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,SAAS,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAE,SAAQ,YAAY;;gBA2BhF,QAAQ,EAAE,MAAM,EAChB,aAAa,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,EAClD,WAAW,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,MAAM,EACpC,IAAI,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC;IA+FxB,IAAI,KAAK;;;;;;;;;;;sBACmB,MAAM;kBAAQ,MAAM,GAAG,SAAS;;MAqB3D;IAED,CAAC,MAAM,CAAC,OAAO,CAAC;IAIhB,KAAK,IAAI,IAAI;IAsBb,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC;IAO/B,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC;IAOhC,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC;IA0BnC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,IAAI;IAYxB,UAAU,IAAI,IAAI;CAgXnB;AAoBD,uCAAuC;AACvC,eAAO,MAAM,UAAU,cAAQ,CAAA;AAC/B,8CAA8C;AAC9C,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAAI,YAAY,CAAC,CAAC,CAAC,CAAA"}