@edv4h/usketch-plugin-sync-ywebsocket 1.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 +21 -0
- package/README.md +157 -0
- package/dist/divergence-tracker.d.ts +40 -0
- package/dist/divergence-tracker.d.ts.map +1 -0
- package/dist/divergence-tracker.js +134 -0
- package/dist/divergence-tracker.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +18 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +54 -0
- package/dist/plugin.js.map +1 -0
- package/dist/sync-status-tracker.d.ts +112 -0
- package/dist/sync-status-tracker.d.ts.map +1 -0
- package/dist/sync-status-tracker.js +163 -0
- package/dist/sync-status-tracker.js.map +1 -0
- package/dist/types.d.ts +91 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/unconfirmed-overlay.d.ts +21 -0
- package/dist/unconfirmed-overlay.d.ts.map +1 -0
- package/dist/unconfirmed-overlay.js +61 -0
- package/dist/unconfirmed-overlay.js.map +1 -0
- package/dist/yws-sync.d.ts +4 -0
- package/dist/yws-sync.d.ts.map +1 -0
- package/dist/yws-sync.js +492 -0
- package/dist/yws-sync.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EdV4H
|
|
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,157 @@
|
|
|
1
|
+
# @edv4h/usketch-plugin-sync-ywebsocket
|
|
2
|
+
|
|
3
|
+
Bridge a uSketch app to any [y-websocket](https://github.com/yjs/y-websocket) server. Wraps `WebsocketProvider` and exposes a `WsProviderHandle`-compatible adapter so it plugs directly into plugins like `@edv4h/usketch-plugin-presence-cursor`.
|
|
4
|
+
|
|
5
|
+
Designed for teams that already run a y-websocket backend (or need to migrate off another realtime engine) and want to keep their sync layer while adopting uSketch on the client.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @edv4h/usketch-plugin-sync-ywebsocket
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Basic usage
|
|
14
|
+
|
|
15
|
+
`getWsProvider()` is only valid *after* the sync plugin's `setup()` has run. Since
|
|
16
|
+
`createApp` calls `setup()` in plugin order, the recommended pattern is to defer
|
|
17
|
+
the `wsProvider` lookup into a thin wrapper plugin that runs after the sync
|
|
18
|
+
plugin. This keeps everything inside a single `createApp` call:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createApp } from "@edv4h/usketch-core";
|
|
22
|
+
import { createBoardStore } from "@edv4h/usketch-store";
|
|
23
|
+
import type { UsketchPlugin } from "@edv4h/usketch-shared";
|
|
24
|
+
import { createPresenceCursorPlugin } from "@edv4h/usketch-plugin-presence-cursor";
|
|
25
|
+
import { createYwebsocketSyncPlugin, type YwebsocketSyncPlugin } from "@edv4h/usketch-plugin-sync-ywebsocket";
|
|
26
|
+
|
|
27
|
+
const syncPlugin = createYwebsocketSyncPlugin({
|
|
28
|
+
url: "wss://yws.example.com",
|
|
29
|
+
roomName: "board-123",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Wrap presence-cursor so the provider is read at setup time, when syncPlugin.setup
|
|
33
|
+
// has already run (plugins are set up in order).
|
|
34
|
+
function presenceCursorWithSync(
|
|
35
|
+
sync: YwebsocketSyncPlugin,
|
|
36
|
+
opts: { userId: string; userName: string },
|
|
37
|
+
): UsketchPlugin {
|
|
38
|
+
let inner: UsketchPlugin | null = null;
|
|
39
|
+
return {
|
|
40
|
+
id: "usketch-plugin-presence-cursor",
|
|
41
|
+
name: "Presence Cursor",
|
|
42
|
+
async setup(ctx) {
|
|
43
|
+
inner = createPresenceCursorPlugin({
|
|
44
|
+
wsProvider: sync.getWsProvider(),
|
|
45
|
+
userId: opts.userId,
|
|
46
|
+
userName: opts.userName,
|
|
47
|
+
});
|
|
48
|
+
await inner.setup(ctx);
|
|
49
|
+
},
|
|
50
|
+
teardown() {
|
|
51
|
+
inner?.teardown?.();
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const app = await createApp({
|
|
57
|
+
store: createBoardStore(),
|
|
58
|
+
plugins: [
|
|
59
|
+
syncPlugin,
|
|
60
|
+
presenceCursorWithSync(syncPlugin, { userId: "alice", userName: "Alice" }),
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This pattern generalizes to any plugin that needs a `WsProviderHandle`: wrap it
|
|
66
|
+
in a small factory that reads `sync.getWsProvider()` inside its own `setup()`.
|
|
67
|
+
|
|
68
|
+
## Options
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
createYwebsocketSyncPlugin({
|
|
72
|
+
url, // ws(s):// base URL of the y-websocket server
|
|
73
|
+
roomName, // server-side document identifier
|
|
74
|
+
shapesMapKey, // optional — defaults to "shapes" (weboard uses "map")
|
|
75
|
+
resolveParams, // async/sync callback to supply URL query params on each connect
|
|
76
|
+
onCloseCode, // classify close codes: "retry" | "stop" | undefined (default backoff)
|
|
77
|
+
idleTimeoutMs, // 0 (off) — disconnect after this many ms of inactivity
|
|
78
|
+
autoConnect, // default true — connect on plugin setup
|
|
79
|
+
doc, // optional — bring your own Y.Doc (useful for mid-life migrations)
|
|
80
|
+
WebSocketPolyfill, // optional — Node test environments only
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Resolving auth tokens on each connect
|
|
85
|
+
|
|
86
|
+
`resolveParams` is called before every connect and reconnect. Use it to hand back freshly refreshed tokens so the server sees a valid credential on reconnect:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
createYwebsocketSyncPlugin({
|
|
90
|
+
url: "wss://yws.example.com",
|
|
91
|
+
roomName: "board-123",
|
|
92
|
+
async resolveParams({ attempt, previousCloseCode }) {
|
|
93
|
+
// Force a fresh token if the previous connection was rejected for auth.
|
|
94
|
+
const forceRefresh = previousCloseCode === 4003 || previousCloseCode === 4004;
|
|
95
|
+
const token = await cognitoSession.getAccessToken({ forceRefresh });
|
|
96
|
+
return {
|
|
97
|
+
params: {
|
|
98
|
+
token: token.jwt,
|
|
99
|
+
employeeId: String(token.employeeId),
|
|
100
|
+
companyId: String(token.companyId),
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
onCloseCode(code) {
|
|
105
|
+
if (code === 4003 || code === 4004) return "retry"; // immediate retry; resolveParams will fetch a fresh token
|
|
106
|
+
return undefined; // fall through to exponential backoff
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Idle disconnect
|
|
112
|
+
|
|
113
|
+
Set `idleTimeoutMs` to disconnect after inactivity (no local board edits / store mutations). The timer also resets whenever the socket reports `connected` or `resume()` is called. Call `handle.resume()` to reconnect on the next user interaction.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const plugin = createYwebsocketSyncPlugin({
|
|
117
|
+
url: "wss://yws.example.com",
|
|
118
|
+
roomName: "board-123",
|
|
119
|
+
idleTimeoutMs: 5 * 60 * 1000, // 5 minutes
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Later, in an interaction handler:
|
|
123
|
+
plugin.getHandle().resume();
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Advanced: accessing the handle
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const handle = syncPlugin.getHandle();
|
|
130
|
+
handle.disconnect(); // manual disconnect; stays disconnected
|
|
131
|
+
handle.resume(); // reconnect after a disconnect/idle
|
|
132
|
+
handle.status.subscribe(() => console.log(handle.status.getSnapshot()));
|
|
133
|
+
handle.doc; // the underlying Y.Doc
|
|
134
|
+
handle.whenSynced; // promise that resolves on first server sync
|
|
135
|
+
handle.wsProvider; // WsProviderHandle adapter (same as getWsProvider())
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## WsProviderHandle compatibility
|
|
139
|
+
|
|
140
|
+
`handle.wsProvider` implements the `WsProviderHandle` contract from `@edv4h/usketch-sync` so plugins that consume `WsProviderHandle` work out of the box:
|
|
141
|
+
|
|
142
|
+
| member | supported |
|
|
143
|
+
| ------------------- | ----------------------------------------- |
|
|
144
|
+
| `connected` | yes |
|
|
145
|
+
| `awareness` | yes (always — shared across reconnects) |
|
|
146
|
+
| `onStatusChange` | yes |
|
|
147
|
+
| `onBroadcast` | listeners accepted; never emits (no-op) |
|
|
148
|
+
| `broadcast` | no-op — y-websocket has no side-channel |
|
|
149
|
+
| `requestPartition` | no-op — y-websocket has no partitions |
|
|
150
|
+
| `onPartitionMeta` | no-op — y-websocket has no partitions |
|
|
151
|
+
| `destroy` | yes |
|
|
152
|
+
|
|
153
|
+
If you need broadcast / partition semantics, stay on `@edv4h/usketch-sync`'s `createWsProvider()` with a uSketch-native server.
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { BoardStore } from "@edv4h/usketch-shared";
|
|
2
|
+
import type * as Y from "yjs";
|
|
3
|
+
import { SyncStatusTracker } from "./sync-status-tracker.js";
|
|
4
|
+
/**
|
|
5
|
+
* Standalone divergence tracker for apps that wire IDB sync + WsProvider
|
|
6
|
+
* **manually** (rather than via `createYwebsocketSyncPlugin`). It watches the
|
|
7
|
+
* store, the Y.Map of shapes, and the WebSocket connection, then exposes a
|
|
8
|
+
* `SyncStatusTracker` whose `unconfirmedShapeIds` reflects shapes that exist
|
|
9
|
+
* locally but the server hasn't acknowledged.
|
|
10
|
+
*
|
|
11
|
+
* Why this exists: the full plugin embeds y-websocket's `provider.on("sync")`
|
|
12
|
+
* event for the "first server sync" stamp, but `@edv4h/usketch-sync`'s
|
|
13
|
+
* `createWsProvider` doesn't expose that signal. We approximate by stamping
|
|
14
|
+
* `firstServerSyncAt` on the first remote-origin Y.Doc update, which is the
|
|
15
|
+
* earliest moment we can be sure the server has talked back to us.
|
|
16
|
+
*/
|
|
17
|
+
export interface DivergenceTrackerOptions {
|
|
18
|
+
store: BoardStore;
|
|
19
|
+
doc: Y.Doc;
|
|
20
|
+
shapesMap: Y.Map<Record<string, unknown>>;
|
|
21
|
+
/** Subscribe to socket-level connection state (`"connected"` / `"connecting"` / `"disconnected"`). */
|
|
22
|
+
onConnectionStatusChange: (handler: (status: string) => void) => () => void;
|
|
23
|
+
/**
|
|
24
|
+
* Grace period (ms) to wait after the socket reaches `"connected"` before
|
|
25
|
+
* concluding "first sync completed" if no remote Y.Doc update has arrived.
|
|
26
|
+
* The y-websocket SYNC_STEP1/STEP2 round-trip is normally fast, so even an
|
|
27
|
+
* empty server (no shapes to push) will resolve well under a second. We
|
|
28
|
+
* pick a generous default to avoid false positives on slow networks.
|
|
29
|
+
*
|
|
30
|
+
* Defaults to 2000ms. Set to 0 to disable the timer-based fallback (only
|
|
31
|
+
* the first remote update will stamp).
|
|
32
|
+
*/
|
|
33
|
+
emptyServerGraceMs?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface DivergenceTrackerHandle {
|
|
36
|
+
status: SyncStatusTracker;
|
|
37
|
+
destroy: () => void;
|
|
38
|
+
}
|
|
39
|
+
export declare function createDivergenceTracker(opts: DivergenceTrackerOptions): DivergenceTrackerHandle;
|
|
40
|
+
//# sourceMappingURL=divergence-tracker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"divergence-tracker.d.ts","sourceRoot":"","sources":["../src/divergence-tracker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,KAAK,CAAC,MAAM,KAAK,CAAC;AAC9B,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE7D;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,wBAAwB;IACxC,KAAK,EAAE,UAAU,CAAC;IAClB,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC;IACX,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC1C,sGAAsG;IACtG,wBAAwB,EAAE,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAC5E;;;;;;;;;OASG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,uBAAuB;IACvC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,OAAO,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,wBAAwB,GAAG,uBAAuB,CAwI/F"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { SyncStatusTracker } from "./sync-status-tracker.js";
|
|
2
|
+
export function createDivergenceTracker(opts) {
|
|
3
|
+
const { store, doc, shapesMap, onConnectionStatusChange, emptyServerGraceMs = 2000 } = opts;
|
|
4
|
+
const status = new SyncStatusTracker();
|
|
5
|
+
let currentWsStatus = "disconnected";
|
|
6
|
+
let serverSynced = false;
|
|
7
|
+
// Timer that fires `markServerSynced()` if `connected` persists for
|
|
8
|
+
// `emptyServerGraceMs` without any remote Y.Doc update. Covers the
|
|
9
|
+
// "empty server / no history" case where a remote update never comes.
|
|
10
|
+
let emptyServerTimer = null;
|
|
11
|
+
function clearEmptyServerTimer() {
|
|
12
|
+
if (emptyServerTimer !== null) {
|
|
13
|
+
clearTimeout(emptyServerTimer);
|
|
14
|
+
emptyServerTimer = null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function markServerSynced() {
|
|
18
|
+
if (serverSynced)
|
|
19
|
+
return;
|
|
20
|
+
serverSynced = true;
|
|
21
|
+
clearEmptyServerTimer();
|
|
22
|
+
status.batch(() => {
|
|
23
|
+
status.markFirstServerSyncObserved();
|
|
24
|
+
if (currentWsStatus === "connected") {
|
|
25
|
+
status.update({ state: "synced" });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
// 1. Connection state — drives the "is local add already confirmed?" decision
|
|
30
|
+
// in the store mutation handler below, and is mirrored onto the snapshot
|
|
31
|
+
// `state` field so consumers (e.g. Debug HUD's Persistence indicator)
|
|
32
|
+
// show the right thing.
|
|
33
|
+
const offStatus = onConnectionStatusChange((next) => {
|
|
34
|
+
currentWsStatus = next;
|
|
35
|
+
// Mirror transport state into the snapshot using the same mapping the
|
|
36
|
+
// full ywebsocket plugin uses: socket-level "connected" maps to
|
|
37
|
+
// "syncing" until the first remote update arrives, then "synced".
|
|
38
|
+
const mappedState = next === "connected"
|
|
39
|
+
? serverSynced
|
|
40
|
+
? "synced"
|
|
41
|
+
: "syncing"
|
|
42
|
+
: next === "connecting"
|
|
43
|
+
? "connecting"
|
|
44
|
+
: next === "disconnected"
|
|
45
|
+
? "disconnected"
|
|
46
|
+
: "error";
|
|
47
|
+
status.update({
|
|
48
|
+
state: mappedState,
|
|
49
|
+
error: next === "failed" ? "connection failed" : null,
|
|
50
|
+
});
|
|
51
|
+
// Empty-server fallback: when we connect but the server has nothing to
|
|
52
|
+
// push (new room, lost history), `doc.on("update")` never fires. Start
|
|
53
|
+
// a timer so `firstServerSyncAt` is still stamped after the SYNC round-
|
|
54
|
+
// trip has had time to complete. `emptyServerGraceMs: 0` opts out.
|
|
55
|
+
if (next === "connected" && !serverSynced && emptyServerGraceMs > 0) {
|
|
56
|
+
clearEmptyServerTimer();
|
|
57
|
+
emptyServerTimer = setTimeout(markServerSynced, emptyServerGraceMs);
|
|
58
|
+
}
|
|
59
|
+
else if (next !== "connected") {
|
|
60
|
+
// Drop the timer if we got disconnected before the grace expired.
|
|
61
|
+
clearEmptyServerTimer();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// 2. Initial load: every shape already in the Y.Map (typically restored from
|
|
65
|
+
// IndexedDB) is "local-only" until the server confirms.
|
|
66
|
+
if (shapesMap.size > 0) {
|
|
67
|
+
const initialIds = [];
|
|
68
|
+
for (const id of shapesMap.keys())
|
|
69
|
+
initialIds.push(id);
|
|
70
|
+
status.noteShapesLoaded(initialIds, "local");
|
|
71
|
+
}
|
|
72
|
+
// 3. Local mutations from the store — online → confirmed (broadcast goes
|
|
73
|
+
// out immediately), offline → unconfirmed.
|
|
74
|
+
const offMutation = store.onMutation((event) => {
|
|
75
|
+
const payload = event.payload;
|
|
76
|
+
if (!payload?.id)
|
|
77
|
+
return;
|
|
78
|
+
status.batch(() => {
|
|
79
|
+
if (event.type === "shape:added") {
|
|
80
|
+
const isOnline = currentWsStatus === "connected";
|
|
81
|
+
status.noteShapeAdded(payload.id, isOnline ? "remote" : "local");
|
|
82
|
+
}
|
|
83
|
+
else if (event.type === "shape:removed") {
|
|
84
|
+
status.noteShapeRemoved(payload.id);
|
|
85
|
+
}
|
|
86
|
+
status.update({ shapeCount: shapesMap.size, lastSyncedAt: Date.now() });
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
// 4. Y.Map observe — for remote-origin additions (server pushed a shape
|
|
90
|
+
// we didn't have, or another peer added one).
|
|
91
|
+
const observer = (events) => {
|
|
92
|
+
const isLocalTxn = events.transaction.local;
|
|
93
|
+
status.batch(() => {
|
|
94
|
+
for (const [key, change] of events.changes.keys) {
|
|
95
|
+
if (change.action === "add" && !isLocalTxn) {
|
|
96
|
+
// Remote add → confirmed by server.
|
|
97
|
+
status.noteShapeAdded(key, "remote");
|
|
98
|
+
}
|
|
99
|
+
else if (change.action === "delete") {
|
|
100
|
+
status.noteShapeRemoved(key);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
status.update({ shapeCount: shapesMap.size, lastSyncedAt: Date.now() });
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
shapesMap.observe(observer);
|
|
107
|
+
// 5. First remote-origin doc update marks the server as "synced with us".
|
|
108
|
+
// We deliberately DO NOT bulk-confirm `shapesMap.keys()` here — that
|
|
109
|
+
// would silently mark every IndexedDB-restored shape as "the server
|
|
110
|
+
// knows about it", which is exactly what we want to NOT assume (the
|
|
111
|
+
// whole point of this overlay is to surface phantom/orphaned shapes).
|
|
112
|
+
// Instead, the Y.Map observer above marks individual remote-origin
|
|
113
|
+
// additions as confirmed via `noteShapeAdded(key, "remote")`. Anything
|
|
114
|
+
// the server didn't push to us during this session stays unconfirmed.
|
|
115
|
+
//
|
|
116
|
+
// Note: an "empty" server sync (no shapes to push) won't trigger this
|
|
117
|
+
// handler — the `emptyServerTimer` started above is the fallback that
|
|
118
|
+
// catches that case.
|
|
119
|
+
function onDocUpdate(_update, origin) {
|
|
120
|
+
if (origin !== "remote")
|
|
121
|
+
return;
|
|
122
|
+
markServerSynced();
|
|
123
|
+
}
|
|
124
|
+
doc.on("update", onDocUpdate);
|
|
125
|
+
function destroy() {
|
|
126
|
+
clearEmptyServerTimer();
|
|
127
|
+
offStatus();
|
|
128
|
+
offMutation();
|
|
129
|
+
shapesMap.unobserve(observer);
|
|
130
|
+
doc.off("update", onDocUpdate);
|
|
131
|
+
}
|
|
132
|
+
return { status, destroy };
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=divergence-tracker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"divergence-tracker.js","sourceRoot":"","sources":["../src/divergence-tracker.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { createDivergenceTracker, type DivergenceTrackerHandle, type DivergenceTrackerOptions, } from "./divergence-tracker.js";
|
|
2
|
+
export { createYwebsocketSyncPlugin, type YwebsocketSyncPlugin } from "./plugin.js";
|
|
3
|
+
export { type SyncState, type SyncStatusSnapshot, SyncStatusTracker, } from "./sync-status-tracker.js";
|
|
4
|
+
export type { ConnectionParams, ResolveParamsContext, WsConnectionStatus, YwebsocketSyncHandle, YwebsocketSyncOptions, } from "./types.js";
|
|
5
|
+
export { UnconfirmedOverlay } from "./unconfirmed-overlay.js";
|
|
6
|
+
export { createYwebsocketSync } from "./yws-sync.js";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACN,uBAAuB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,GAC7B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,0BAA0B,EAAE,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,EACN,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,iBAAiB,GACjB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACX,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Standalone helpers for apps that wire IDB sync + WsProvider manually
|
|
2
|
+
// (rather than via `createYwebsocketSyncPlugin`). `createDivergenceTracker`
|
|
3
|
+
// produces a SyncStatusTracker driven by a Y.Doc + WsProvider, and
|
|
4
|
+
// `<UnconfirmedOverlay />` renders the SVG badge layer.
|
|
5
|
+
export { createDivergenceTracker, } from "./divergence-tracker.js";
|
|
6
|
+
export { createYwebsocketSyncPlugin } from "./plugin.js";
|
|
7
|
+
export { SyncStatusTracker, } from "./sync-status-tracker.js";
|
|
8
|
+
export { UnconfirmedOverlay } from "./unconfirmed-overlay.js";
|
|
9
|
+
export { createYwebsocketSync } from "./yws-sync.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,4EAA4E;AAC5E,mEAAmE;AACnE,wDAAwD;AACxD,OAAO,EACN,uBAAuB,GAGvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,0BAA0B,EAA6B,MAAM,aAAa,CAAC;AACpF,OAAO,EAGN,iBAAiB,GACjB,MAAM,0BAA0B,CAAC;AAQlC,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { UsketchPlugin } from "@edv4h/usketch-shared";
|
|
2
|
+
import type { WsProviderHandle } from "@edv4h/usketch-sync";
|
|
3
|
+
import type { YwebsocketSyncHandle, YwebsocketSyncOptions } from "./types.js";
|
|
4
|
+
export interface YwebsocketSyncPlugin extends UsketchPlugin {
|
|
5
|
+
/**
|
|
6
|
+
* Returns the underlying `WsProviderHandle` for consumption by other plugins
|
|
7
|
+
* (e.g. `@edv4h/usketch-plugin-presence-cursor`).
|
|
8
|
+
*
|
|
9
|
+
* Only valid *after* the plugin's `setup()` has run. See the README for the
|
|
10
|
+
* recommended pattern (wrap the dependent plugin so it reads the provider
|
|
11
|
+
* from inside its own `setup()`).
|
|
12
|
+
*/
|
|
13
|
+
getWsProvider(): WsProviderHandle;
|
|
14
|
+
/** Access the full sync handle — status, Y.Doc, disconnect/resume/destroy. */
|
|
15
|
+
getHandle(): YwebsocketSyncHandle;
|
|
16
|
+
}
|
|
17
|
+
export declare function createYwebsocketSyncPlugin(options: YwebsocketSyncOptions): YwebsocketSyncPlugin;
|
|
18
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAI9E,MAAM,WAAW,oBAAqB,SAAQ,aAAa;IAC1D;;;;;;;OAOG;IACH,aAAa,IAAI,gBAAgB,CAAC;IAClC,8EAA8E;IAC9E,SAAS,IAAI,oBAAoB,CAAC;CAClC;AAID,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,qBAAqB,GAAG,oBAAoB,CAmE/F"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { UnconfirmedOverlay } from "./unconfirmed-overlay.js";
|
|
3
|
+
import { createYwebsocketSync } from "./yws-sync.js";
|
|
4
|
+
const UNCONFIRMED_OVERLAY_LAYER_ID = "unconfirmed-shapes-overlay";
|
|
5
|
+
export function createYwebsocketSyncPlugin(options) {
|
|
6
|
+
let handle = null;
|
|
7
|
+
let unregisterOverlay = null;
|
|
8
|
+
const plugin = {
|
|
9
|
+
id: "usketch-plugin-sync-ywebsocket",
|
|
10
|
+
name: "Realtime Sync (y-websocket)",
|
|
11
|
+
async setup(ctx) {
|
|
12
|
+
handle = createYwebsocketSync(ctx.store, options);
|
|
13
|
+
globalThis.__usketchSyncStatus = handle.status;
|
|
14
|
+
// Diagnostic overlay: draws a red badge on shapes that exist locally
|
|
15
|
+
// but haven't been confirmed by the server. Sits between standard
|
|
16
|
+
// shapes (default order) and the debug HUD (9999).
|
|
17
|
+
const overlayHandle = handle;
|
|
18
|
+
ctx.layers.register({
|
|
19
|
+
id: UNCONFIRMED_OVERLAY_LAYER_ID,
|
|
20
|
+
order: 250,
|
|
21
|
+
fixed: true,
|
|
22
|
+
render: (renderCtx) => (_jsx(UnconfirmedOverlay, { store: ctx.store, shapes: ctx.shapes, viewport: renderCtx.viewport, syncStatus: overlayHandle.status })),
|
|
23
|
+
});
|
|
24
|
+
unregisterOverlay = () => ctx.layers.unregister(UNCONFIRMED_OVERLAY_LAYER_ID);
|
|
25
|
+
// When `autoConnect: false`, `connect()` is never called, so awaiting
|
|
26
|
+
// `whenSynced` here would hang the entire plugin setup chain. Skip it
|
|
27
|
+
// and let the caller drive connection via `handle.resume()`.
|
|
28
|
+
if (options.autoConnect !== false) {
|
|
29
|
+
await handle.whenSynced;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
teardown() {
|
|
33
|
+
unregisterOverlay?.();
|
|
34
|
+
unregisterOverlay = null;
|
|
35
|
+
handle?.destroy();
|
|
36
|
+
delete globalThis.__usketchSyncStatus;
|
|
37
|
+
handle = null;
|
|
38
|
+
},
|
|
39
|
+
getWsProvider() {
|
|
40
|
+
if (!handle) {
|
|
41
|
+
throw new Error("[usketch-plugin-sync-ywebsocket] getWsProvider() called before setup(); register the plugin with createApp() first, or call getWsProvider() from inside another plugin's setup().");
|
|
42
|
+
}
|
|
43
|
+
return handle.wsProvider;
|
|
44
|
+
},
|
|
45
|
+
getHandle() {
|
|
46
|
+
if (!handle) {
|
|
47
|
+
throw new Error("[usketch-plugin-sync-ywebsocket] getHandle() called before setup(); register the plugin with createApp() first, or call getHandle() from inside another plugin's setup().");
|
|
48
|
+
}
|
|
49
|
+
return handle;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
return plugin;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.tsx"],"names":[],"mappings":";AAGA,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAgBrD,MAAM,4BAA4B,GAAG,4BAA4B,CAAC;AAElE,MAAM,UAAU,0BAA0B,CAAC,OAA8B;IACxE,IAAI,MAAM,GAAgC,IAAI,CAAC;IAC/C,IAAI,iBAAiB,GAAwB,IAAI,CAAC;IAElD,MAAM,MAAM,GAAyB;QACpC,EAAE,EAAE,gCAAgC;QACpC,IAAI,EAAE,6BAA6B;QAEnC,KAAK,CAAC,KAAK,CAAC,GAAG;YACd,MAAM,GAAG,oBAAoB,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YACjD,UAAsC,CAAC,mBAAmB,GAAG,MAAM,CAAC,MAAM,CAAC;YAE5E,qEAAqE;YACrE,kEAAkE;YAClE,mDAAmD;YACnD,MAAM,aAAa,GAAG,MAAM,CAAC;YAC7B,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC;gBACnB,EAAE,EAAE,4BAA4B;gBAChC,KAAK,EAAE,GAAG;gBACV,KAAK,EAAE,IAAI;gBACX,MAAM,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,CACtB,KAAC,kBAAkB,IAClB,KAAK,EAAE,GAAG,CAAC,KAAK,EAChB,MAAM,EAAE,GAAG,CAAC,MAAM,EAClB,QAAQ,EAAE,SAAS,CAAC,QAAQ,EAC5B,UAAU,EAAE,aAAa,CAAC,MAAM,GAC/B,CACF;aACD,CAAC,CAAC;YACH,iBAAiB,GAAG,GAAG,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,4BAA4B,CAAC,CAAC;YAE9E,sEAAsE;YACtE,sEAAsE;YACtE,6DAA6D;YAC7D,IAAI,OAAO,CAAC,WAAW,KAAK,KAAK,EAAE,CAAC;gBACnC,MAAM,MAAM,CAAC,UAAU,CAAC;YACzB,CAAC;QACF,CAAC;QAED,QAAQ;YACP,iBAAiB,EAAE,EAAE,CAAC;YACtB,iBAAiB,GAAG,IAAI,CAAC;YACzB,MAAM,EAAE,OAAO,EAAE,CAAC;YAClB,OAAQ,UAAsC,CAAC,mBAAmB,CAAC;YACnE,MAAM,GAAG,IAAI,CAAC;QACf,CAAC;QAED,aAAa;YACZ,IAAI,CAAC,MAAM,EAAE,CAAC;gBACb,MAAM,IAAI,KAAK,CACd,mLAAmL,CACnL,CAAC;YACH,CAAC;YACD,OAAO,MAAM,CAAC,UAAU,CAAC;QAC1B,CAAC;QAED,SAAS;YACR,IAAI,CAAC,MAAM,EAAE,CAAC;gBACb,MAAM,IAAI,KAAK,CACd,2KAA2K,CAC3K,CAAC;YACH,CAAC;YACD,OAAO,MAAM,CAAC;QACf,CAAC;KACD,CAAC;IAEF,OAAO,MAAM,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export type SyncState = "loading" | "connecting" | "synced" | "syncing" | "disconnected" | "error";
|
|
2
|
+
export interface SyncStatusSnapshot {
|
|
3
|
+
state: SyncState;
|
|
4
|
+
shapeCount: number;
|
|
5
|
+
/**
|
|
6
|
+
* Timestamp of the most recent activity that touched the snapshot — local
|
|
7
|
+
* mutations and server syncs alike. Useful for "last activity" displays
|
|
8
|
+
* but NOT a reliable indicator that the server has acknowledged anything.
|
|
9
|
+
* For divergence gating use `firstServerSyncAt` instead.
|
|
10
|
+
*/
|
|
11
|
+
lastSyncedAt: number | null;
|
|
12
|
+
/**
|
|
13
|
+
* Timestamp of the first successful `setConfirmedFromServer(...)` call
|
|
14
|
+
* (i.e. the first `provider.on("sync", true)` event). Stays `null` until
|
|
15
|
+
* the server has actually merged its state with us. Set once and never
|
|
16
|
+
* cleared: a later disconnection should still allow consumers to surface
|
|
17
|
+
* divergence (offline edits accumulate exactly when the user needs the
|
|
18
|
+
* warning).
|
|
19
|
+
*/
|
|
20
|
+
firstServerSyncAt: number | null;
|
|
21
|
+
error: string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Shape IDs present in the local Y.Doc that the server has NOT confirmed.
|
|
24
|
+
*
|
|
25
|
+
* Populated by `noteShapeAdded(id, "local")` (a shape created locally that
|
|
26
|
+
* hasn't been seen by the server yet) and by `noteShapesLoaded(...)` for
|
|
27
|
+
* pre-connection IndexedDB restoration. Becomes accurate only **after** the
|
|
28
|
+
* first `setConfirmedFromServer(...)` (`sync` event with `isSynced: true`):
|
|
29
|
+
* before that, every shape from a persisted Y.Doc looks "local-only" because
|
|
30
|
+
* we genuinely don't know what the server holds yet.
|
|
31
|
+
*
|
|
32
|
+
* Consumers should gate divergence UI on `firstServerSyncAt !== null` so
|
|
33
|
+
* the warning isn't surfaced during the initial `loading` / `connecting`
|
|
34
|
+
* phase (every cold start would otherwise look full of "unconfirmed"
|
|
35
|
+
* shapes). Once that first server sync has happened, the gate stays open
|
|
36
|
+
* even across later disconnections.
|
|
37
|
+
*/
|
|
38
|
+
unconfirmedShapeIds: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
export declare class SyncStatusTracker {
|
|
41
|
+
private snapshot;
|
|
42
|
+
private listeners;
|
|
43
|
+
private readonly shapeIds;
|
|
44
|
+
private readonly confirmedShapeIds;
|
|
45
|
+
private batchDepth;
|
|
46
|
+
private batchDirty;
|
|
47
|
+
getSnapshot(): SyncStatusSnapshot;
|
|
48
|
+
subscribe(listener: () => void): () => void;
|
|
49
|
+
/**
|
|
50
|
+
* Update the "free-form" snapshot fields (state / shapeCount / lastSyncedAt
|
|
51
|
+
* / error). The divergence-tracking fields (`unconfirmedShapeIds` and
|
|
52
|
+
* `firstServerSyncAt`) are intentionally excluded so callers can't bypass
|
|
53
|
+
* the dedicated APIs and break the invariant that `firstServerSyncAt` is
|
|
54
|
+
* stamped exactly once on the first server sync.
|
|
55
|
+
*
|
|
56
|
+
* @internal
|
|
57
|
+
*/
|
|
58
|
+
update(partial: Partial<Omit<SyncStatusSnapshot, "unconfirmedShapeIds" | "firstServerSyncAt">>): void;
|
|
59
|
+
/**
|
|
60
|
+
* Replace the "server-confirmed" set with the given IDs. Use this only when
|
|
61
|
+
* you have authoritative knowledge of which IDs the server has acknowledged
|
|
62
|
+
* — typically from y-websocket's `provider.on("sync")` event, where the
|
|
63
|
+
* Y.Map post-merge represents the union of (server view ∪ our uploads), so
|
|
64
|
+
* every current key IS confirmed.
|
|
65
|
+
*
|
|
66
|
+
* Do **not** call this with `shapesMap.keys()` if your transport can't tell
|
|
67
|
+
* you which keys originated from the server: that would silently confirm
|
|
68
|
+
* orphaned IndexedDB shapes the server never had, defeating the divergence
|
|
69
|
+
* detection. Use `markFirstServerSyncObserved()` + the `noteShapeAdded(...,
|
|
70
|
+
* "remote")` per-key path instead.
|
|
71
|
+
*
|
|
72
|
+
* Also stamps `firstServerSyncAt` on the first call so consumers can gate
|
|
73
|
+
* divergence UI on "have we ever heard back from the server?".
|
|
74
|
+
*/
|
|
75
|
+
setConfirmedFromServer(ids: Iterable<string>): void;
|
|
76
|
+
/**
|
|
77
|
+
* Stamp `firstServerSyncAt` on the first call without touching the
|
|
78
|
+
* confirmed set. For transports that can't atomically tell you which
|
|
79
|
+
* shape IDs the server already knew at sync time (e.g. raw `WsProvider`
|
|
80
|
+
* without an `onSync` event), this is the right hook: the per-key
|
|
81
|
+
* `noteShapeAdded(id, "remote")` calls from the Y.Map observer fill in
|
|
82
|
+
* the confirmed set incrementally.
|
|
83
|
+
*/
|
|
84
|
+
markFirstServerSyncObserved(): void;
|
|
85
|
+
private markFirstServerSyncObservedInternal;
|
|
86
|
+
/**
|
|
87
|
+
* Note a shape addition. `source = "remote"` means the shape arrived via the
|
|
88
|
+
* Yjs provider (server origin), so it's already confirmed. `source = "local"`
|
|
89
|
+
* means we created it client-side and the server has yet to acknowledge.
|
|
90
|
+
*/
|
|
91
|
+
noteShapeAdded(id: string, source: "local" | "remote"): void;
|
|
92
|
+
/**
|
|
93
|
+
* Bulk variant of `noteShapeAdded` for initial load. Avoids the O(n²)
|
|
94
|
+
* cost of calling the single-shape mutator in a loop (each call would
|
|
95
|
+
* re-scan `shapeIds` and notify subscribers). Recompute and notify
|
|
96
|
+
* happen once after all IDs are ingested.
|
|
97
|
+
*/
|
|
98
|
+
noteShapesLoaded(ids: Iterable<string>, source: "local" | "remote"): void;
|
|
99
|
+
noteShapeRemoved(id: string): void;
|
|
100
|
+
private recompute;
|
|
101
|
+
/**
|
|
102
|
+
* Combine multiple mutator calls into a single notification. Useful when
|
|
103
|
+
* the caller wants to e.g. `noteShapeAdded(...)` and `update({ shapeCount })`
|
|
104
|
+
* back-to-back without paying for two re-renders.
|
|
105
|
+
*
|
|
106
|
+
* Re-entrant: nested calls are tolerated, the listeners fire once when the
|
|
107
|
+
* outermost batch closes.
|
|
108
|
+
*/
|
|
109
|
+
batch<T>(fn: () => T): T;
|
|
110
|
+
private notify;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=sync-status-tracker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-status-tracker.d.ts","sourceRoot":"","sources":["../src/sync-status-tracker.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,YAAY,GAAG,QAAQ,GAAG,SAAS,GAAG,cAAc,GAAG,OAAO,CAAC;AAEnG,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,SAAS,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;;;;;OAOG;IACH,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB;;;;;;;;;;;;;;;OAeG;IACH,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED,qBAAa,iBAAiB;IAC7B,OAAO,CAAC,QAAQ,CAOd;IACF,OAAO,CAAC,SAAS,CAAyB;IAG1C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAqB;IAIvD,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,UAAU,CAAS;IAE3B,WAAW,IAAI,kBAAkB;IAIjC,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAO3C;;;;;;;;OAQG;IACH,MAAM,CACL,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,qBAAqB,GAAG,mBAAmB,CAAC,CAAC,GACrF,IAAI;IAKP;;;;;;;;;;;;;;;OAeG;IACH,sBAAsB,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI;IAUnD;;;;;;;OAOG;IACH,2BAA2B,IAAI,IAAI;IAMnC,OAAO,CAAC,mCAAmC;IAM3C;;;;OAIG;IACH,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI;IAQ5D;;;;;OAKG;IACH,gBAAgB,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI;IAUzE,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAMlC,OAAO,CAAC,SAAS;IAajB;;;;;;;OAOG;IACH,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC;IAaxB,OAAO,CAAC,MAAM;CAOd"}
|