@optimystic/db-p2p-storage-fs 1.14.0 → 1.16.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/README.md +142 -33
- package/dist/src/file-batch-backend.d.ts +69 -0
- package/dist/src/file-batch-backend.d.ts.map +1 -0
- package/dist/src/file-batch-backend.js +400 -0
- package/dist/src/file-batch-backend.js.map +1 -0
- package/dist/src/file-kv-store.d.ts +37 -3
- package/dist/src/file-kv-store.d.ts.map +1 -1
- package/dist/src/file-kv-store.js +100 -34
- package/dist/src/file-kv-store.js.map +1 -1
- package/dist/src/file-storage.d.ts +20 -58
- package/dist/src/file-storage.d.ts.map +1 -1
- package/dist/src/file-storage.js +16 -430
- package/dist/src/file-storage.js.map +1 -1
- package/dist/src/file-write-log.d.ts +361 -0
- package/dist/src/file-write-log.d.ts.map +1 -0
- package/dist/src/file-write-log.js +848 -0
- package/dist/src/file-write-log.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/store-lock.d.ts +42 -0
- package/dist/src/store-lock.d.ts.map +1 -0
- package/dist/src/store-lock.js +116 -0
- package/dist/src/store-lock.js.map +1 -0
- package/package.json +3 -3
- package/src/file-batch-backend.ts +430 -0
- package/src/file-kv-store.ts +112 -32
- package/src/file-storage.ts +22 -468
- package/src/file-write-log.ts +906 -0
- package/src/index.ts +3 -0
- package/src/store-lock.ts +123 -0
- package/dist/src/atomic-write.d.ts +0 -22
- package/dist/src/atomic-write.d.ts.map +0 -1
- package/dist/src/atomic-write.js +0 -90
- package/dist/src/atomic-write.js.map +0 -1
- package/src/atomic-write.ts +0 -89
package/README.md
CHANGED
|
@@ -11,11 +11,13 @@ Node.js filesystem storage backend for Optimystic peers. Provides:
|
|
|
11
11
|
acknowledged before every cohort member held them), and the persistent
|
|
12
12
|
transaction state used to recover crashed two-phase commits.
|
|
13
13
|
|
|
14
|
+
Both write through one write-ahead log per directory (below).
|
|
15
|
+
|
|
14
16
|
This package targets plain Node.js. Use the sibling adapter for other
|
|
15
17
|
environments:
|
|
16
18
|
|
|
17
19
|
- `@optimystic/db-p2p-storage-ns` — NativeScript (iOS/Android, SQLite)
|
|
18
|
-
- `@optimystic/db-p2p-storage-rn` — React Native (
|
|
20
|
+
- `@optimystic/db-p2p-storage-rn` — React Native (LevelDB)
|
|
19
21
|
- `@optimystic/db-p2p-storage-web` — Browser (IndexedDB)
|
|
20
22
|
|
|
21
23
|
## Install
|
|
@@ -39,21 +41,46 @@ const libp2p = await createLibp2pNode({
|
|
|
39
41
|
storage: rawStorage,
|
|
40
42
|
kvStore,
|
|
41
43
|
});
|
|
44
|
+
|
|
45
|
+
// … and on shutdown, after the node has stopped:
|
|
46
|
+
await libp2p.stop();
|
|
47
|
+
await kvStore.close();
|
|
48
|
+
await rawStorage.close();
|
|
42
49
|
```
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
The node never closes a store it was handed, so close both after `stop()`.
|
|
52
|
+
Closing flushes what the store still holds and, for the last store over the
|
|
53
|
+
directory, checkpoints the write-ahead log and removes the lock. A store that is
|
|
54
|
+
never closed loses nothing it flushed, but leaves a log for the next open to
|
|
55
|
+
replay and a lock the next open replaces as stale.
|
|
56
|
+
|
|
57
|
+
Both constructors take a `basePath` — the directory under which all data is
|
|
58
|
+
stored — and an optional options object:
|
|
59
|
+
|
|
60
|
+
| Option | Default | What it does |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| `idleFlushMs` | 1000 | How long a write may wait for a `flush()` before the store flushes it itself |
|
|
63
|
+
| `checkpointBytes` | 16 MiB | Checkpoint the log once its records reach this size |
|
|
64
|
+
| `checkpointIdleMs` | 5000 | Checkpoint the log this long after its last append |
|
|
65
|
+
|
|
66
|
+
The two checkpoint settings belong to the directory's one log, so they are the
|
|
67
|
+
first-opened store's; a store opened beside it with different values logs them
|
|
68
|
+
on the `optimystic:db-p2p-storage-fs:write-log` channel and shares the open
|
|
69
|
+
log's.
|
|
70
|
+
|
|
71
|
+
The two stores can share the same `basePath` in practice because their
|
|
46
72
|
top-level names do not collide: block data lives under `<blockId>/`
|
|
47
|
-
subdirectories
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
give them separate `basePath`s if
|
|
52
|
-
node itself writes all start with a
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
shared.
|
|
73
|
+
subdirectories, while KV data lives under directories named by the first
|
|
74
|
+
segment of each key (a `/`-separated key becomes nested subdirectories — e.g.
|
|
75
|
+
`coordinator/key1` → `coordinator/key1.json`). Sharing is safe only as long as
|
|
76
|
+
no block id's first segment (a block id containing `/` nests the same way)
|
|
77
|
+
equals a KV key's first segment; give them separate `basePath`s if
|
|
78
|
+
you cannot guarantee that. The KV keys the node itself writes all start with a
|
|
79
|
+
fixed literal segment (`under-replicated/`, `coordinator/`, `participant/`,
|
|
80
|
+
`executed/` — db-p2p's `NODE_KV_NAMESPACES`), and `FileRawStorage.listBlockIds`
|
|
81
|
+
skips those root directories, so a KV key is never mistaken for a block. KV bytes do count
|
|
82
|
+
toward `FileRawStorage.getApproximateBytesUsed` when the path is shared. Over
|
|
83
|
+
one `basePath` the two share one log, one lock and one checkpoint.
|
|
57
84
|
|
|
58
85
|
Without a `kvStore`, the node falls back to an in-memory store and logs a
|
|
59
86
|
`node-wiring` warning: what the ledger records is then lost on restart.
|
|
@@ -69,8 +96,14 @@ Without a `kvStore`, the node falls back to an in-memory store and logs a
|
|
|
69
96
|
actions/<id>.json — committed Transform
|
|
70
97
|
proofs/<rev>.json — BlockCommitProof retained for that revision
|
|
71
98
|
blocks/<id>.json — materialized IBlock snapshot
|
|
99
|
+
<seg>/<seg>/ — a block id containing "/" (e.g. `default/app/Ledger`) nests one directory per segment
|
|
100
|
+
meta.json — and holds the same files as above; it can contain another block's directory
|
|
101
|
+
index/<seg>/meta.json — (e.g. `default/app/Ledger/index/LedgerLatest`)
|
|
72
102
|
<key-segment>/
|
|
73
103
|
<key-segment>.json — FileKVStore value; key "/" separators become subdirectories
|
|
104
|
+
write-ahead.log — groups written since the last checkpoint
|
|
105
|
+
write-ahead.log.sealed — the log a checkpoint is syncing the tree under; present only during one, or after a crash in one
|
|
106
|
+
store.lock — { pid, hostname } of the process holding the store
|
|
74
107
|
```
|
|
75
108
|
|
|
76
109
|
Action ids that contain a colon (e.g. `tx:abcd1234`) are percent-encoded in
|
|
@@ -78,12 +111,83 @@ filenames (`tx%3Aabcd1234.json`) for Windows compatibility; the storage layer
|
|
|
78
111
|
encodes and decodes transparently so callers always work with the canonical
|
|
79
112
|
id form.
|
|
80
113
|
|
|
81
|
-
##
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
114
|
+
## Write-ahead log
|
|
115
|
+
|
|
116
|
+
Each store holds its writes in memory and, at a `flush()`, appends everything
|
|
117
|
+
written since the last flush to `<basePath>/write-ahead.log` as one record — a
|
|
118
|
+
length, a CRC-32, then each put or delete with its path. A flush given no
|
|
119
|
+
window (`withinMs` absent or 0) then waits for the disk once, and that sync
|
|
120
|
+
covers every record appended before it. A flush given a window resolves as soon
|
|
121
|
+
as the record is appended; the log syncs by the earliest deadline any waiting
|
|
122
|
+
record was given, so many groups share one sync. A group that arrives to find
|
|
123
|
+
an earlier deadline already passed makes the sync itself before it resolves,
|
|
124
|
+
whatever its own window, so a disk that falls behind slows the writers rather
|
|
125
|
+
than widening the window. Only after the sync are the files written to their
|
|
126
|
+
canonical paths in the tree, without waiting. A tree file therefore never holds
|
|
127
|
+
content whose log record is not durable, and the next open replays the log in
|
|
128
|
+
order before serving anything.
|
|
129
|
+
|
|
130
|
+
**What a crash loses.** A crash of the process loses nothing a flush resolved:
|
|
131
|
+
every record is in the operating system's hands before its flush resolves, and
|
|
132
|
+
the next open replays it. A power cut or operating-system crash loses at most
|
|
133
|
+
the groups still inside their window — whole groups only, never half of one,
|
|
134
|
+
and always the last ones: the log is one sequence, and a record cut short, or
|
|
135
|
+
one whose checksum fails because a later page of the log was written while an
|
|
136
|
+
earlier one was not, ends the replay there, so what survives is a prefix of the
|
|
137
|
+
groups in flush order.
|
|
138
|
+
|
|
139
|
+
**Checkpoint.** The log does not grow for ever. A checkpoint seals the log —
|
|
140
|
+
renames it to `write-ahead.log.sealed` and starts an empty one, which new
|
|
141
|
+
writes go to at once — then syncs every file written since the last checkpoint
|
|
142
|
+
and every directory whose entries changed, then deletes the sealed segment. It
|
|
143
|
+
runs when the log reaches `checkpointBytes`, `checkpointIdleMs` after the last
|
|
144
|
+
append, and when the last store over the directory closes. Only the seal waits
|
|
145
|
+
in line with writes; the syncs run beside them. A crash during a checkpoint
|
|
146
|
+
leaves the sealed segment behind, and the next open replays it before the
|
|
147
|
+
current log. A checkpoint that fails latches the store failed, like a failed
|
|
148
|
+
flush, because a half-synced tree under a discarded log could not be recovered.
|
|
149
|
+
|
|
150
|
+
## Durability
|
|
151
|
+
|
|
152
|
+
`FileRawStorage` reports `durability: 'power-loss'`, and a resolved
|
|
153
|
+
`FileKVStore.flush()` promises the same: a flushed group survives a power cut
|
|
154
|
+
once its window has passed, or at once for a flush given none, and the tree is
|
|
155
|
+
rebuilt from the log on the next open. `whenDurable()` resolves once every group
|
|
156
|
+
flushed so far is synced. The log is synced once per strict group and once per
|
|
157
|
+
window for the relaxed groups appended inside it; a strict flush also covers
|
|
158
|
+
every relaxed group waiting, which is how a strict flush of one store makes the
|
|
159
|
+
other store's earlier groups durable too. The write path syncs nothing else.
|
|
160
|
+
Directories are synced at the checkpoint (and at open, by the replay), not per
|
|
161
|
+
write; on Windows, where a directory cannot be synced, only files are. See
|
|
162
|
+
[storage.md §6](../db-p2p/docs/storage.md#6-a-write-survives-a-crash-once-a-flush-called-after-it-has-resolved).
|
|
163
|
+
|
|
164
|
+
The idle checkpoint, `checkpointIdleMs` after the last append, syncs whatever is
|
|
165
|
+
waiting before it seals the log, so a record waits at most the shorter of its
|
|
166
|
+
window and that setting; the default 5 s equals the longest window the node
|
|
167
|
+
computes. A sync that started after a deadline it covered — the disk, or the
|
|
168
|
+
log's queue, fell behind — is logged once per such episode as `window-overrun`
|
|
169
|
+
on the `optimystic:db-p2p-storage-fs:write-log` channel, with how late it
|
|
170
|
+
started and how many groups it covered.
|
|
171
|
+
|
|
172
|
+
A solo node's write therefore costs three syncs: its pend, its commit, and the
|
|
173
|
+
under-replication entry it records, since the ledger flushes after every call
|
|
174
|
+
that wrote, and every one of those flushes is strict. A write a cohort agreed to
|
|
175
|
+
is flushed with the window its cohort allows, so a member applying many such
|
|
176
|
+
writes pays one sync per window instead. GitHub issue #37 measured more than
|
|
177
|
+
seventy syncs for one solo write before the log.
|
|
178
|
+
|
|
179
|
+
Opening a store takes `<basePath>/store.lock`, which records the holder's
|
|
180
|
+
`{ pid, hostname }`. A lock naming a live process on this machine refuses the
|
|
181
|
+
open with `StoreLockedError` from every operation; a lock left by a process that
|
|
182
|
+
has exited, or by another machine, is stale and replaced. Every store over one
|
|
183
|
+
directory in one process shares one log and one lock.
|
|
184
|
+
|
|
185
|
+
**Downgrading.** A build older than the log reads the tree and ignores
|
|
186
|
+
`write-ahead.log`, so it sees only what the tree held: after an unclean stop it
|
|
187
|
+
loses the groups the log held and the tree did not, and a later upgrade back
|
|
188
|
+
would replay that stale log over what the older build wrote meanwhile. A clean
|
|
189
|
+
`close()` checkpoints first, leaving an empty log. To downgrade after an
|
|
190
|
+
unclean stop, open and close the store once with this build first.
|
|
87
191
|
|
|
88
192
|
## Identity
|
|
89
193
|
|
|
@@ -105,24 +209,29 @@ cleanup:
|
|
|
105
209
|
|
|
106
210
|
- **Shared conformance suite** — `runRawStorageConformance('FileSystem', …)` from
|
|
107
211
|
`@optimystic/db-p2p/testing/conformance`, the one cross-backend parity target. It proves the
|
|
108
|
-
fs backend (
|
|
109
|
-
every other backend: round-trips, `listRevisions`
|
|
110
|
-
the exact missing-pend error, clone-on-store/read
|
|
111
|
-
boundary), drain-before-yield iteration,
|
|
212
|
+
fs backend (`KvRawStorage` over `WriteGroupDriver` over a `FileBatchBackend`)
|
|
213
|
+
behaves identically to every other backend: round-trips, `listRevisions`
|
|
214
|
+
ordering, promote atomicity + the exact missing-pend error, clone-on-store/read
|
|
215
|
+
(structural via the byte boundary), drain-before-yield iteration, a
|
|
216
|
+
`BlockStorage` parity slice, and the flush-then-reopen case.
|
|
112
217
|
- **fs-only tests** (`node:assert`) — the behaviors the shared suite can't cover:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
218
|
+
what the log leaves in the tree and what a reopen replays
|
|
219
|
+
(`test/file-write-log.spec.ts` for a damaged log, a failed apply, a failed sync,
|
|
220
|
+
the sync a relaxed group defers and what hastens it, the checkpoint and its
|
|
221
|
+
crash cases, and the lock; `test/fsync-count.spec.ts`
|
|
222
|
+
for one sync per `StorageRepo` group), torn-file corruption tolerance,
|
|
223
|
+
`readdir` error discrimination, colon-encoded filenames + the POSIX legacy
|
|
224
|
+
raw-colon fallback, the directory-based `listBlockIds` meta-gate, `FileKVStore`
|
|
225
|
+
listings over unflushed writes and the log it shares with `FileRawStorage`,
|
|
226
|
+
and the under-replication ledger over `FileKVStore`.
|
|
116
227
|
|
|
117
228
|
## Known limitations
|
|
118
229
|
|
|
119
|
-
**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
[
|
|
124
|
-
an explicit `dispose()` pattern. Until that lands, `FileRawStorage` is
|
|
125
|
-
single-process only.
|
|
230
|
+
**The lock protects one machine.** `store.lock` refuses a second live process
|
|
231
|
+
on the same host; it cannot tell a live process on another machine from a dead
|
|
232
|
+
one, so two machines over one network share can both open the store. That was
|
|
233
|
+
never supported (Invariant 5 in
|
|
234
|
+
[storage.md](../db-p2p/docs/storage.md#5-a-store-is-owned-by-exactly-one-process)).
|
|
126
235
|
|
|
127
236
|
**Ephemeral peer identity.** There is no `loadOrCreateFSPeerKey` equivalent.
|
|
128
237
|
A Node peer backed by `FileRawStorage` gets a new libp2p peer id each restart.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { BlockId, ActionId } from '@optimystic/db-core';
|
|
2
|
+
import { type BatchBackend, type BatchEntry, type CommittedBatch, type SinkCommitOptions, type StoreDurability, type StoreIdentity, type StoreKey } from '@optimystic/db-p2p';
|
|
3
|
+
import { type FileWriteLogOptions } from './file-write-log.js';
|
|
4
|
+
/**
|
|
5
|
+
* Filesystem {@link BatchBackend}: the six logical block-storage stores mapped to six paths under
|
|
6
|
+
* `basePath/<blockId>/` (`{meta.json,revs/,pend/,actions/,proofs/,blocks/}`), every batch committed
|
|
7
|
+
* through the directory's {@link FileWriteLog}. The directory tree is a deliberate,
|
|
8
|
+
* human-inspectable layout — it is NOT flattened into encoded-filename KV keys.
|
|
9
|
+
*
|
|
10
|
+
* `KvRawStorage` owns all JSON serialization, so this backend reads raw `Uint8Array` bytes and
|
|
11
|
+
* never does `JSON.stringify/parse` on values. What is fs-specific lives here: the path per
|
|
12
|
+
* {@link StoreKey}, the corrupt-content-as-missing read guard, colon-encoded action-id filenames
|
|
13
|
+
* with the legacy raw-colon read fallback and win32 guards, and the directory listings.
|
|
14
|
+
*
|
|
15
|
+
* Every operation opens the shared log first — on the first one: take the lock, replay, then
|
|
16
|
+
* serve — so no read ever sees a tree the log has not been replayed into. The log is acquired on
|
|
17
|
+
* that first operation rather than in the constructor, so an instance that is constructed and
|
|
18
|
+
* never used (one `withReadCache` dedupes away) holds no handle and no lock.
|
|
19
|
+
*/
|
|
20
|
+
export declare class FileBatchBackend implements BatchBackend {
|
|
21
|
+
private readonly options;
|
|
22
|
+
/** A committed batch's record is synced to the log by the deadline its commit allowed, or before the commit resolves; replay rebuilds the tree from it. */
|
|
23
|
+
readonly durability: StoreDurability;
|
|
24
|
+
readonly basePath: string;
|
|
25
|
+
private readonly identity;
|
|
26
|
+
private log;
|
|
27
|
+
/** `options` are the directory log's checkpoint settings, applied when this is the first store to open it. */
|
|
28
|
+
constructor(basePath: string, options?: FileWriteLogOptions);
|
|
29
|
+
storeIdentity(): StoreIdentity;
|
|
30
|
+
get(key: StoreKey): Promise<Uint8Array | undefined>;
|
|
31
|
+
rangeRevisions(blockId: BlockId, lo: number, hi: number): Promise<[number, Uint8Array][]>;
|
|
32
|
+
listPendingActionIds(blockId: BlockId): Promise<ActionId[]>;
|
|
33
|
+
listBlockIds(): AsyncIterable<BlockId>;
|
|
34
|
+
private walkBlockDirectories;
|
|
35
|
+
private childDirectoryNames;
|
|
36
|
+
private hasMetadataFile;
|
|
37
|
+
/** The tree's bytes, the write-ahead log and the lock included: all of it is what the store occupies. */
|
|
38
|
+
approximateBytesUsed(): Promise<number>;
|
|
39
|
+
/** One log record, synced within `syncWithinMs` (see `FileWriteLog.commit`); the `durable` returned is the log's. */
|
|
40
|
+
commit(entries: readonly BatchEntry[], options: SinkCommitOptions): Promise<CommittedBatch>;
|
|
41
|
+
/** Hasten the log's sync. A backend that never committed holds no log, and so has nothing to hasten. */
|
|
42
|
+
sync(): Promise<void>;
|
|
43
|
+
close(): Promise<void>;
|
|
44
|
+
private shared;
|
|
45
|
+
private open;
|
|
46
|
+
/**
|
|
47
|
+
* A put is one entry at the key's canonical path. A delete of an action-scoped record is that
|
|
48
|
+
* path's entry plus, off win32 and when the id carries a colon, the pre-encode raw-colon sibling's,
|
|
49
|
+
* so a deleted item cannot resurface through the read fallback in readActionScopedBytes — in the
|
|
50
|
+
* log, so a replay removes the sibling too.
|
|
51
|
+
*
|
|
52
|
+
* NOTE: that sibling entry names a path with a colon, which only a POSIX build writes; a log
|
|
53
|
+
* carrying one and moved unreplayed to a win32 machine replays it as an unlink of an NTFS
|
|
54
|
+
* stream path, where anything but ENOENT fails the open. A store is not moved across platforms
|
|
55
|
+
* mid-log today; if it ever is, have the replay skip a delete it cannot spell on this platform.
|
|
56
|
+
*/
|
|
57
|
+
private toLogEntries;
|
|
58
|
+
private relativePath;
|
|
59
|
+
private relativeActionScopedPath;
|
|
60
|
+
private blockPath;
|
|
61
|
+
private metadataPath;
|
|
62
|
+
private revisionPath;
|
|
63
|
+
private proofPath;
|
|
64
|
+
private actionScopedPath;
|
|
65
|
+
private readActionScopedBytes;
|
|
66
|
+
private readBytesIfExists;
|
|
67
|
+
private directoryByteSize;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=file-batch-backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-batch-backend.d.ts","sourceRoot":"","sources":["../../src/file-batch-backend.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAsB,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,KAAK,iBAAiB,EAAE,KAAK,eAAe,EAAE,KAAK,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAElM,OAAO,EAAgF,KAAK,mBAAmB,EAAsB,MAAM,qBAAqB,CAAC;AAuEjK;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,gBAAiB,YAAW,YAAY;IAStB,OAAO,CAAC,QAAQ,CAAC,OAAO;IARtD,2JAA2J;IAC3J,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAgB;IAEpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IACzC,OAAO,CAAC,GAAG,CAA2B;IAEtC,8GAA8G;gBAClG,QAAQ,EAAE,MAAM,EAAmB,OAAO,GAAE,mBAAwB;IAMhF,aAAa,IAAI,aAAa;IAMxB,GAAG,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IAiCnD,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC;IAczF,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAyC1D,YAAY,IAAI,aAAa,CAAC,OAAO,CAAC;YA4B9B,oBAAoB;YAWrB,mBAAmB;IAUjC,OAAO,CAAC,eAAe;IAUvB,yGAAyG;IACnG,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC;IAO7C,qHAAqH;IACrH,MAAM,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC;IAI3F,wGAAwG;IACxG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIf,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ5B,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,IAAI;IAIZ;;;;;;;;;;OAUG;IACH,OAAO,CAAC,YAAY;IAcpB,OAAO,CAAC,YAAY;IAkBpB,OAAO,CAAC,wBAAwB;IAKhC,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,gBAAgB;YAqBV,qBAAqB;YAcrB,iBAAiB;YAUjB,iBAAiB;CA0B/B"}
|