@brftech/filex-core 0.1.74 → 0.1.76

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.
Files changed (34) hide show
  1. package/dist/{ArchiveViewer-o8CcI64y.js → ArchiveViewer-BmpDoIWA.js} +2 -2
  2. package/dist/{ArchiveViewer-o8CcI64y.js.map → ArchiveViewer-BmpDoIWA.js.map} +1 -1
  3. package/dist/{CsvViewer-u8oNfRP1.js → CsvViewer-BAMxNxy4.js} +2 -2
  4. package/dist/{CsvViewer-u8oNfRP1.js.map → CsvViewer-BAMxNxy4.js.map} +1 -1
  5. package/dist/{DrawioViewer-BXgp480V.js → DrawioViewer-BIm8G98S.js} +2 -2
  6. package/dist/{DrawioViewer-BXgp480V.js.map → DrawioViewer-BIm8G98S.js.map} +1 -1
  7. package/dist/{EpubViewer-CSs-I3ZL.js → EpubViewer-D2OggB7W.js} +2 -2
  8. package/dist/{EpubViewer-CSs-I3ZL.js.map → EpubViewer-D2OggB7W.js.map} +1 -1
  9. package/dist/{IpynbViewer-DHY9HH_F.js → IpynbViewer-Dcew5gZQ.js} +2 -2
  10. package/dist/{IpynbViewer-DHY9HH_F.js.map → IpynbViewer-Dcew5gZQ.js.map} +1 -1
  11. package/dist/{MermaidViewer-Bly86gd2.js → MermaidViewer-iG7VuHeu.js} +2 -2
  12. package/dist/{MermaidViewer-Bly86gd2.js.map → MermaidViewer-iG7VuHeu.js.map} +1 -1
  13. package/dist/{PsdViewer-CDQGyl0o.js → PsdViewer-C-92G5j0.js} +2 -2
  14. package/dist/{PsdViewer-CDQGyl0o.js.map → PsdViewer-C-92G5j0.js.map} +1 -1
  15. package/dist/{TiffViewer-Cs9Ze9-W.js → TiffViewer-uOJVggkp.js} +2 -2
  16. package/dist/{TiffViewer-Cs9Ze9-W.js.map → TiffViewer-uOJVggkp.js.map} +1 -1
  17. package/dist/{Viewer3D-B54FtoDP.js → Viewer3D-RnzhEk_6.js} +2 -2
  18. package/dist/{Viewer3D-B54FtoDP.js.map → Viewer3D-RnzhEk_6.js.map} +1 -1
  19. package/dist/filex-core.js +1 -1
  20. package/dist/filex-core.umd.cjs +38 -38
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/index-B6jiFQPw.js +5758 -0
  23. package/dist/index-B6jiFQPw.js.map +1 -0
  24. package/dist/index.d.ts +4 -0
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +31 -0
  28. package/src/components/PresenceBar.vue +121 -0
  29. package/src/composables/useFileApi.ts +15 -0
  30. package/src/composables/useRealtime.ts +94 -0
  31. package/src/lib/realtime.ts +223 -0
  32. package/src/styles/base.css +7 -0
  33. package/dist/index-BSfDPPtn.js +0 -5523
  34. package/dist/index-BSfDPPtn.js.map +0 -1
@@ -0,0 +1,223 @@
1
+ // Realtime (WebSocket) client for filex live collaboration, bundled into the
2
+ // core explorer so EVERY consumer — the native panel AND the vendored
3
+ // webcomponent embedded in host apps — gets live folder updates + presence.
4
+ //
5
+ // Auth: the browser's native WebSocket can't set an Authorization header and,
6
+ // when embedded, connects cross-origin to fm.brf.sh. So instead of a header we
7
+ // fetch a short-lived, single-use TICKET through the host's normal API (which
8
+ // injects the real token server-side) and open `wss://…/api/ws?ticket=<t>`.
9
+ // The durable token never reaches the browser.
10
+ //
11
+ // Degradation: if the ticket or the socket is unavailable (old backend, blocked
12
+ // upgrade, unsupported env), `onFallback(true)` fires so the consumer can fall
13
+ // back to plain API polling. The page always keeps working — every send is
14
+ // guarded, connect() is wrapped, reconnects are capped.
15
+
16
+ export interface PresenceUser {
17
+ id: number;
18
+ name: string;
19
+ file?: string;
20
+ }
21
+
22
+ export interface ChangeMessage {
23
+ type: 'change';
24
+ path: string;
25
+ action: string; // create | delete | rename | move | upload | modify
26
+ name?: string;
27
+ new_name?: string;
28
+ }
29
+
30
+ export interface PresenceMessage {
31
+ type: 'presence';
32
+ path: string;
33
+ users: PresenceUser[];
34
+ }
35
+
36
+ export interface WsTicket {
37
+ ticket: string;
38
+ ws_url: string;
39
+ }
40
+
41
+ export interface RealtimeHandlers {
42
+ onChange?: (msg: ChangeMessage) => void;
43
+ onPresence?: (msg: PresenceMessage) => void;
44
+ onStatus?: (connected: boolean) => void;
45
+ /** Fires true when the live socket is unavailable (consumer should poll),
46
+ * false when a live socket is (re)established. */
47
+ onFallback?: (active: boolean) => void;
48
+ }
49
+
50
+ export interface RealtimeOptions {
51
+ /** Fetch a fresh ticket (single-use) via the host API. null → no live socket. */
52
+ getTicket: () => Promise<WsTicket | null>;
53
+ handlers: RealtimeHandlers;
54
+ }
55
+
56
+ const PING_INTERVAL_MS = 25_000;
57
+ const MAX_BACKOFF_MS = 15_000;
58
+ const MAX_RETRIES = 6;
59
+
60
+ export class RealtimeClient {
61
+ private ws: WebSocket | null = null;
62
+ private opts: RealtimeOptions;
63
+ private closed = false;
64
+ private connecting = false;
65
+ private retries = 0;
66
+ private fallback = false;
67
+ private pingTimer: ReturnType<typeof setInterval> | null = null;
68
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
69
+
70
+ // Last-known intent, replayed on (re)connect.
71
+ private currentPath: string | null = null;
72
+ private currentFocus: string | null = null;
73
+
74
+ constructor(opts: RealtimeOptions) {
75
+ this.opts = opts;
76
+ void this.connect();
77
+ }
78
+
79
+ /** Subscribe to a folder ("<adapter>://<dir>"). Swaps the active room and
80
+ * resets focus. Passing null unsubscribes (drive list / trash have no room). */
81
+ subscribe(path: string | null): void {
82
+ this.currentPath = path;
83
+ this.currentFocus = null;
84
+ if (path) this.send({ type: 'subscribe', path });
85
+ }
86
+
87
+ /** Report the file the user is focused on (or null to clear). */
88
+ setFocus(file: string | null): void {
89
+ this.currentFocus = file;
90
+ this.send({ type: 'focus', file });
91
+ }
92
+
93
+ /** Tear down permanently — call on unmount. */
94
+ close(): void {
95
+ this.closed = true;
96
+ this.clearTimers();
97
+ this.dropSocket();
98
+ }
99
+
100
+ private async connect(): Promise<void> {
101
+ if (this.closed || this.connecting || this.ws) return;
102
+ this.connecting = true;
103
+ let ticket: WsTicket | null = null;
104
+ try {
105
+ ticket = await this.opts.getTicket();
106
+ } catch {
107
+ ticket = null;
108
+ }
109
+ this.connecting = false;
110
+ if (this.closed) return;
111
+ if (!ticket || !ticket.ticket || !ticket.ws_url) {
112
+ this.fail();
113
+ return;
114
+ }
115
+
116
+ const sep = ticket.ws_url.includes('?') ? '&' : '?';
117
+ let ws: WebSocket;
118
+ try {
119
+ ws = new WebSocket(`${ticket.ws_url}${sep}ticket=${encodeURIComponent(ticket.ticket)}`);
120
+ } catch {
121
+ this.fail();
122
+ return;
123
+ }
124
+ this.ws = ws;
125
+
126
+ ws.onopen = () => {
127
+ this.retries = 0;
128
+ this.setFallback(false);
129
+ this.opts.handlers.onStatus?.(true);
130
+ if (this.currentPath) this.send({ type: 'subscribe', path: this.currentPath });
131
+ if (this.currentFocus !== null) this.send({ type: 'focus', file: this.currentFocus });
132
+ this.startPing();
133
+ };
134
+
135
+ ws.onmessage = (ev: MessageEvent) => {
136
+ let msg: unknown;
137
+ try {
138
+ msg = JSON.parse(typeof ev.data === 'string' ? ev.data : '');
139
+ } catch {
140
+ return;
141
+ }
142
+ const m = msg as { type?: string };
143
+ if (m?.type === 'change') this.opts.handlers.onChange?.(msg as ChangeMessage);
144
+ else if (m?.type === 'presence') this.opts.handlers.onPresence?.(msg as PresenceMessage);
145
+ // pong / error frames are intentionally ignored by the UI.
146
+ };
147
+
148
+ ws.onerror = () => {
149
+ // onclose fires next; reconnect is handled there.
150
+ };
151
+
152
+ ws.onclose = () => {
153
+ this.stopPing();
154
+ this.ws = null;
155
+ this.opts.handlers.onStatus?.(false);
156
+ this.fail();
157
+ };
158
+ }
159
+
160
+ // fail schedules a capped reconnect; after MAX_RETRIES it gives up on the live
161
+ // socket and flips to fallback (polling) mode.
162
+ private fail(): void {
163
+ if (this.closed || this.reconnectTimer) return;
164
+ if (this.retries >= MAX_RETRIES) {
165
+ this.setFallback(true);
166
+ return;
167
+ }
168
+ const delay = Math.min(1000 * 2 ** this.retries, MAX_BACKOFF_MS);
169
+ this.retries += 1;
170
+ this.reconnectTimer = setTimeout(() => {
171
+ this.reconnectTimer = null;
172
+ void this.connect();
173
+ }, delay);
174
+ }
175
+
176
+ private setFallback(active: boolean): void {
177
+ if (this.fallback === active) return;
178
+ this.fallback = active;
179
+ this.opts.handlers.onFallback?.(active);
180
+ }
181
+
182
+ private send(obj: unknown): void {
183
+ const ws = this.ws;
184
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
185
+ try {
186
+ ws.send(JSON.stringify(obj));
187
+ } catch {
188
+ /* a failed send just means a missed live update */
189
+ }
190
+ }
191
+
192
+ private startPing(): void {
193
+ this.stopPing();
194
+ this.pingTimer = setInterval(() => this.send({ type: 'ping' }), PING_INTERVAL_MS);
195
+ }
196
+
197
+ private stopPing(): void {
198
+ if (this.pingTimer) {
199
+ clearInterval(this.pingTimer);
200
+ this.pingTimer = null;
201
+ }
202
+ }
203
+
204
+ private dropSocket(): void {
205
+ if (this.ws) {
206
+ try {
207
+ this.ws.onclose = null; // suppress reconnect
208
+ this.ws.close();
209
+ } catch {
210
+ /* ignore */
211
+ }
212
+ this.ws = null;
213
+ }
214
+ }
215
+
216
+ private clearTimers(): void {
217
+ this.stopPing();
218
+ if (this.reconnectTimer) {
219
+ clearTimeout(this.reconnectTimer);
220
+ this.reconnectTimer = null;
221
+ }
222
+ }
223
+ }
@@ -26,6 +26,13 @@
26
26
  background: var(--fe-bg);
27
27
  }
28
28
 
29
+ /* Live-presence strip (who else is in this folder) — shown under the breadcrumb. */
30
+ .fe__presence {
31
+ flex: 0 0 auto;
32
+ padding: 0.25rem 0.75rem;
33
+ border-bottom: 1px solid var(--fe-border, rgba(0, 0, 0, 0.06));
34
+ }
35
+
29
36
  /* Initial-load spinner (shown until the first listing arrives). */
30
37
  .fe__loading {
31
38
  display: flex;