@fluxstack/live-client 0.3.1 → 0.5.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/dist/index.cjs ADDED
@@ -0,0 +1,1601 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AdaptiveChunkSizer: () => AdaptiveChunkSizer,
24
+ ChunkedUploader: () => ChunkedUploader,
25
+ LiveComponentHandle: () => LiveComponentHandle,
26
+ LiveConnection: () => LiveConnection,
27
+ RoomManager: () => RoomManager,
28
+ StateValidator: () => StateValidator,
29
+ clearPersistedState: () => clearPersistedState,
30
+ createBinaryChunkMessage: () => createBinaryChunkMessage,
31
+ getConnection: () => getConnection,
32
+ getPersistedState: () => getPersistedState,
33
+ onConnectionChange: () => onConnectionChange,
34
+ persistState: () => persistState,
35
+ useLive: () => useLive
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/connection.ts
40
+ var LiveConnection = class {
41
+ ws = null;
42
+ options;
43
+ reconnectAttempts = 0;
44
+ reconnectTimeout = null;
45
+ heartbeatInterval = null;
46
+ componentCallbacks = /* @__PURE__ */ new Map();
47
+ binaryCallbacks = /* @__PURE__ */ new Map();
48
+ roomBinaryHandlers = /* @__PURE__ */ new Set();
49
+ pendingRequests = /* @__PURE__ */ new Map();
50
+ stateListeners = /* @__PURE__ */ new Set();
51
+ _state = {
52
+ connected: false,
53
+ connecting: false,
54
+ error: null,
55
+ connectionId: null,
56
+ authenticated: false,
57
+ auth: { authenticated: false, session: null }
58
+ };
59
+ constructor(options = {}) {
60
+ this.options = {
61
+ url: options.url,
62
+ auth: options.auth,
63
+ autoConnect: options.autoConnect ?? true,
64
+ reconnectInterval: options.reconnectInterval ?? 1e3,
65
+ maxReconnectAttempts: options.maxReconnectAttempts ?? 5,
66
+ heartbeatInterval: options.heartbeatInterval ?? 3e4,
67
+ debug: options.debug ?? false
68
+ };
69
+ if (this.options.autoConnect) {
70
+ this.connect();
71
+ }
72
+ }
73
+ get state() {
74
+ return { ...this._state };
75
+ }
76
+ /** Subscribe to connection state changes */
77
+ onStateChange(callback) {
78
+ this.stateListeners.add(callback);
79
+ return () => {
80
+ this.stateListeners.delete(callback);
81
+ };
82
+ }
83
+ setState(patch) {
84
+ this._state = { ...this._state, ...patch };
85
+ for (const cb of this.stateListeners) {
86
+ cb(this._state);
87
+ }
88
+ }
89
+ getWebSocketUrl() {
90
+ if (this.options.url) {
91
+ return this.options.url;
92
+ } else if (typeof window === "undefined") {
93
+ return "ws://localhost:3000/api/live/ws";
94
+ } else {
95
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
96
+ return `${protocol}//${window.location.host}/api/live/ws`;
97
+ }
98
+ }
99
+ log(message, data) {
100
+ if (this.options.debug) {
101
+ console.log(`[LiveConnection] ${message}`, data || "");
102
+ }
103
+ }
104
+ /** Generate unique request ID */
105
+ generateRequestId() {
106
+ return `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
107
+ }
108
+ /** Connect to WebSocket server */
109
+ connect() {
110
+ if (this.ws?.readyState === WebSocket.CONNECTING) {
111
+ this.log("Already connecting, skipping...");
112
+ return;
113
+ }
114
+ if (this.ws?.readyState === WebSocket.OPEN) {
115
+ this.log("Already connected, skipping...");
116
+ return;
117
+ }
118
+ this.setState({ connecting: true, error: null });
119
+ const url = this.getWebSocketUrl();
120
+ this.log("Connecting...", { url });
121
+ try {
122
+ const ws = new WebSocket(url);
123
+ ws.binaryType = "arraybuffer";
124
+ this.ws = ws;
125
+ ws.onopen = () => {
126
+ this.log("Connected");
127
+ this.setState({ connected: true, connecting: false });
128
+ this.reconnectAttempts = 0;
129
+ this.startHeartbeat();
130
+ };
131
+ ws.onmessage = (event) => {
132
+ if (event.data instanceof ArrayBuffer) {
133
+ this.handleBinaryMessage(new Uint8Array(event.data));
134
+ return;
135
+ }
136
+ try {
137
+ const parsed = JSON.parse(event.data);
138
+ if (Array.isArray(parsed)) {
139
+ for (const msg of parsed) {
140
+ this.log("Received", { type: msg.type, componentId: msg.componentId });
141
+ this.handleMessage(msg);
142
+ }
143
+ } else {
144
+ this.log("Received", { type: parsed.type, componentId: parsed.componentId });
145
+ this.handleMessage(parsed);
146
+ }
147
+ } catch {
148
+ this.log("Failed to parse message");
149
+ this.setState({ error: "Failed to parse message" });
150
+ }
151
+ };
152
+ ws.onclose = (event) => {
153
+ this.log("Disconnected", { code: event.code, reason: event.reason });
154
+ this.setState({ connected: false, connecting: false, connectionId: null, authenticated: false, auth: { authenticated: false, session: null } });
155
+ this.stopHeartbeat();
156
+ if (event.code === 4003) {
157
+ this.setState({ error: "Connection rejected: origin not allowed" });
158
+ return;
159
+ }
160
+ this.attemptReconnect();
161
+ };
162
+ ws.onerror = () => {
163
+ this.log("WebSocket error");
164
+ this.setState({ error: "WebSocket connection error", connecting: false });
165
+ };
166
+ } catch (error) {
167
+ this.setState({
168
+ connecting: false,
169
+ error: error instanceof Error ? error.message : "Connection failed"
170
+ });
171
+ }
172
+ }
173
+ /** Disconnect from WebSocket server */
174
+ disconnect() {
175
+ if (this.reconnectTimeout) {
176
+ clearTimeout(this.reconnectTimeout);
177
+ this.reconnectTimeout = null;
178
+ }
179
+ this.stopHeartbeat();
180
+ if (this.ws) {
181
+ this.ws.close();
182
+ this.ws = null;
183
+ }
184
+ this.reconnectAttempts = this.options.maxReconnectAttempts;
185
+ this.setState({ connected: false, connecting: false, connectionId: null });
186
+ }
187
+ /** Manual reconnect */
188
+ reconnect() {
189
+ this.disconnect();
190
+ this.reconnectAttempts = 0;
191
+ setTimeout(() => this.connect(), 100);
192
+ }
193
+ attemptReconnect() {
194
+ if (this.reconnectAttempts < this.options.maxReconnectAttempts) {
195
+ this.reconnectAttempts++;
196
+ this.log(`Reconnecting... (${this.reconnectAttempts}/${this.options.maxReconnectAttempts})`);
197
+ this.reconnectTimeout = setTimeout(() => this.connect(), this.options.reconnectInterval);
198
+ } else {
199
+ this.setState({ error: "Max reconnection attempts reached" });
200
+ }
201
+ }
202
+ startHeartbeat() {
203
+ this.stopHeartbeat();
204
+ this.heartbeatInterval = setInterval(() => {
205
+ if (this.ws?.readyState === WebSocket.OPEN) {
206
+ for (const componentId of this.componentCallbacks.keys()) {
207
+ this.sendMessage({
208
+ type: "COMPONENT_PING",
209
+ componentId,
210
+ timestamp: Date.now()
211
+ }).catch(() => {
212
+ });
213
+ }
214
+ }
215
+ }, this.options.heartbeatInterval);
216
+ }
217
+ stopHeartbeat() {
218
+ if (this.heartbeatInterval) {
219
+ clearInterval(this.heartbeatInterval);
220
+ this.heartbeatInterval = null;
221
+ }
222
+ }
223
+ handleMessage(response) {
224
+ if (response.type === "CONNECTION_ESTABLISHED") {
225
+ this.setState({
226
+ connectionId: response.connectionId || null,
227
+ authenticated: response.authenticated || false
228
+ });
229
+ const auth = this.options.auth;
230
+ if (auth && Object.keys(auth).some((k) => auth[k])) {
231
+ this.sendMessageAndWait({ type: "AUTH", payload: auth }).then((authResp) => {
232
+ const payload = authResp.payload;
233
+ if (payload?.authenticated) {
234
+ this.setState({
235
+ authenticated: true,
236
+ auth: { authenticated: true, session: payload.session || null }
237
+ });
238
+ }
239
+ }).catch(() => {
240
+ });
241
+ }
242
+ }
243
+ if (response.type === "AUTH_RESPONSE") {
244
+ const payload = response.payload;
245
+ const authenticated = payload?.authenticated || false;
246
+ this.setState({
247
+ authenticated,
248
+ auth: {
249
+ authenticated,
250
+ session: authenticated ? payload?.session || null : null
251
+ }
252
+ });
253
+ }
254
+ if (response.requestId && this.pendingRequests.has(response.requestId)) {
255
+ const request = this.pendingRequests.get(response.requestId);
256
+ clearTimeout(request.timeout);
257
+ this.pendingRequests.delete(response.requestId);
258
+ if (response.success !== false) {
259
+ request.resolve(response);
260
+ } else {
261
+ if (response.error?.includes?.("COMPONENT_REHYDRATION_REQUIRED")) {
262
+ request.resolve(response);
263
+ } else {
264
+ request.reject(new Error(response.error || "Request failed"));
265
+ }
266
+ }
267
+ return;
268
+ }
269
+ if (response.type === "BROADCAST") {
270
+ this.componentCallbacks.forEach((callback, compId) => {
271
+ if (compId !== response.componentId) {
272
+ callback(response);
273
+ }
274
+ });
275
+ return;
276
+ }
277
+ if (response.componentId) {
278
+ const callback = this.componentCallbacks.get(response.componentId);
279
+ if (callback) {
280
+ callback(response);
281
+ } else {
282
+ this.log("No callback registered for component:", response.componentId);
283
+ }
284
+ }
285
+ }
286
+ /** Send message without waiting for response */
287
+ async sendMessage(message) {
288
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
289
+ throw new Error("WebSocket is not connected");
290
+ }
291
+ const messageWithTimestamp = { ...message, timestamp: Date.now() };
292
+ this.ws.send(JSON.stringify(messageWithTimestamp));
293
+ this.log("Sent", { type: message.type, componentId: message.componentId });
294
+ }
295
+ /** Send message and wait for response */
296
+ async sendMessageAndWait(message, timeout = 1e4) {
297
+ return new Promise((resolve, reject) => {
298
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
299
+ reject(new Error("WebSocket is not connected"));
300
+ return;
301
+ }
302
+ const requestId = this.generateRequestId();
303
+ const timeoutHandle = setTimeout(() => {
304
+ this.pendingRequests.delete(requestId);
305
+ reject(new Error(`Request timeout after ${timeout}ms`));
306
+ }, timeout);
307
+ this.pendingRequests.set(requestId, { resolve, reject, timeout: timeoutHandle });
308
+ try {
309
+ const messageWithRequestId = {
310
+ ...message,
311
+ requestId,
312
+ expectResponse: true,
313
+ timestamp: Date.now()
314
+ };
315
+ this.ws.send(JSON.stringify(messageWithRequestId));
316
+ this.log("Sent with requestId", { requestId, type: message.type });
317
+ } catch (error) {
318
+ clearTimeout(timeoutHandle);
319
+ this.pendingRequests.delete(requestId);
320
+ reject(error);
321
+ }
322
+ });
323
+ }
324
+ /** Send binary data and wait for response (for file uploads) */
325
+ async sendBinaryAndWait(data, requestId, timeout = 1e4) {
326
+ return new Promise((resolve, reject) => {
327
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
328
+ reject(new Error("WebSocket is not connected"));
329
+ return;
330
+ }
331
+ const timeoutHandle = setTimeout(() => {
332
+ this.pendingRequests.delete(requestId);
333
+ reject(new Error(`Binary request timeout after ${timeout}ms`));
334
+ }, timeout);
335
+ this.pendingRequests.set(requestId, { resolve, reject, timeout: timeoutHandle });
336
+ try {
337
+ this.ws.send(data);
338
+ this.log("Sent binary", { requestId, size: data.byteLength });
339
+ } catch (error) {
340
+ clearTimeout(timeoutHandle);
341
+ this.pendingRequests.delete(requestId);
342
+ reject(error);
343
+ }
344
+ });
345
+ }
346
+ /** Parse and route binary frames (state delta, room events, room state) */
347
+ handleBinaryMessage(buffer) {
348
+ if (buffer.length < 3) return;
349
+ const frameType = buffer[0];
350
+ if (frameType === 1) {
351
+ const idLen = buffer[1];
352
+ if (buffer.length < 2 + idLen) return;
353
+ const componentId = new TextDecoder().decode(buffer.subarray(2, 2 + idLen));
354
+ const payload = buffer.subarray(2 + idLen);
355
+ const callback = this.binaryCallbacks.get(componentId);
356
+ if (callback) callback(payload);
357
+ } else if (frameType === 2 || frameType === 3) {
358
+ for (const callback of this.roomBinaryHandlers) {
359
+ callback(buffer);
360
+ }
361
+ }
362
+ }
363
+ /** Register a handler for binary room frames (0x02 / 0x03). Returns unsubscribe. */
364
+ registerRoomBinaryHandler(callback) {
365
+ this.roomBinaryHandlers.add(callback);
366
+ return () => {
367
+ this.roomBinaryHandlers.delete(callback);
368
+ };
369
+ }
370
+ /** Register a binary message handler for a component */
371
+ registerBinaryHandler(componentId, callback) {
372
+ this.binaryCallbacks.set(componentId, callback);
373
+ return () => {
374
+ this.binaryCallbacks.delete(componentId);
375
+ };
376
+ }
377
+ /** Register a component message callback */
378
+ registerComponent(componentId, callback) {
379
+ this.log("Registering component", componentId);
380
+ this.componentCallbacks.set(componentId, callback);
381
+ return () => {
382
+ this.componentCallbacks.delete(componentId);
383
+ this.log("Unregistered component", componentId);
384
+ };
385
+ }
386
+ /** Unregister a component */
387
+ unregisterComponent(componentId) {
388
+ this.componentCallbacks.delete(componentId);
389
+ }
390
+ /** Authenticate (or re-authenticate) the WebSocket connection */
391
+ async authenticate(credentials) {
392
+ try {
393
+ const response = await this.sendMessageAndWait(
394
+ { type: "AUTH", payload: credentials },
395
+ 5e3
396
+ );
397
+ const success = response.authenticated || false;
398
+ this.setState({ authenticated: success });
399
+ return success;
400
+ } catch {
401
+ return false;
402
+ }
403
+ }
404
+ /** Get the raw WebSocket instance */
405
+ getWebSocket() {
406
+ return this.ws;
407
+ }
408
+ /** Destroy the connection and clean up all resources */
409
+ destroy() {
410
+ this.disconnect();
411
+ this.componentCallbacks.clear();
412
+ this.binaryCallbacks.clear();
413
+ this.roomBinaryHandlers.clear();
414
+ for (const [, req] of this.pendingRequests) {
415
+ clearTimeout(req.timeout);
416
+ req.reject(new Error("Connection destroyed"));
417
+ }
418
+ this.pendingRequests.clear();
419
+ this.stateListeners.clear();
420
+ }
421
+ };
422
+
423
+ // src/component.ts
424
+ function isPlainObject(v) {
425
+ return v !== null && typeof v === "object" && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;
426
+ }
427
+ function deepMerge(target, source, seen) {
428
+ if (!seen) seen = /* @__PURE__ */ new Set();
429
+ if (seen.has(source)) return target;
430
+ seen.add(source);
431
+ const result = { ...target };
432
+ for (const key of Object.keys(source)) {
433
+ const newVal = source[key];
434
+ const oldVal = result[key];
435
+ if (isPlainObject(oldVal) && isPlainObject(newVal)) {
436
+ result[key] = deepMerge(oldVal, newVal, seen);
437
+ } else {
438
+ result[key] = newVal;
439
+ }
440
+ }
441
+ return result;
442
+ }
443
+ var LiveComponentHandle = class {
444
+ connection;
445
+ componentName;
446
+ options;
447
+ _componentId = null;
448
+ _state;
449
+ _mounted = false;
450
+ _mounting = false;
451
+ _error = null;
452
+ stateListeners = /* @__PURE__ */ new Set();
453
+ errorListeners = /* @__PURE__ */ new Set();
454
+ unregisterComponent = null;
455
+ unsubConnection = null;
456
+ constructor(connection, componentName, options = {}) {
457
+ this.connection = connection;
458
+ this.componentName = componentName;
459
+ this._state = options.initialState ?? {};
460
+ this.options = {
461
+ initialState: options.initialState ?? {},
462
+ room: options.room,
463
+ userId: options.userId,
464
+ autoMount: options.autoMount ?? true,
465
+ debug: options.debug ?? false
466
+ };
467
+ if (this.options.autoMount) {
468
+ if (this.connection.state.connected) {
469
+ this.mount();
470
+ }
471
+ this.unsubConnection = this.connection.onStateChange((connState) => {
472
+ if (connState.connected && !this._mounted && !this._mounting) {
473
+ this.mount();
474
+ }
475
+ });
476
+ }
477
+ }
478
+ // ── Getters ──
479
+ /** Current component state */
480
+ get state() {
481
+ return this._state;
482
+ }
483
+ /** Server-assigned component ID (null before mount) */
484
+ get componentId() {
485
+ return this._componentId;
486
+ }
487
+ /** Whether the component has been mounted */
488
+ get mounted() {
489
+ return this._mounted;
490
+ }
491
+ /** Whether the component is currently mounting */
492
+ get mounting() {
493
+ return this._mounting;
494
+ }
495
+ /** Last error message */
496
+ get error() {
497
+ return this._error;
498
+ }
499
+ // ── Lifecycle ──
500
+ /** Mount the component on the server */
501
+ async mount() {
502
+ if (this._mounted || this._mounting) return;
503
+ if (!this.connection.state.connected) {
504
+ throw new Error("Cannot mount: not connected");
505
+ }
506
+ this._mounting = true;
507
+ this._error = null;
508
+ this.log("Mounting...");
509
+ try {
510
+ const response = await this.connection.sendMessageAndWait({
511
+ type: "COMPONENT_MOUNT",
512
+ componentId: `mount-${this.componentName}`,
513
+ payload: {
514
+ component: this.componentName,
515
+ props: this.options.initialState,
516
+ room: this.options.room,
517
+ userId: this.options.userId
518
+ }
519
+ });
520
+ if (!response.success) {
521
+ throw new Error(response.error || "Mount failed");
522
+ }
523
+ const result = response.result;
524
+ this._componentId = result.componentId;
525
+ this._mounted = true;
526
+ this._mounting = false;
527
+ const serverState = result.initialState || {};
528
+ this._state = { ...this._state, ...serverState };
529
+ this.unregisterComponent = this.connection.registerComponent(
530
+ this._componentId,
531
+ (msg) => this.handleServerMessage(msg)
532
+ );
533
+ this.log("Mounted", { componentId: this._componentId });
534
+ this.notifyStateChange(this._state, null);
535
+ } catch (err) {
536
+ this._mounting = false;
537
+ const errorMsg = err instanceof Error ? err.message : String(err);
538
+ this._error = errorMsg;
539
+ this.notifyError(errorMsg);
540
+ throw err;
541
+ }
542
+ }
543
+ /** Unmount the component from the server */
544
+ async unmount() {
545
+ if (!this._mounted || !this._componentId) return;
546
+ this.log("Unmounting...");
547
+ try {
548
+ await this.connection.sendMessage({
549
+ type: "COMPONENT_UNMOUNT",
550
+ componentId: this._componentId
551
+ });
552
+ } catch {
553
+ }
554
+ this.cleanup();
555
+ }
556
+ /** Destroy the handle and clean up all resources */
557
+ destroy() {
558
+ this.unmount().catch(() => {
559
+ });
560
+ if (this.unsubConnection) {
561
+ this.unsubConnection();
562
+ this.unsubConnection = null;
563
+ }
564
+ this.stateListeners.clear();
565
+ this.errorListeners.clear();
566
+ }
567
+ // ── Actions ──
568
+ /**
569
+ * Call an action on the server component.
570
+ * Returns the action's return value.
571
+ */
572
+ async call(action, payload = {}) {
573
+ if (!this._mounted || !this._componentId) {
574
+ throw new Error(`Cannot call '${action}': component not mounted`);
575
+ }
576
+ this.log(`Calling action: ${action}`, payload);
577
+ const response = await this.connection.sendMessageAndWait({
578
+ type: "CALL_ACTION",
579
+ componentId: this._componentId,
580
+ action,
581
+ payload
582
+ });
583
+ if (!response.success) {
584
+ const errorMsg = response.error || `Action '${action}' failed`;
585
+ this._error = errorMsg;
586
+ this.notifyError(errorMsg);
587
+ throw new Error(errorMsg);
588
+ }
589
+ return response.result;
590
+ }
591
+ /**
592
+ * Fire an action without waiting for a response (fire-and-forget).
593
+ * Useful for high-frequency operations like game input where the
594
+ * server doesn't need to send back a result.
595
+ */
596
+ fire(action, payload = {}) {
597
+ if (!this._mounted || !this._componentId) return;
598
+ this.connection.sendMessage({
599
+ type: "CALL_ACTION",
600
+ componentId: this._componentId,
601
+ action,
602
+ payload,
603
+ expectResponse: false
604
+ });
605
+ }
606
+ // ── State ──
607
+ /**
608
+ * Subscribe to state changes.
609
+ * Callback receives the full new state and the delta (or null for full updates).
610
+ * Returns an unsubscribe function.
611
+ */
612
+ onStateChange(callback) {
613
+ this.stateListeners.add(callback);
614
+ return () => {
615
+ this.stateListeners.delete(callback);
616
+ };
617
+ }
618
+ /**
619
+ * Register a binary decoder for this component.
620
+ * When the server sends a BINARY_STATE_DELTA frame targeting this component,
621
+ * the decoder converts the raw payload into a delta object which is merged into state.
622
+ * Returns an unsubscribe function.
623
+ */
624
+ setBinaryDecoder(decoder) {
625
+ if (!this._componentId) {
626
+ throw new Error("Component must be mounted before setting binary decoder");
627
+ }
628
+ return this.connection.registerBinaryHandler(this._componentId, (payload) => {
629
+ try {
630
+ const delta = decoder(payload);
631
+ this._state = deepMerge(this._state, delta);
632
+ this.notifyStateChange(this._state, delta);
633
+ } catch (e) {
634
+ console.error("Binary decode error:", e);
635
+ }
636
+ });
637
+ }
638
+ /**
639
+ * Subscribe to errors.
640
+ * Returns an unsubscribe function.
641
+ */
642
+ onError(callback) {
643
+ this.errorListeners.add(callback);
644
+ return () => {
645
+ this.errorListeners.delete(callback);
646
+ };
647
+ }
648
+ // ── Internal ──
649
+ handleServerMessage(msg) {
650
+ switch (msg.type) {
651
+ case "STATE_UPDATE": {
652
+ const newState = msg.payload?.state;
653
+ if (newState) {
654
+ this._state = deepMerge(this._state, newState);
655
+ this.notifyStateChange(this._state, null);
656
+ }
657
+ break;
658
+ }
659
+ case "STATE_DELTA": {
660
+ const delta = msg.payload?.delta;
661
+ if (delta) {
662
+ this._state = deepMerge(this._state, delta);
663
+ this.notifyStateChange(this._state, delta);
664
+ }
665
+ break;
666
+ }
667
+ case "ERROR": {
668
+ const error = msg.error || "Unknown error";
669
+ this._error = error;
670
+ this.notifyError(error);
671
+ break;
672
+ }
673
+ default:
674
+ this.log("Unhandled message type:", msg.type);
675
+ }
676
+ }
677
+ notifyStateChange(state, delta) {
678
+ for (const cb of this.stateListeners) {
679
+ cb(state, delta);
680
+ }
681
+ }
682
+ notifyError(error) {
683
+ for (const cb of this.errorListeners) {
684
+ cb(error);
685
+ }
686
+ }
687
+ cleanup() {
688
+ if (this.unregisterComponent) {
689
+ this.unregisterComponent();
690
+ this.unregisterComponent = null;
691
+ }
692
+ this._componentId = null;
693
+ this._mounted = false;
694
+ this._mounting = false;
695
+ }
696
+ log(message, data) {
697
+ if (this.options.debug) {
698
+ console.log(`[Live:${this.componentName}] ${message}`, data ?? "");
699
+ }
700
+ }
701
+ };
702
+
703
+ // src/rooms.ts
704
+ function isPlainObject2(v) {
705
+ return v !== null && typeof v === "object" && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;
706
+ }
707
+ function deepMerge2(target, source, seen) {
708
+ if (!seen) seen = /* @__PURE__ */ new Set();
709
+ if (seen.has(source)) return target;
710
+ seen.add(source);
711
+ const result = { ...target };
712
+ for (const key of Object.keys(source)) {
713
+ const newVal = source[key];
714
+ const oldVal = result[key];
715
+ if (isPlainObject2(oldVal) && isPlainObject2(newVal)) {
716
+ result[key] = deepMerge2(oldVal, newVal, seen);
717
+ } else {
718
+ result[key] = newVal;
719
+ }
720
+ }
721
+ return result;
722
+ }
723
+ var BINARY_ROOM_EVENT = 2;
724
+ var BINARY_ROOM_STATE = 3;
725
+ var _decoder = new TextDecoder();
726
+ function msgpackDecode(buf) {
727
+ return _decodeAt(buf, 0).value;
728
+ }
729
+ function _decodeAt(buf, offset) {
730
+ if (offset >= buf.length) return { value: null, offset };
731
+ const byte = buf[offset];
732
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
733
+ if (byte < 128) return { value: byte, offset: offset + 1 };
734
+ if (byte >= 128 && byte <= 143) return _decodeMap(buf, offset + 1, byte & 15);
735
+ if (byte >= 144 && byte <= 159) return _decodeArr(buf, offset + 1, byte & 15);
736
+ if (byte >= 160 && byte <= 191) {
737
+ const len = byte & 31;
738
+ return { value: _decoder.decode(buf.subarray(offset + 1, offset + 1 + len)), offset: offset + 1 + len };
739
+ }
740
+ if (byte >= 224) return { value: byte - 256, offset: offset + 1 };
741
+ switch (byte) {
742
+ case 192:
743
+ return { value: null, offset: offset + 1 };
744
+ case 194:
745
+ return { value: false, offset: offset + 1 };
746
+ case 195:
747
+ return { value: true, offset: offset + 1 };
748
+ case 196: {
749
+ const l = buf[offset + 1];
750
+ return { value: buf.slice(offset + 2, offset + 2 + l), offset: offset + 2 + l };
751
+ }
752
+ case 197: {
753
+ const l = view.getUint16(offset + 1, false);
754
+ return { value: buf.slice(offset + 3, offset + 3 + l), offset: offset + 3 + l };
755
+ }
756
+ case 198: {
757
+ const l = view.getUint32(offset + 1, false);
758
+ return { value: buf.slice(offset + 5, offset + 5 + l), offset: offset + 5 + l };
759
+ }
760
+ case 203:
761
+ return { value: view.getFloat64(offset + 1, false), offset: offset + 9 };
762
+ case 204:
763
+ return { value: buf[offset + 1], offset: offset + 2 };
764
+ case 205:
765
+ return { value: view.getUint16(offset + 1, false), offset: offset + 3 };
766
+ case 206:
767
+ return { value: view.getUint32(offset + 1, false), offset: offset + 5 };
768
+ case 208:
769
+ return { value: view.getInt8(offset + 1), offset: offset + 2 };
770
+ case 209:
771
+ return { value: view.getInt16(offset + 1, false), offset: offset + 3 };
772
+ case 210:
773
+ return { value: view.getInt32(offset + 1, false), offset: offset + 5 };
774
+ case 217: {
775
+ const l = buf[offset + 1];
776
+ return { value: _decoder.decode(buf.subarray(offset + 2, offset + 2 + l)), offset: offset + 2 + l };
777
+ }
778
+ case 218: {
779
+ const l = view.getUint16(offset + 1, false);
780
+ return { value: _decoder.decode(buf.subarray(offset + 3, offset + 3 + l)), offset: offset + 3 + l };
781
+ }
782
+ case 219: {
783
+ const l = view.getUint32(offset + 1, false);
784
+ return { value: _decoder.decode(buf.subarray(offset + 5, offset + 5 + l)), offset: offset + 5 + l };
785
+ }
786
+ case 220:
787
+ return _decodeArr(buf, offset + 3, view.getUint16(offset + 1, false));
788
+ case 221:
789
+ return _decodeArr(buf, offset + 5, view.getUint32(offset + 1, false));
790
+ case 222:
791
+ return _decodeMap(buf, offset + 3, view.getUint16(offset + 1, false));
792
+ case 223:
793
+ return _decodeMap(buf, offset + 5, view.getUint32(offset + 1, false));
794
+ }
795
+ return { value: null, offset: offset + 1 };
796
+ }
797
+ function _decodeArr(buf, offset, count) {
798
+ const arr = new Array(count);
799
+ for (let i = 0; i < count; i++) {
800
+ const r = _decodeAt(buf, offset);
801
+ arr[i] = r.value;
802
+ offset = r.offset;
803
+ }
804
+ return { value: arr, offset };
805
+ }
806
+ function _decodeMap(buf, offset, count) {
807
+ const obj = {};
808
+ for (let i = 0; i < count; i++) {
809
+ const k = _decodeAt(buf, offset);
810
+ offset = k.offset;
811
+ const v = _decodeAt(buf, offset);
812
+ offset = v.offset;
813
+ obj[String(k.value)] = v.value;
814
+ }
815
+ return { value: obj, offset };
816
+ }
817
+ function parseRoomFrame(buf) {
818
+ if (buf.length < 6) return null;
819
+ let offset = 0;
820
+ const frameType = buf[offset++];
821
+ const compIdLen = buf[offset++];
822
+ if (offset + compIdLen > buf.length) return null;
823
+ const componentId = _decoder.decode(buf.subarray(offset, offset + compIdLen));
824
+ offset += compIdLen;
825
+ const roomIdLen = buf[offset++];
826
+ if (offset + roomIdLen > buf.length) return null;
827
+ const roomId = _decoder.decode(buf.subarray(offset, offset + roomIdLen));
828
+ offset += roomIdLen;
829
+ if (offset + 2 > buf.length) return null;
830
+ const eventLen = buf[offset] << 8 | buf[offset + 1];
831
+ offset += 2;
832
+ if (offset + eventLen > buf.length) return null;
833
+ const event = _decoder.decode(buf.subarray(offset, offset + eventLen));
834
+ offset += eventLen;
835
+ return { frameType, componentId, roomId, event, payload: buf.subarray(offset) };
836
+ }
837
+ var ROOM_RESERVED_KEYS = /* @__PURE__ */ new Set([
838
+ "id",
839
+ "joined",
840
+ "state",
841
+ "join",
842
+ "leave",
843
+ "emit",
844
+ "on",
845
+ "onSystem",
846
+ "setState",
847
+ "call",
848
+ "apply",
849
+ "bind",
850
+ "prototype",
851
+ "length",
852
+ "name",
853
+ "arguments",
854
+ "caller",
855
+ Symbol.toPrimitive,
856
+ Symbol.toStringTag,
857
+ Symbol.hasInstance
858
+ ]);
859
+ function wrapWithStateProxy(target, getState, setStateFn) {
860
+ return new Proxy(target, {
861
+ get(obj, prop, receiver) {
862
+ if (ROOM_RESERVED_KEYS.has(prop) || typeof prop === "symbol") {
863
+ return Reflect.get(obj, prop, receiver);
864
+ }
865
+ const desc = Object.getOwnPropertyDescriptor(obj, prop);
866
+ if (desc) return Reflect.get(obj, prop, receiver);
867
+ if (prop in obj) return Reflect.get(obj, prop, receiver);
868
+ const st = getState();
869
+ return st?.[prop];
870
+ },
871
+ set(_obj, prop, value) {
872
+ if (typeof prop === "symbol") return false;
873
+ setStateFn({ [prop]: value });
874
+ return true;
875
+ }
876
+ });
877
+ }
878
+ var RoomManager = class {
879
+ componentId;
880
+ defaultRoom;
881
+ rooms = /* @__PURE__ */ new Map();
882
+ handles = /* @__PURE__ */ new Map();
883
+ sendMessage;
884
+ sendMessageAndWait;
885
+ globalUnsubscribe = null;
886
+ binaryUnsubscribe = null;
887
+ onBinaryMessage = null;
888
+ onMessageFactory = null;
889
+ constructor(options) {
890
+ this.componentId = options.componentId;
891
+ this.defaultRoom = options.defaultRoom || null;
892
+ this.sendMessage = options.sendMessage;
893
+ this.sendMessageAndWait = options.sendMessageAndWait;
894
+ this.onBinaryMessage = options.onBinaryMessage ?? null;
895
+ this.onMessageFactory = options.onMessage;
896
+ this.globalUnsubscribe = options.onMessage((msg) => this.handleServerMessage(msg));
897
+ if (options.onBinaryMessage) {
898
+ this.binaryUnsubscribe = options.onBinaryMessage((frame) => this.handleBinaryFrame(frame));
899
+ }
900
+ }
901
+ /** Re-subscribe message and binary handlers (needed after destroy/remount in React Strict Mode) */
902
+ resubscribe() {
903
+ if (!this.globalUnsubscribe && this.onMessageFactory) {
904
+ this.globalUnsubscribe = this.onMessageFactory((msg) => this.handleServerMessage(msg));
905
+ }
906
+ if (!this.binaryUnsubscribe && this.onBinaryMessage) {
907
+ this.binaryUnsubscribe = this.onBinaryMessage((frame) => this.handleBinaryFrame(frame));
908
+ }
909
+ }
910
+ handleServerMessage(msg) {
911
+ if (msg.componentId !== this.componentId) return;
912
+ const room = this.rooms.get(msg.roomId);
913
+ if (!room) return;
914
+ switch (msg.type) {
915
+ case "ROOM_EVENT":
916
+ case "ROOM_SYSTEM": {
917
+ const handlers = room.handlers.get(msg.event);
918
+ if (handlers) {
919
+ for (const handler of handlers) {
920
+ try {
921
+ handler(msg.data);
922
+ } catch (error) {
923
+ console.error(`[Room:${msg.roomId}] Handler error for '${msg.event}':`, error);
924
+ }
925
+ }
926
+ }
927
+ break;
928
+ }
929
+ case "ROOM_STATE": {
930
+ const stateChanges = msg.data?.state ?? msg.data;
931
+ room.state = deepMerge2(room.state, stateChanges);
932
+ const stateHandlers = room.handlers.get("$state:change");
933
+ if (stateHandlers) {
934
+ for (const handler of stateHandlers) handler(stateChanges);
935
+ }
936
+ break;
937
+ }
938
+ case "ROOM_JOINED":
939
+ room.joined = true;
940
+ if (msg.data?.state) room.state = msg.data.state;
941
+ break;
942
+ case "ROOM_LEFT":
943
+ room.joined = false;
944
+ break;
945
+ }
946
+ }
947
+ /** Handle binary room frames (0x02 ROOM_EVENT, 0x03 ROOM_STATE) */
948
+ handleBinaryFrame(frame) {
949
+ const parsed = parseRoomFrame(frame);
950
+ if (!parsed) return;
951
+ if (parsed.componentId !== this.componentId) return;
952
+ const room = this.rooms.get(parsed.roomId);
953
+ if (!room) return;
954
+ const data = msgpackDecode(parsed.payload);
955
+ if (parsed.frameType === BINARY_ROOM_EVENT) {
956
+ const handlers = room.handlers.get(parsed.event);
957
+ if (handlers) {
958
+ for (const handler of handlers) {
959
+ try {
960
+ handler(data);
961
+ } catch (error) {
962
+ console.error(`[Room:${parsed.roomId}] Handler error for '${parsed.event}':`, error);
963
+ }
964
+ }
965
+ }
966
+ } else if (parsed.frameType === BINARY_ROOM_STATE) {
967
+ const stateChanges = data?.state ?? data;
968
+ room.state = deepMerge2(room.state, stateChanges);
969
+ const stateHandlers = room.handlers.get("$state:change");
970
+ if (stateHandlers) {
971
+ for (const handler of stateHandlers) handler(stateChanges);
972
+ }
973
+ }
974
+ }
975
+ getOrCreateRoom(roomId) {
976
+ if (!this.rooms.has(roomId)) {
977
+ this.rooms.set(roomId, {
978
+ joined: false,
979
+ state: {},
980
+ handlers: /* @__PURE__ */ new Map()
981
+ });
982
+ }
983
+ return this.rooms.get(roomId);
984
+ }
985
+ /** Create handle for a specific room (cached) */
986
+ createHandle(roomId) {
987
+ if (this.handles.has(roomId)) return this.handles.get(roomId);
988
+ const room = this.getOrCreateRoom(roomId);
989
+ const handle = {
990
+ get id() {
991
+ return roomId;
992
+ },
993
+ get joined() {
994
+ return room.joined;
995
+ },
996
+ get state() {
997
+ return room.state;
998
+ },
999
+ join: async (initialState) => {
1000
+ if (!this.componentId) throw new Error("Component not mounted");
1001
+ if (room.joined) return;
1002
+ if (initialState) room.state = initialState;
1003
+ const response = await this.sendMessageAndWait({
1004
+ type: "ROOM_JOIN",
1005
+ componentId: this.componentId,
1006
+ roomId,
1007
+ data: { initialState: room.state },
1008
+ timestamp: Date.now()
1009
+ }, 5e3);
1010
+ if (response?.success) {
1011
+ room.joined = true;
1012
+ if (response.state) room.state = response.state;
1013
+ }
1014
+ },
1015
+ leave: async () => {
1016
+ if (!this.componentId || !room.joined) return;
1017
+ await this.sendMessageAndWait({
1018
+ type: "ROOM_LEAVE",
1019
+ componentId: this.componentId,
1020
+ roomId,
1021
+ timestamp: Date.now()
1022
+ }, 5e3);
1023
+ room.joined = false;
1024
+ room.handlers.clear();
1025
+ },
1026
+ emit: (event, data) => {
1027
+ if (!this.componentId) return;
1028
+ this.sendMessage({
1029
+ type: "ROOM_EMIT",
1030
+ componentId: this.componentId,
1031
+ roomId,
1032
+ event,
1033
+ data,
1034
+ timestamp: Date.now()
1035
+ });
1036
+ },
1037
+ on: (event, handler) => {
1038
+ const eventKey = event;
1039
+ if (!room.handlers.has(eventKey)) room.handlers.set(eventKey, /* @__PURE__ */ new Set());
1040
+ room.handlers.get(eventKey).add(handler);
1041
+ return () => {
1042
+ room.handlers.get(eventKey)?.delete(handler);
1043
+ };
1044
+ },
1045
+ onSystem: (event, handler) => {
1046
+ const eventKey = `$${event}`;
1047
+ if (!room.handlers.has(eventKey)) room.handlers.set(eventKey, /* @__PURE__ */ new Set());
1048
+ room.handlers.get(eventKey).add(handler);
1049
+ return () => {
1050
+ room.handlers.get(eventKey)?.delete(handler);
1051
+ };
1052
+ },
1053
+ setState: (updates) => {
1054
+ if (!this.componentId) return;
1055
+ room.state = deepMerge2(room.state, updates);
1056
+ this.sendMessage({
1057
+ type: "ROOM_STATE_SET",
1058
+ componentId: this.componentId,
1059
+ roomId,
1060
+ data: updates,
1061
+ timestamp: Date.now()
1062
+ });
1063
+ }
1064
+ };
1065
+ const proxied = wrapWithStateProxy(
1066
+ handle,
1067
+ () => room.state,
1068
+ (updates) => handle.setState(updates)
1069
+ );
1070
+ this.handles.set(roomId, proxied);
1071
+ return proxied;
1072
+ }
1073
+ /** Create the $room proxy */
1074
+ createProxy() {
1075
+ const self = this;
1076
+ const proxyFn = function(roomId) {
1077
+ return self.createHandle(roomId);
1078
+ };
1079
+ const defaultHandle = this.defaultRoom ? this.createHandle(this.defaultRoom) : null;
1080
+ Object.defineProperties(proxyFn, {
1081
+ id: { get: () => this.defaultRoom },
1082
+ joined: { get: () => defaultHandle?.joined ?? false },
1083
+ state: { get: () => defaultHandle?.state ?? {} },
1084
+ join: {
1085
+ value: async (initialState) => {
1086
+ if (!defaultHandle) throw new Error("No default room set");
1087
+ return defaultHandle.join(initialState);
1088
+ }
1089
+ },
1090
+ leave: {
1091
+ value: async () => {
1092
+ if (!defaultHandle) throw new Error("No default room set");
1093
+ return defaultHandle.leave();
1094
+ }
1095
+ },
1096
+ emit: {
1097
+ value: (event, data) => {
1098
+ if (!defaultHandle) throw new Error("No default room set");
1099
+ return defaultHandle.emit(event, data);
1100
+ }
1101
+ },
1102
+ on: {
1103
+ value: (event, handler) => {
1104
+ if (!defaultHandle) throw new Error("No default room set");
1105
+ return defaultHandle.on(event, handler);
1106
+ }
1107
+ },
1108
+ onSystem: {
1109
+ value: (event, handler) => {
1110
+ if (!defaultHandle) throw new Error("No default room set");
1111
+ return defaultHandle.onSystem(event, handler);
1112
+ }
1113
+ },
1114
+ setState: {
1115
+ value: (updates) => {
1116
+ if (!defaultHandle) throw new Error("No default room set");
1117
+ return defaultHandle.setState(updates);
1118
+ }
1119
+ }
1120
+ });
1121
+ if (this.defaultRoom && defaultHandle) {
1122
+ const room = this.getOrCreateRoom(this.defaultRoom);
1123
+ return wrapWithStateProxy(
1124
+ proxyFn,
1125
+ () => room.state,
1126
+ (updates) => defaultHandle.setState(updates)
1127
+ );
1128
+ }
1129
+ return proxyFn;
1130
+ }
1131
+ /** List of rooms currently joined */
1132
+ getJoinedRooms() {
1133
+ const joined = [];
1134
+ for (const [id, room] of this.rooms) {
1135
+ if (room.joined) joined.push(id);
1136
+ }
1137
+ return joined;
1138
+ }
1139
+ /** Update componentId (when component mounts) */
1140
+ setComponentId(id) {
1141
+ this.componentId = id;
1142
+ }
1143
+ /** Cleanup — unsubscribes handlers but keeps factory refs for resubscribe() */
1144
+ destroy() {
1145
+ this.globalUnsubscribe?.();
1146
+ this.globalUnsubscribe = null;
1147
+ this.binaryUnsubscribe?.();
1148
+ this.binaryUnsubscribe = null;
1149
+ for (const [, room] of this.rooms) {
1150
+ room.handlers.clear();
1151
+ }
1152
+ this.rooms.clear();
1153
+ this.handles.clear();
1154
+ }
1155
+ };
1156
+
1157
+ // src/upload.ts
1158
+ var AdaptiveChunkSizer = class {
1159
+ config;
1160
+ currentChunkSize;
1161
+ metrics = [];
1162
+ consecutiveErrors = 0;
1163
+ consecutiveSuccesses = 0;
1164
+ constructor(config = {}) {
1165
+ this.config = {
1166
+ minChunkSize: config.minChunkSize ?? 16 * 1024,
1167
+ maxChunkSize: config.maxChunkSize ?? 1024 * 1024,
1168
+ initialChunkSize: config.initialChunkSize ?? 64 * 1024,
1169
+ targetLatency: config.targetLatency ?? 200,
1170
+ adjustmentFactor: config.adjustmentFactor ?? 1.5,
1171
+ measurementWindow: config.measurementWindow ?? 3
1172
+ };
1173
+ this.currentChunkSize = this.config.initialChunkSize;
1174
+ }
1175
+ getChunkSize() {
1176
+ return this.currentChunkSize;
1177
+ }
1178
+ recordChunkStart(_chunkIndex) {
1179
+ return Date.now();
1180
+ }
1181
+ recordChunkComplete(chunkIndex, chunkSize, startTime, success) {
1182
+ const endTime = Date.now();
1183
+ const latency = endTime - startTime;
1184
+ const throughput = success ? chunkSize / latency * 1e3 : 0;
1185
+ this.metrics.push({ chunkIndex, chunkSize, startTime, endTime, latency, throughput, success });
1186
+ if (this.metrics.length > this.config.measurementWindow * 2) {
1187
+ this.metrics = this.metrics.slice(-this.config.measurementWindow * 2);
1188
+ }
1189
+ if (success) {
1190
+ this.consecutiveSuccesses++;
1191
+ this.consecutiveErrors = 0;
1192
+ this.adjustUp(latency);
1193
+ } else {
1194
+ this.consecutiveErrors++;
1195
+ this.consecutiveSuccesses = 0;
1196
+ this.adjustDown();
1197
+ }
1198
+ }
1199
+ adjustUp(latency) {
1200
+ if (this.consecutiveSuccesses < 2) return;
1201
+ if (latency > this.config.targetLatency) return;
1202
+ const latencyRatio = this.config.targetLatency / latency;
1203
+ let newSize = Math.floor(this.currentChunkSize * Math.min(latencyRatio, this.config.adjustmentFactor));
1204
+ newSize = Math.min(newSize, this.config.maxChunkSize);
1205
+ if (newSize > this.currentChunkSize) this.currentChunkSize = newSize;
1206
+ }
1207
+ adjustDown() {
1208
+ const decreaseFactor = this.consecutiveErrors > 1 ? 2 : this.config.adjustmentFactor;
1209
+ let newSize = Math.floor(this.currentChunkSize / decreaseFactor);
1210
+ newSize = Math.max(newSize, this.config.minChunkSize);
1211
+ if (newSize < this.currentChunkSize) this.currentChunkSize = newSize;
1212
+ }
1213
+ getAverageThroughput() {
1214
+ const recent = this.metrics.slice(-this.config.measurementWindow).filter((m) => m.success);
1215
+ if (recent.length === 0) return 0;
1216
+ return recent.reduce((sum, m) => sum + m.throughput, 0) / recent.length;
1217
+ }
1218
+ getStats() {
1219
+ return {
1220
+ currentChunkSize: this.currentChunkSize,
1221
+ averageThroughput: this.getAverageThroughput(),
1222
+ consecutiveSuccesses: this.consecutiveSuccesses,
1223
+ consecutiveErrors: this.consecutiveErrors,
1224
+ totalMeasurements: this.metrics.length
1225
+ };
1226
+ }
1227
+ reset() {
1228
+ this.currentChunkSize = this.config.initialChunkSize;
1229
+ this.metrics = [];
1230
+ this.consecutiveErrors = 0;
1231
+ this.consecutiveSuccesses = 0;
1232
+ }
1233
+ };
1234
+ function createBinaryChunkMessage(header, chunkData) {
1235
+ const headerJson = JSON.stringify(header);
1236
+ const headerBytes = new TextEncoder().encode(headerJson);
1237
+ const totalSize = 4 + headerBytes.length + chunkData.length;
1238
+ const buffer = new ArrayBuffer(totalSize);
1239
+ const view = new DataView(buffer);
1240
+ const uint8View = new Uint8Array(buffer);
1241
+ view.setUint32(0, headerBytes.length, true);
1242
+ uint8View.set(headerBytes, 4);
1243
+ uint8View.set(chunkData, 4 + headerBytes.length);
1244
+ return buffer;
1245
+ }
1246
+ var ChunkedUploader = class {
1247
+ constructor(componentId, options) {
1248
+ this.componentId = componentId;
1249
+ this.options = {
1250
+ chunkSize: options.chunkSize ?? 64 * 1024,
1251
+ maxFileSize: options.maxFileSize ?? 50 * 1024 * 1024,
1252
+ allowedTypes: options.allowedTypes ?? [],
1253
+ useBinaryProtocol: options.useBinaryProtocol ?? true,
1254
+ adaptiveChunking: options.adaptiveChunking ?? false,
1255
+ ...options
1256
+ };
1257
+ if (this.options.adaptiveChunking) {
1258
+ this.adaptiveSizer = new AdaptiveChunkSizer({
1259
+ initialChunkSize: this.options.chunkSize,
1260
+ minChunkSize: this.options.chunkSize,
1261
+ maxChunkSize: 1024 * 1024,
1262
+ ...options.adaptiveConfig
1263
+ });
1264
+ }
1265
+ }
1266
+ options;
1267
+ abortController = null;
1268
+ adaptiveSizer = null;
1269
+ _state = {
1270
+ uploading: false,
1271
+ progress: 0,
1272
+ error: null,
1273
+ uploadId: null,
1274
+ bytesUploaded: 0,
1275
+ totalBytes: 0
1276
+ };
1277
+ stateListeners = /* @__PURE__ */ new Set();
1278
+ get state() {
1279
+ return { ...this._state };
1280
+ }
1281
+ onStateChange(callback) {
1282
+ this.stateListeners.add(callback);
1283
+ return () => {
1284
+ this.stateListeners.delete(callback);
1285
+ };
1286
+ }
1287
+ setState(patch) {
1288
+ this._state = { ...this._state, ...patch };
1289
+ for (const cb of this.stateListeners) cb(this._state);
1290
+ }
1291
+ async uploadFile(file) {
1292
+ const { allowedTypes, maxFileSize, chunkSize, sendMessageAndWait, sendBinaryAndWait, useBinaryProtocol } = this.options;
1293
+ const canUseBinary = useBinaryProtocol && sendBinaryAndWait;
1294
+ if (allowedTypes.length > 0 && !allowedTypes.includes(file.type)) {
1295
+ const error = `Invalid file type: ${file.type}. Allowed: ${allowedTypes.join(", ")}`;
1296
+ this.setState({ error });
1297
+ this.options.onError?.(error);
1298
+ return;
1299
+ }
1300
+ if (file.size > maxFileSize) {
1301
+ const error = `File too large: ${file.size} bytes. Max: ${maxFileSize} bytes`;
1302
+ this.setState({ error });
1303
+ this.options.onError?.(error);
1304
+ return;
1305
+ }
1306
+ try {
1307
+ const uploadId = `upload-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
1308
+ this.abortController = new AbortController();
1309
+ this.adaptiveSizer?.reset();
1310
+ this.setState({ uploading: true, progress: 0, error: null, uploadId, bytesUploaded: 0, totalBytes: file.size });
1311
+ const initialChunkSize = this.adaptiveSizer?.getChunkSize() ?? chunkSize;
1312
+ const startMessage = {
1313
+ type: "FILE_UPLOAD_START",
1314
+ componentId: this.componentId,
1315
+ uploadId,
1316
+ filename: file.name,
1317
+ fileType: file.type,
1318
+ fileSize: file.size,
1319
+ chunkSize,
1320
+ requestId: `start-${uploadId}`
1321
+ };
1322
+ const startResponse = await sendMessageAndWait(startMessage, 1e4);
1323
+ if (!startResponse?.success) throw new Error(startResponse?.error || "Failed to start upload");
1324
+ let offset = 0;
1325
+ let chunkIndex = 0;
1326
+ const estimatedTotalChunks = Math.ceil(file.size / initialChunkSize);
1327
+ while (offset < file.size) {
1328
+ if (this.abortController?.signal.aborted) throw new Error("Upload cancelled");
1329
+ const currentChunkSize = this.adaptiveSizer?.getChunkSize() ?? chunkSize;
1330
+ const chunkEnd = Math.min(offset + currentChunkSize, file.size);
1331
+ const sliceBuffer = await file.slice(offset, chunkEnd).arrayBuffer();
1332
+ const chunkBytes = new Uint8Array(sliceBuffer);
1333
+ const chunkStartTime = this.adaptiveSizer?.recordChunkStart(chunkIndex) ?? 0;
1334
+ const requestId = `chunk-${uploadId}-${chunkIndex}`;
1335
+ try {
1336
+ let progressResponse;
1337
+ if (canUseBinary) {
1338
+ const header = {
1339
+ type: "FILE_UPLOAD_CHUNK",
1340
+ componentId: this.componentId,
1341
+ uploadId,
1342
+ chunkIndex,
1343
+ totalChunks: estimatedTotalChunks,
1344
+ requestId
1345
+ };
1346
+ const binaryMessage = createBinaryChunkMessage(header, chunkBytes);
1347
+ progressResponse = await sendBinaryAndWait(binaryMessage, requestId, 1e4);
1348
+ } else {
1349
+ let binary = "";
1350
+ for (let j = 0; j < chunkBytes.length; j++) binary += String.fromCharCode(chunkBytes[j]);
1351
+ const chunkMessage = {
1352
+ type: "FILE_UPLOAD_CHUNK",
1353
+ componentId: this.componentId,
1354
+ uploadId,
1355
+ chunkIndex,
1356
+ totalChunks: estimatedTotalChunks,
1357
+ data: btoa(binary),
1358
+ requestId
1359
+ };
1360
+ progressResponse = await sendMessageAndWait(chunkMessage, 1e4);
1361
+ }
1362
+ if (progressResponse) {
1363
+ this.setState({ progress: progressResponse.progress, bytesUploaded: progressResponse.bytesUploaded });
1364
+ this.options.onProgress?.(progressResponse.progress, progressResponse.bytesUploaded, file.size);
1365
+ }
1366
+ this.adaptiveSizer?.recordChunkComplete(chunkIndex, chunkBytes.length, chunkStartTime, true);
1367
+ } catch (error) {
1368
+ this.adaptiveSizer?.recordChunkComplete(chunkIndex, chunkBytes.length, chunkStartTime, false);
1369
+ throw error;
1370
+ }
1371
+ offset += chunkBytes.length;
1372
+ chunkIndex++;
1373
+ if (!this.options.adaptiveChunking) {
1374
+ await new Promise((resolve) => setTimeout(resolve, 10));
1375
+ }
1376
+ }
1377
+ const completeMessage = {
1378
+ type: "FILE_UPLOAD_COMPLETE",
1379
+ componentId: this.componentId,
1380
+ uploadId,
1381
+ requestId: `complete-${uploadId}`
1382
+ };
1383
+ const completeResponse = await sendMessageAndWait(completeMessage, 1e4);
1384
+ if (completeResponse?.success) {
1385
+ this.setState({ uploading: false, progress: 100, bytesUploaded: file.size });
1386
+ this.options.onComplete?.(completeResponse);
1387
+ } else {
1388
+ throw new Error(completeResponse?.error || "Upload completion failed");
1389
+ }
1390
+ } catch (error) {
1391
+ this.setState({ uploading: false, error: error.message });
1392
+ this.options.onError?.(error.message);
1393
+ }
1394
+ }
1395
+ cancelUpload() {
1396
+ if (this.abortController) {
1397
+ this.abortController.abort();
1398
+ this.setState({ uploading: false, error: "Upload cancelled" });
1399
+ }
1400
+ }
1401
+ reset() {
1402
+ this._state = { uploading: false, progress: 0, error: null, uploadId: null, bytesUploaded: 0, totalBytes: 0 };
1403
+ for (const cb of this.stateListeners) cb(this._state);
1404
+ }
1405
+ };
1406
+
1407
+ // src/persistence.ts
1408
+ var STORAGE_KEY_PREFIX = "fluxstack_component_";
1409
+ var STATE_MAX_AGE = 24 * 60 * 60 * 1e3;
1410
+ function persistState(enabled, name, signedState, room, userId) {
1411
+ if (!enabled) return;
1412
+ try {
1413
+ localStorage.setItem(`${STORAGE_KEY_PREFIX}${name}`, JSON.stringify({
1414
+ componentName: name,
1415
+ signedState,
1416
+ room,
1417
+ userId,
1418
+ lastUpdate: Date.now()
1419
+ }));
1420
+ } catch {
1421
+ }
1422
+ }
1423
+ function getPersistedState(enabled, name) {
1424
+ if (!enabled) return null;
1425
+ try {
1426
+ const stored = localStorage.getItem(`${STORAGE_KEY_PREFIX}${name}`);
1427
+ if (!stored) return null;
1428
+ const state = JSON.parse(stored);
1429
+ if (Date.now() - state.lastUpdate > STATE_MAX_AGE) {
1430
+ localStorage.removeItem(`${STORAGE_KEY_PREFIX}${name}`);
1431
+ return null;
1432
+ }
1433
+ return state;
1434
+ } catch {
1435
+ return null;
1436
+ }
1437
+ }
1438
+ function clearPersistedState(enabled, name) {
1439
+ if (!enabled) return;
1440
+ try {
1441
+ localStorage.removeItem(`${STORAGE_KEY_PREFIX}${name}`);
1442
+ } catch {
1443
+ }
1444
+ }
1445
+
1446
+ // src/state-validator.ts
1447
+ var StateValidator = class {
1448
+ static generateChecksum(state) {
1449
+ const json = JSON.stringify(state, Object.keys(state).sort());
1450
+ let hash = 0;
1451
+ for (let i = 0; i < json.length; i++) {
1452
+ const char = json.charCodeAt(i);
1453
+ hash = (hash << 5) - hash + char;
1454
+ hash = hash & hash;
1455
+ }
1456
+ return Math.abs(hash).toString(16);
1457
+ }
1458
+ static createValidation(state, source = "client") {
1459
+ return {
1460
+ checksum: this.generateChecksum(state),
1461
+ version: Date.now(),
1462
+ timestamp: Date.now(),
1463
+ source
1464
+ };
1465
+ }
1466
+ static detectConflicts(clientState, serverState, excludeFields = ["lastUpdated", "version"]) {
1467
+ const conflicts = [];
1468
+ const clientKeys = Object.keys(clientState);
1469
+ const serverKeys = Object.keys(serverState);
1470
+ const allKeys = Array.from(/* @__PURE__ */ new Set([...clientKeys, ...serverKeys]));
1471
+ for (const key of allKeys) {
1472
+ if (excludeFields.includes(key)) continue;
1473
+ const clientValue = clientState?.[key];
1474
+ const serverValue = serverState?.[key];
1475
+ if (JSON.stringify(clientValue) !== JSON.stringify(serverValue)) {
1476
+ conflicts.push({
1477
+ property: key,
1478
+ clientValue,
1479
+ serverValue,
1480
+ timestamp: Date.now(),
1481
+ resolved: false
1482
+ });
1483
+ }
1484
+ }
1485
+ return conflicts;
1486
+ }
1487
+ static mergeStates(clientState, serverState, conflicts, strategy = "smart") {
1488
+ const merged = { ...clientState };
1489
+ for (const conflict of conflicts) {
1490
+ switch (strategy) {
1491
+ case "client":
1492
+ break;
1493
+ case "server":
1494
+ merged[conflict.property] = conflict.serverValue;
1495
+ break;
1496
+ case "smart":
1497
+ if (conflict.property === "lastUpdated") {
1498
+ merged[conflict.property] = conflict.serverValue;
1499
+ } else if (typeof conflict.serverValue === "number" && typeof conflict.clientValue === "number") {
1500
+ merged[conflict.property] = Math.max(conflict.serverValue, conflict.clientValue);
1501
+ } else {
1502
+ merged[conflict.property] = conflict.serverValue;
1503
+ }
1504
+ break;
1505
+ }
1506
+ }
1507
+ return merged;
1508
+ }
1509
+ static validateState(hybridState) {
1510
+ const currentChecksum = this.generateChecksum(hybridState.data);
1511
+ return currentChecksum === hybridState.validation.checksum;
1512
+ }
1513
+ static updateValidation(hybridState, source = "client") {
1514
+ return {
1515
+ ...hybridState,
1516
+ validation: this.createValidation(hybridState.data, source),
1517
+ status: "synced"
1518
+ };
1519
+ }
1520
+ };
1521
+
1522
+ // src/index.ts
1523
+ var _sharedConnection = null;
1524
+ var _sharedConnectionUrl = null;
1525
+ var _statusListeners = /* @__PURE__ */ new Set();
1526
+ function getOrCreateConnection(url) {
1527
+ const resolvedUrl = url ?? `ws://${typeof location !== "undefined" ? location.host : "localhost:3000"}/api/live/ws`;
1528
+ if (_sharedConnection && _sharedConnectionUrl === resolvedUrl) {
1529
+ return _sharedConnection;
1530
+ }
1531
+ if (_sharedConnection) {
1532
+ _sharedConnection.destroy();
1533
+ }
1534
+ _sharedConnection = new LiveConnection({ url: resolvedUrl });
1535
+ _sharedConnectionUrl = resolvedUrl;
1536
+ _sharedConnection.onStateChange((state) => {
1537
+ for (const cb of _statusListeners) {
1538
+ cb(state.connected);
1539
+ }
1540
+ });
1541
+ return _sharedConnection;
1542
+ }
1543
+ function useLive(componentName, initialState, options = {}) {
1544
+ const { url, room, userId, autoMount = true, debug = false } = options;
1545
+ const connection = getOrCreateConnection(url);
1546
+ const handle = new LiveComponentHandle(connection, componentName, {
1547
+ initialState,
1548
+ room,
1549
+ userId,
1550
+ autoMount,
1551
+ debug
1552
+ });
1553
+ return {
1554
+ call: (action, payload) => handle.call(action, payload ?? {}),
1555
+ on: (callback) => handle.onStateChange(callback),
1556
+ onError: (callback) => handle.onError(callback),
1557
+ get state() {
1558
+ return handle.state;
1559
+ },
1560
+ get mounted() {
1561
+ return handle.mounted;
1562
+ },
1563
+ get componentId() {
1564
+ return handle.componentId;
1565
+ },
1566
+ get error() {
1567
+ return handle.error;
1568
+ },
1569
+ destroy: () => handle.destroy(),
1570
+ handle
1571
+ };
1572
+ }
1573
+ function onConnectionChange(callback) {
1574
+ _statusListeners.add(callback);
1575
+ if (_sharedConnection) {
1576
+ callback(_sharedConnection.state.connected);
1577
+ }
1578
+ return () => {
1579
+ _statusListeners.delete(callback);
1580
+ };
1581
+ }
1582
+ function getConnection(url) {
1583
+ return getOrCreateConnection(url);
1584
+ }
1585
+ // Annotate the CommonJS export names for ESM import in node:
1586
+ 0 && (module.exports = {
1587
+ AdaptiveChunkSizer,
1588
+ ChunkedUploader,
1589
+ LiveComponentHandle,
1590
+ LiveConnection,
1591
+ RoomManager,
1592
+ StateValidator,
1593
+ clearPersistedState,
1594
+ createBinaryChunkMessage,
1595
+ getConnection,
1596
+ getPersistedState,
1597
+ onConnectionChange,
1598
+ persistState,
1599
+ useLive
1600
+ });
1601
+ //# sourceMappingURL=index.cjs.map