@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.
@@ -0,0 +1,163 @@
1
+ export class SyncStatusTracker {
2
+ snapshot = {
3
+ state: "loading",
4
+ shapeCount: 0,
5
+ lastSyncedAt: null,
6
+ firstServerSyncAt: null,
7
+ error: null,
8
+ unconfirmedShapeIds: [],
9
+ };
10
+ listeners = new Set();
11
+ // Local sources of truth for divergence calculation. Kept private so callers
12
+ // can only mutate via the dedicated methods below.
13
+ shapeIds = new Set();
14
+ confirmedShapeIds = new Set();
15
+ // Batch mode: while > 0, mutators skip notification and a single notify is
16
+ // fired when the batch closes. Used by yws-sync to combine `noteShapeAdded`
17
+ // + `update({ shapeCount, lastSyncedAt })` into one render per mutation.
18
+ batchDepth = 0;
19
+ batchDirty = false;
20
+ getSnapshot() {
21
+ return this.snapshot;
22
+ }
23
+ subscribe(listener) {
24
+ this.listeners.add(listener);
25
+ return () => {
26
+ this.listeners.delete(listener);
27
+ };
28
+ }
29
+ /**
30
+ * Update the "free-form" snapshot fields (state / shapeCount / lastSyncedAt
31
+ * / error). The divergence-tracking fields (`unconfirmedShapeIds` and
32
+ * `firstServerSyncAt`) are intentionally excluded so callers can't bypass
33
+ * the dedicated APIs and break the invariant that `firstServerSyncAt` is
34
+ * stamped exactly once on the first server sync.
35
+ *
36
+ * @internal
37
+ */
38
+ update(partial) {
39
+ this.snapshot = { ...this.snapshot, ...partial };
40
+ this.notify();
41
+ }
42
+ /**
43
+ * Replace the "server-confirmed" set with the given IDs. Use this only when
44
+ * you have authoritative knowledge of which IDs the server has acknowledged
45
+ * — typically from y-websocket's `provider.on("sync")` event, where the
46
+ * Y.Map post-merge represents the union of (server view ∪ our uploads), so
47
+ * every current key IS confirmed.
48
+ *
49
+ * Do **not** call this with `shapesMap.keys()` if your transport can't tell
50
+ * you which keys originated from the server: that would silently confirm
51
+ * orphaned IndexedDB shapes the server never had, defeating the divergence
52
+ * detection. Use `markFirstServerSyncObserved()` + the `noteShapeAdded(...,
53
+ * "remote")` per-key path instead.
54
+ *
55
+ * Also stamps `firstServerSyncAt` on the first call so consumers can gate
56
+ * divergence UI on "have we ever heard back from the server?".
57
+ */
58
+ setConfirmedFromServer(ids) {
59
+ this.confirmedShapeIds.clear();
60
+ for (const id of ids)
61
+ this.confirmedShapeIds.add(id);
62
+ // Ensure shapeIds is a superset (ids that exist server-side must exist
63
+ // locally too — they were just merged into our doc).
64
+ for (const id of this.confirmedShapeIds)
65
+ this.shapeIds.add(id);
66
+ this.markFirstServerSyncObservedInternal();
67
+ this.recompute();
68
+ }
69
+ /**
70
+ * Stamp `firstServerSyncAt` on the first call without touching the
71
+ * confirmed set. For transports that can't atomically tell you which
72
+ * shape IDs the server already knew at sync time (e.g. raw `WsProvider`
73
+ * without an `onSync` event), this is the right hook: the per-key
74
+ * `noteShapeAdded(id, "remote")` calls from the Y.Map observer fill in
75
+ * the confirmed set incrementally.
76
+ */
77
+ markFirstServerSyncObserved() {
78
+ if (this.snapshot.firstServerSyncAt !== null)
79
+ return;
80
+ this.markFirstServerSyncObservedInternal();
81
+ this.notify();
82
+ }
83
+ markFirstServerSyncObservedInternal() {
84
+ if (this.snapshot.firstServerSyncAt === null) {
85
+ this.snapshot = { ...this.snapshot, firstServerSyncAt: Date.now() };
86
+ }
87
+ }
88
+ /**
89
+ * Note a shape addition. `source = "remote"` means the shape arrived via the
90
+ * Yjs provider (server origin), so it's already confirmed. `source = "local"`
91
+ * means we created it client-side and the server has yet to acknowledge.
92
+ */
93
+ noteShapeAdded(id, source) {
94
+ this.shapeIds.add(id);
95
+ if (source === "remote") {
96
+ this.confirmedShapeIds.add(id);
97
+ }
98
+ this.recompute();
99
+ }
100
+ /**
101
+ * Bulk variant of `noteShapeAdded` for initial load. Avoids the O(n²)
102
+ * cost of calling the single-shape mutator in a loop (each call would
103
+ * re-scan `shapeIds` and notify subscribers). Recompute and notify
104
+ * happen once after all IDs are ingested.
105
+ */
106
+ noteShapesLoaded(ids, source) {
107
+ for (const id of ids) {
108
+ this.shapeIds.add(id);
109
+ if (source === "remote") {
110
+ this.confirmedShapeIds.add(id);
111
+ }
112
+ }
113
+ this.recompute();
114
+ }
115
+ noteShapeRemoved(id) {
116
+ this.shapeIds.delete(id);
117
+ this.confirmedShapeIds.delete(id);
118
+ this.recompute();
119
+ }
120
+ recompute() {
121
+ const unconfirmed = [];
122
+ for (const id of this.shapeIds) {
123
+ if (!this.confirmedShapeIds.has(id))
124
+ unconfirmed.push(id);
125
+ }
126
+ this.snapshot = {
127
+ ...this.snapshot,
128
+ shapeCount: this.shapeIds.size,
129
+ unconfirmedShapeIds: unconfirmed,
130
+ };
131
+ this.notify();
132
+ }
133
+ /**
134
+ * Combine multiple mutator calls into a single notification. Useful when
135
+ * the caller wants to e.g. `noteShapeAdded(...)` and `update({ shapeCount })`
136
+ * back-to-back without paying for two re-renders.
137
+ *
138
+ * Re-entrant: nested calls are tolerated, the listeners fire once when the
139
+ * outermost batch closes.
140
+ */
141
+ batch(fn) {
142
+ this.batchDepth++;
143
+ try {
144
+ return fn();
145
+ }
146
+ finally {
147
+ this.batchDepth--;
148
+ if (this.batchDepth === 0 && this.batchDirty) {
149
+ this.batchDirty = false;
150
+ this.notify();
151
+ }
152
+ }
153
+ }
154
+ notify() {
155
+ if (this.batchDepth > 0) {
156
+ this.batchDirty = true;
157
+ return;
158
+ }
159
+ for (const listener of this.listeners)
160
+ listener();
161
+ }
162
+ }
163
+ //# sourceMappingURL=sync-status-tracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-status-tracker.js","sourceRoot":"","sources":["../src/sync-status-tracker.ts"],"names":[],"mappings":"AAyCA,MAAM,OAAO,iBAAiB;IACrB,QAAQ,GAAuB;QACtC,KAAK,EAAE,SAAS;QAChB,UAAU,EAAE,CAAC;QACb,YAAY,EAAE,IAAI;QAClB,iBAAiB,EAAE,IAAI;QACvB,KAAK,EAAE,IAAI;QACX,mBAAmB,EAAE,EAAE;KACvB,CAAC;IACM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAC;IAC1C,6EAA6E;IAC7E,mDAAmD;IAClC,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IAC7B,iBAAiB,GAAG,IAAI,GAAG,EAAU,CAAC;IACvD,2EAA2E;IAC3E,4EAA4E;IAC5E,yEAAyE;IACjE,UAAU,GAAG,CAAC,CAAC;IACf,UAAU,GAAG,KAAK,CAAC;IAE3B,WAAW;QACV,OAAO,IAAI,CAAC,QAAQ,CAAC;IACtB,CAAC;IAED,SAAS,CAAC,QAAoB;QAC7B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,GAAG,EAAE;YACX,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACjC,CAAC,CAAC;IACH,CAAC;IAED;;;;;;;;OAQG;IACH,MAAM,CACL,OAAuF;QAEvF,IAAI,CAAC,QAAQ,GAAG,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,OAAO,EAAE,CAAC;QACjD,IAAI,CAAC,MAAM,EAAE,CAAC;IACf,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACH,sBAAsB,CAAC,GAAqB;QAC3C,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,CAAC;QAC/B,KAAK,MAAM,EAAE,IAAI,GAAG;YAAE,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACrD,uEAAuE;QACvE,qDAAqD;QACrD,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,iBAAiB;YAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC/D,IAAI,CAAC,mCAAmC,EAAE,CAAC;QAC3C,IAAI,CAAC,SAAS,EAAE,CAAC;IAClB,CAAC;IAED;;;;;;;OAOG;IACH,2BAA2B;QAC1B,IAAI,IAAI,CAAC,QAAQ,CAAC,iBAAiB,KAAK,IAAI;YAAE,OAAO;QACrD,IAAI,CAAC,mCAAmC,EAAE,CAAC;QAC3C,IAAI,CAAC,MAAM,EAAE,CAAC;IACf,CAAC;IAEO,mCAAmC;QAC1C,IAAI,IAAI,CAAC,QAAQ,CAAC,iBAAiB,KAAK,IAAI,EAAE,CAAC;YAC9C,IAAI,CAAC,QAAQ,GAAG,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,iBAAiB,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;QACrE,CAAC;IACF,CAAC;IAED;;;;OAIG;IACH,cAAc,CAAC,EAAU,EAAE,MAA0B;QACpD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACtB,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;YACzB,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChC,CAAC;QACD,IAAI,CAAC,SAAS,EAAE,CAAC;IAClB,CAAC;IAED;;;;;OAKG;IACH,gBAAgB,CAAC,GAAqB,EAAE,MAA0B;QACjE,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;YACtB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACtB,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;gBACzB,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAChC,CAAC;QACF,CAAC;QACD,IAAI,CAAC,SAAS,EAAE,CAAC;IAClB,CAAC;IAED,gBAAgB,CAAC,EAAU;QAC1B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACzB,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAClC,IAAI,CAAC,SAAS,EAAE,CAAC;IAClB,CAAC;IAEO,SAAS;QAChB,MAAM,WAAW,GAAa,EAAE,CAAC;QACjC,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAChC,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3D,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG;YACf,GAAG,IAAI,CAAC,QAAQ;YAChB,UAAU,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI;YAC9B,mBAAmB,EAAE,WAAW;SAChC,CAAC;QACF,IAAI,CAAC,MAAM,EAAE,CAAC;IACf,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAI,EAAW;QACnB,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,IAAI,CAAC;YACJ,OAAO,EAAE,EAAE,CAAC;QACb,CAAC;gBAAS,CAAC;YACV,IAAI,CAAC,UAAU,EAAE,CAAC;YAClB,IAAI,IAAI,CAAC,UAAU,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;gBAC9C,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;gBACxB,IAAI,CAAC,MAAM,EAAE,CAAC;YACf,CAAC;QACF,CAAC;IACF,CAAC;IAEO,MAAM;QACb,IAAI,IAAI,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;YACzB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;YACvB,OAAO;QACR,CAAC;QACD,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS;YAAE,QAAQ,EAAE,CAAC;IACnD,CAAC;CACD"}
@@ -0,0 +1,91 @@
1
+ import type { WsConnectionStatus, WsProviderHandle } from "@edv4h/usketch-sync";
2
+ import type * as Y from "yjs";
3
+ import type { SyncStatusTracker } from "./sync-status-tracker.js";
4
+ export interface ConnectionParams {
5
+ /** URL query params to attach on each connection attempt. */
6
+ params?: Record<string, string>;
7
+ }
8
+ export interface ResolveParamsContext {
9
+ /**
10
+ * Backoff attempt counter — 0 on the first connect, increments with each
11
+ * failed reconnect cycle, and resets to 0 whenever an `onCloseCode` handler
12
+ * returns `"retry"` (explicit retries skip the backoff ramp).
13
+ */
14
+ attempt: number;
15
+ /** Optional close code from the previous disconnect (if any). */
16
+ previousCloseCode?: number;
17
+ /** Optional close reason from the previous disconnect (if any). */
18
+ previousCloseReason?: string;
19
+ }
20
+ export interface YwebsocketSyncOptions {
21
+ /** Base URL of the y-websocket server (e.g. "wss://yws.example.com"). */
22
+ url: string;
23
+ /** Room name — the document identifier on the server. */
24
+ roomName: string;
25
+ /**
26
+ * The Yjs map key that stores shapes. Defaults to `"shapes"`.
27
+ * Use this when connecting to a server that expects a different key (e.g. weboard uses `"map"`).
28
+ */
29
+ shapesMapKey?: string;
30
+ /**
31
+ * Resolve URL query params on each connection attempt.
32
+ * Called before every connect/reconnect — return freshly refreshed query params
33
+ * or tokens here. If omitted, no query params are attached.
34
+ */
35
+ resolveParams?: (ctx: ResolveParamsContext) => Promise<ConnectionParams> | ConnectionParams;
36
+ /**
37
+ * Custom close-code handler. Called whenever the underlying socket closes.
38
+ * Return `"retry"` to trigger an immediate reconnect (bypassing backoff),
39
+ * `"stop"` to give up and leave the provider disconnected,
40
+ * or `undefined` to fall through to the default backoff retry.
41
+ */
42
+ onCloseCode?: (code: number, reason: string) => "retry" | "stop" | undefined;
43
+ /**
44
+ * Disconnect automatically after this many milliseconds of inactivity.
45
+ * The timer resets on local store mutations, when the socket reports `connected`,
46
+ * and when `resume()` is called. Set to `0` (or omit) to disable.
47
+ */
48
+ idleTimeoutMs?: number;
49
+ /**
50
+ * Explicitly opt in/out of the initial `connect` call.
51
+ * Defaults to `true` — the provider connects on creation.
52
+ */
53
+ autoConnect?: boolean;
54
+ /**
55
+ * Provide an existing Y.Doc to sync. If omitted, a new Y.Doc is created.
56
+ * Weboard's migration path can pass in a pre-existing Y.Doc that already holds legacy state.
57
+ */
58
+ doc?: Y.Doc;
59
+ /**
60
+ * WebSocket constructor polyfill (for Node test environments).
61
+ * In the browser, this is picked up from `globalThis.WebSocket` automatically.
62
+ */
63
+ WebSocketPolyfill?: typeof WebSocket;
64
+ }
65
+ export interface YwebsocketSyncHandle {
66
+ /** The Y.Doc being synced. */
67
+ doc: Y.Doc;
68
+ /** Observable sync status (loading/synced/disconnected/error). */
69
+ status: SyncStatusTracker;
70
+ /**
71
+ * Resolves when the first server sync completes, when `resolveParams` fails the
72
+ * initial attempt, when `onCloseCode` returns `"stop"`, or on `destroy()`.
73
+ * Resolves, never rejects — inspect `status.getSnapshot()` for the latest state
74
+ * (in particular `state === "error"` / `"disconnected"`) to detect failure modes.
75
+ */
76
+ whenSynced: Promise<void>;
77
+ /**
78
+ * WsProviderHandle-compatible adapter. Plug this into `@edv4h/usketch-plugin-presence-cursor`
79
+ * (and similar plugins) that accept a `WsProviderHandle`.
80
+ * Broadcast / partition APIs are no-ops (y-websocket has no equivalent).
81
+ */
82
+ wsProvider: WsProviderHandle;
83
+ /** Manually disconnect. The provider stays disconnected until `resume()` is called. */
84
+ disconnect(): void;
85
+ /** Reconnect after a manual `disconnect()` or an idle timeout. */
86
+ resume(): void;
87
+ /** Tear down all resources — removes listeners, destroys provider and awareness. */
88
+ destroy(): void;
89
+ }
90
+ export type { WsConnectionStatus };
91
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAChF,OAAO,KAAK,KAAK,CAAC,MAAM,KAAK,CAAC;AAC9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,MAAM,WAAW,gBAAgB;IAChC,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,oBAAoB;IACpC;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mEAAmE;IACnE,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACrC,yEAAyE;IACzE,GAAG,EAAE,MAAM,CAAC;IACZ,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,oBAAoB,KAAK,OAAO,CAAC,gBAAgB,CAAC,GAAG,gBAAgB,CAAC;IAE5F;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;IAE7E;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;OAGG;IACH,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC;IAEZ;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,oBAAoB;IACpC,8BAA8B;IAC9B,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC;IACX,kEAAkE;IAClE,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;;;;OAKG;IACH,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1B;;;;OAIG;IACH,UAAU,EAAE,gBAAgB,CAAC;IAE7B,uFAAuF;IACvF,UAAU,IAAI,IAAI,CAAC;IACnB,kEAAkE;IAClE,MAAM,IAAI,IAAI,CAAC;IAEf,oFAAoF;IACpF,OAAO,IAAI,IAAI,CAAC;CAChB;AAED,YAAY,EAAE,kBAAkB,EAAE,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,21 @@
1
+ import type { BoardStore, ShapeRegistry, Viewport } from "@edv4h/usketch-shared";
2
+ import type { SyncStatusTracker } from "./sync-status-tracker.js";
3
+ interface UnconfirmedOverlayProps {
4
+ store: BoardStore;
5
+ shapes: ShapeRegistry;
6
+ viewport: Viewport;
7
+ syncStatus: SyncStatusTracker;
8
+ }
9
+ /**
10
+ * SVG overlay that draws a small red exclamation badge on the top-right corner
11
+ * of any shape whose ID appears in `syncStatus.getSnapshot().unconfirmedShapeIds`
12
+ * — i.e. shapes that exist in the local Y.Doc but the server hasn't
13
+ * acknowledged. The debug HUD uses the `⚠` glyph; here we render a circle +
14
+ * `!` so the badge stays legible at small zoom levels where multi-codepoint
15
+ * emoji distort.
16
+ *
17
+ * This is purely diagnostic; clicks pass through to the underlying shape.
18
+ */
19
+ export declare function UnconfirmedOverlay({ store, shapes, viewport, syncStatus, }: UnconfirmedOverlayProps): import("react/jsx-runtime").JSX.Element | null;
20
+ export {};
21
+ //# sourceMappingURL=unconfirmed-overlay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unconfirmed-overlay.d.ts","sourceRoot":"","sources":["../src/unconfirmed-overlay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEjF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,UAAU,uBAAuB;IAChC,KAAK,EAAE,UAAU,CAAC;IAClB,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,EAAE,iBAAiB,CAAC;CAC9B;AAMD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,EAClC,KAAK,EACL,MAAM,EACN,QAAQ,EACR,UAAU,GACV,EAAE,uBAAuB,kDAqCzB"}
@@ -0,0 +1,61 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useSyncExternalStore } from "react";
3
+ const BADGE_FILL = "#dc2626";
4
+ const BADGE_RADIUS = 9;
5
+ const BADGE_OFFSET = 4;
6
+ /**
7
+ * SVG overlay that draws a small red exclamation badge on the top-right corner
8
+ * of any shape whose ID appears in `syncStatus.getSnapshot().unconfirmedShapeIds`
9
+ * — i.e. shapes that exist in the local Y.Doc but the server hasn't
10
+ * acknowledged. The debug HUD uses the `⚠` glyph; here we render a circle +
11
+ * `!` so the badge stays legible at small zoom levels where multi-codepoint
12
+ * emoji distort.
13
+ *
14
+ * This is purely diagnostic; clicks pass through to the underlying shape.
15
+ */
16
+ export function UnconfirmedOverlay({ store, shapes, viewport, syncStatus, }) {
17
+ // Keep `subscribe` / `getSnapshot` identities stable across renders so that
18
+ // re-renders triggered by viewport changes don't churn through unsubscribe
19
+ // /resubscribe cycles. Dependent only on `syncStatus`, which is itself
20
+ // stable for the lifetime of the plugin.
21
+ const subscribe = useCallback((listener) => syncStatus.subscribe(listener), [syncStatus]);
22
+ const getSnapshot = useCallback(() => syncStatus.getSnapshot(), [syncStatus]);
23
+ const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
24
+ // Only surface divergence after the server has confirmed us at least once.
25
+ // `firstServerSyncAt` is set exactly by `setConfirmedFromServer(...)` so
26
+ // it's the only field that means "we've actually heard back from the
27
+ // server" — `lastSyncedAt` also moves on local edits and would let warnings
28
+ // leak out during the initial connecting phase. Once set, the gate stays
29
+ // open across later disconnections so offline edits surface as divergence.
30
+ if (snapshot.firstServerSyncAt === null)
31
+ return null;
32
+ if (snapshot.unconfirmedShapeIds.length === 0)
33
+ return null;
34
+ return (_jsxs("svg", { style: {
35
+ position: "absolute",
36
+ inset: 0,
37
+ width: "100%",
38
+ height: "100%",
39
+ pointerEvents: "none",
40
+ }, children: [_jsx("title", { children: "\u30B5\u30FC\u30D0\u672A\u540C\u671F Shape" }), snapshot.unconfirmedShapeIds.map((id) => (_jsx(UnconfirmedBadge, { store: store, shapes: shapes, viewport: viewport, shapeId: id }, id)))] }));
41
+ }
42
+ function UnconfirmedBadge({ store, shapes, viewport, shapeId, }) {
43
+ const shape = store.getShape(shapeId);
44
+ if (!shape)
45
+ return null;
46
+ // Use the shape definition's bounds when available, falling back to the
47
+ // raw `x/y/width/height` for shapes without a registered type (e.g. legacy
48
+ // shape types that disappeared after a refactor — the very case this
49
+ // overlay was designed to surface).
50
+ const def = shapes.get(shape.type);
51
+ const bounds = def?.getBounds(shape) ?? {
52
+ x: shape.x,
53
+ y: shape.y,
54
+ width: shape.width,
55
+ height: shape.height,
56
+ };
57
+ const cx = (bounds.x + bounds.width) * viewport.zoom + viewport.x + BADGE_OFFSET;
58
+ const cy = bounds.y * viewport.zoom + viewport.y - BADGE_OFFSET;
59
+ return (_jsxs("g", { children: [_jsx("title", { children: `${shape.type} (${shapeId.slice(0, 8)}…) — サーバに存在しません` }), _jsx("circle", { cx: cx, cy: cy, r: BADGE_RADIUS, fill: BADGE_FILL }), _jsx("text", { x: cx, y: cy + 1, textAnchor: "middle", dominantBaseline: "middle", fontSize: 11, fontWeight: 700, fill: "#ffffff", style: { userSelect: "none" }, children: "!" })] }));
60
+ }
61
+ //# sourceMappingURL=unconfirmed-overlay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unconfirmed-overlay.js","sourceRoot":"","sources":["../src/unconfirmed-overlay.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { BoardStore } from "@edv4h/usketch-shared";
2
+ import type { YwebsocketSyncHandle, YwebsocketSyncOptions } from "./types.js";
3
+ export declare function createYwebsocketSync(store: BoardStore, options: YwebsocketSyncOptions): YwebsocketSyncHandle;
4
+ //# sourceMappingURL=yws-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"yws-sync.d.ts","sourceRoot":"","sources":["../src/yws-sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAa,MAAM,uBAAuB,CAAC;AAMnE,OAAO,KAAK,EAAwB,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAWpG,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,qBAAqB,GAC5B,oBAAoB,CAqgBtB"}