@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
|
@@ -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":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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"}
|