@fluxstack/live-client 0.1.0 → 0.2.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/README.md +179 -0
- package/dist/index.d.ts +96 -1
- package/dist/index.js +145 -6
- package/dist/index.js.map +1 -1
- package/dist/live-client.browser.global.js +145 -6
- package/dist/live-client.browser.global.js.map +1 -1
- package/package.json +3 -3
|
@@ -31,8 +31,11 @@ var FluxstackLive = (() => {
|
|
|
31
31
|
StateValidator: () => StateValidator,
|
|
32
32
|
clearPersistedState: () => clearPersistedState,
|
|
33
33
|
createBinaryChunkMessage: () => createBinaryChunkMessage,
|
|
34
|
+
getConnection: () => getConnection,
|
|
34
35
|
getPersistedState: () => getPersistedState,
|
|
35
|
-
|
|
36
|
+
onConnectionChange: () => onConnectionChange,
|
|
37
|
+
persistState: () => persistState,
|
|
38
|
+
useLive: () => useLive
|
|
36
39
|
});
|
|
37
40
|
|
|
38
41
|
// src/connection.ts
|
|
@@ -44,6 +47,7 @@ var FluxstackLive = (() => {
|
|
|
44
47
|
__publicField(this, "reconnectTimeout", null);
|
|
45
48
|
__publicField(this, "heartbeatInterval", null);
|
|
46
49
|
__publicField(this, "componentCallbacks", /* @__PURE__ */ new Map());
|
|
50
|
+
__publicField(this, "binaryCallbacks", /* @__PURE__ */ new Map());
|
|
47
51
|
__publicField(this, "pendingRequests", /* @__PURE__ */ new Map());
|
|
48
52
|
__publicField(this, "stateListeners", /* @__PURE__ */ new Set());
|
|
49
53
|
__publicField(this, "_state", {
|
|
@@ -123,6 +127,7 @@ var FluxstackLive = (() => {
|
|
|
123
127
|
this.log("Connecting...", { url });
|
|
124
128
|
try {
|
|
125
129
|
const ws = new WebSocket(url);
|
|
130
|
+
ws.binaryType = "arraybuffer";
|
|
126
131
|
this.ws = ws;
|
|
127
132
|
ws.onopen = () => {
|
|
128
133
|
this.log("Connected");
|
|
@@ -131,19 +136,34 @@ var FluxstackLive = (() => {
|
|
|
131
136
|
this.startHeartbeat();
|
|
132
137
|
};
|
|
133
138
|
ws.onmessage = (event) => {
|
|
139
|
+
if (event.data instanceof ArrayBuffer) {
|
|
140
|
+
this.handleBinaryMessage(new Uint8Array(event.data));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
134
143
|
try {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
144
|
+
const parsed = JSON.parse(event.data);
|
|
145
|
+
if (Array.isArray(parsed)) {
|
|
146
|
+
for (const msg of parsed) {
|
|
147
|
+
this.log("Received", { type: msg.type, componentId: msg.componentId });
|
|
148
|
+
this.handleMessage(msg);
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
this.log("Received", { type: parsed.type, componentId: parsed.componentId });
|
|
152
|
+
this.handleMessage(parsed);
|
|
153
|
+
}
|
|
138
154
|
} catch {
|
|
139
155
|
this.log("Failed to parse message");
|
|
140
156
|
this.setState({ error: "Failed to parse message" });
|
|
141
157
|
}
|
|
142
158
|
};
|
|
143
|
-
ws.onclose = () => {
|
|
144
|
-
this.log("Disconnected");
|
|
159
|
+
ws.onclose = (event) => {
|
|
160
|
+
this.log("Disconnected", { code: event.code, reason: event.reason });
|
|
145
161
|
this.setState({ connected: false, connecting: false, connectionId: null });
|
|
146
162
|
this.stopHeartbeat();
|
|
163
|
+
if (event.code === 4003) {
|
|
164
|
+
this.setState({ error: "Connection rejected: origin not allowed" });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
147
167
|
this.attemptReconnect();
|
|
148
168
|
};
|
|
149
169
|
ws.onerror = () => {
|
|
@@ -318,6 +338,25 @@ var FluxstackLive = (() => {
|
|
|
318
338
|
}
|
|
319
339
|
});
|
|
320
340
|
}
|
|
341
|
+
/** Parse and route a binary BINARY_STATE_DELTA frame */
|
|
342
|
+
handleBinaryMessage(buffer) {
|
|
343
|
+
if (buffer.length < 3 || buffer[0] !== 1) return;
|
|
344
|
+
const idLen = buffer[1];
|
|
345
|
+
if (buffer.length < 2 + idLen) return;
|
|
346
|
+
const componentId = new TextDecoder().decode(buffer.subarray(2, 2 + idLen));
|
|
347
|
+
const payload = buffer.subarray(2 + idLen);
|
|
348
|
+
const callback = this.binaryCallbacks.get(componentId);
|
|
349
|
+
if (callback) {
|
|
350
|
+
callback(payload);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/** Register a binary message handler for a component */
|
|
354
|
+
registerBinaryHandler(componentId, callback) {
|
|
355
|
+
this.binaryCallbacks.set(componentId, callback);
|
|
356
|
+
return () => {
|
|
357
|
+
this.binaryCallbacks.delete(componentId);
|
|
358
|
+
};
|
|
359
|
+
}
|
|
321
360
|
/** Register a component message callback */
|
|
322
361
|
registerComponent(componentId, callback) {
|
|
323
362
|
this.log("Registering component", componentId);
|
|
@@ -353,6 +392,7 @@ var FluxstackLive = (() => {
|
|
|
353
392
|
destroy() {
|
|
354
393
|
this.disconnect();
|
|
355
394
|
this.componentCallbacks.clear();
|
|
395
|
+
this.binaryCallbacks.clear();
|
|
356
396
|
for (const [, req] of this.pendingRequests) {
|
|
357
397
|
clearTimeout(req.timeout);
|
|
358
398
|
req.reject(new Error("Connection destroyed"));
|
|
@@ -511,6 +551,21 @@ var FluxstackLive = (() => {
|
|
|
511
551
|
}
|
|
512
552
|
return response.result;
|
|
513
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* Fire an action without waiting for a response (fire-and-forget).
|
|
556
|
+
* Useful for high-frequency operations like game input where the
|
|
557
|
+
* server doesn't need to send back a result.
|
|
558
|
+
*/
|
|
559
|
+
fire(action, payload = {}) {
|
|
560
|
+
if (!this._mounted || !this._componentId) return;
|
|
561
|
+
this.connection.sendMessage({
|
|
562
|
+
type: "CALL_ACTION",
|
|
563
|
+
componentId: this._componentId,
|
|
564
|
+
action,
|
|
565
|
+
payload,
|
|
566
|
+
expectResponse: false
|
|
567
|
+
});
|
|
568
|
+
}
|
|
514
569
|
// ── State ──
|
|
515
570
|
/**
|
|
516
571
|
* Subscribe to state changes.
|
|
@@ -523,6 +578,26 @@ var FluxstackLive = (() => {
|
|
|
523
578
|
this.stateListeners.delete(callback);
|
|
524
579
|
};
|
|
525
580
|
}
|
|
581
|
+
/**
|
|
582
|
+
* Register a binary decoder for this component.
|
|
583
|
+
* When the server sends a BINARY_STATE_DELTA frame targeting this component,
|
|
584
|
+
* the decoder converts the raw payload into a delta object which is merged into state.
|
|
585
|
+
* Returns an unsubscribe function.
|
|
586
|
+
*/
|
|
587
|
+
setBinaryDecoder(decoder) {
|
|
588
|
+
if (!this._componentId) {
|
|
589
|
+
throw new Error("Component must be mounted before setting binary decoder");
|
|
590
|
+
}
|
|
591
|
+
return this.connection.registerBinaryHandler(this._componentId, (payload) => {
|
|
592
|
+
try {
|
|
593
|
+
const delta = decoder(payload);
|
|
594
|
+
this._state = { ...this._state, ...delta };
|
|
595
|
+
this.notifyStateChange(this._state, delta);
|
|
596
|
+
} catch (e) {
|
|
597
|
+
console.error("Binary decode error:", e);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
}
|
|
526
601
|
/**
|
|
527
602
|
* Subscribe to errors.
|
|
528
603
|
* Returns an unsubscribe function.
|
|
@@ -1170,6 +1245,70 @@ var FluxstackLive = (() => {
|
|
|
1170
1245
|
};
|
|
1171
1246
|
}
|
|
1172
1247
|
};
|
|
1248
|
+
|
|
1249
|
+
// src/index.ts
|
|
1250
|
+
var _sharedConnection = null;
|
|
1251
|
+
var _sharedConnectionUrl = null;
|
|
1252
|
+
var _statusListeners = /* @__PURE__ */ new Set();
|
|
1253
|
+
function getOrCreateConnection(url) {
|
|
1254
|
+
const resolvedUrl = url ?? `ws://${typeof location !== "undefined" ? location.host : "localhost:3000"}/api/live/ws`;
|
|
1255
|
+
if (_sharedConnection && _sharedConnectionUrl === resolvedUrl) {
|
|
1256
|
+
return _sharedConnection;
|
|
1257
|
+
}
|
|
1258
|
+
if (_sharedConnection) {
|
|
1259
|
+
_sharedConnection.destroy();
|
|
1260
|
+
}
|
|
1261
|
+
_sharedConnection = new LiveConnection({ url: resolvedUrl });
|
|
1262
|
+
_sharedConnectionUrl = resolvedUrl;
|
|
1263
|
+
_sharedConnection.onStateChange((state) => {
|
|
1264
|
+
for (const cb of _statusListeners) {
|
|
1265
|
+
cb(state.connected);
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
return _sharedConnection;
|
|
1269
|
+
}
|
|
1270
|
+
function useLive(componentName, initialState, options = {}) {
|
|
1271
|
+
const { url, room, userId, autoMount = true, debug = false } = options;
|
|
1272
|
+
const connection = getOrCreateConnection(url);
|
|
1273
|
+
const handle = new LiveComponentHandle(connection, componentName, {
|
|
1274
|
+
initialState,
|
|
1275
|
+
room,
|
|
1276
|
+
userId,
|
|
1277
|
+
autoMount,
|
|
1278
|
+
debug
|
|
1279
|
+
});
|
|
1280
|
+
return {
|
|
1281
|
+
call: (action, payload) => handle.call(action, payload ?? {}),
|
|
1282
|
+
on: (callback) => handle.onStateChange(callback),
|
|
1283
|
+
onError: (callback) => handle.onError(callback),
|
|
1284
|
+
get state() {
|
|
1285
|
+
return handle.state;
|
|
1286
|
+
},
|
|
1287
|
+
get mounted() {
|
|
1288
|
+
return handle.mounted;
|
|
1289
|
+
},
|
|
1290
|
+
get componentId() {
|
|
1291
|
+
return handle.componentId;
|
|
1292
|
+
},
|
|
1293
|
+
get error() {
|
|
1294
|
+
return handle.error;
|
|
1295
|
+
},
|
|
1296
|
+
destroy: () => handle.destroy(),
|
|
1297
|
+
handle
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
function onConnectionChange(callback) {
|
|
1301
|
+
_statusListeners.add(callback);
|
|
1302
|
+
if (_sharedConnection) {
|
|
1303
|
+
callback(_sharedConnection.state.connected);
|
|
1304
|
+
}
|
|
1305
|
+
return () => {
|
|
1306
|
+
_statusListeners.delete(callback);
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
function getConnection(url) {
|
|
1310
|
+
return getOrCreateConnection(url);
|
|
1311
|
+
}
|
|
1173
1312
|
return __toCommonJS(src_exports);
|
|
1174
1313
|
})();
|
|
1175
1314
|
//# sourceMappingURL=live-client.browser.global.js.map
|