synapse-react-client
Version:
[](https://badge.fury.io/js/synapse-react-client) [](https://github.com/prettier/prettie
204 lines (203 loc) • 6.82 kB
JavaScript
import m from "./utils/json-rx/JsonRx.js";
import p from "./utils/json-rx/JsonRxNotification.js";
import a from "./utils/json-rx/JsonRxRequestComplete.js";
import f from "./utils/json-rx/JsonRxResponse.js";
import g from "./utils/json-rx/JsonRxResponseComplete.js";
import y from "./utils/MessageCounter.js";
import { splitPatch as b } from "./utils/splitPatch.js";
import { Model as S } from "json-joy/lib/json-crdt";
import { decode as k } from "json-joy/lib/json-crdt-patch/codec/compact";
import { Encoder as u } from "json-joy/lib/json-crdt/codec/structural/verbose/Encoder";
import c from "lodash-es/noop";
import C from "lodash-es/throttle";
import { Decoder as M } from "json-joy/lib/json-crdt/codec/indexed/binary/Decoder";
import { decode as P } from "cbor2";
import { fetchWithExponentialTimeout as w } from "@sage-bionetworks/synapse-client";
const v = 30 * 1024, R = 250;
class L {
socket;
model = null;
messageCounter;
replicaId;
verboseEncoder = new u();
maxPayloadSizeBytes;
throttledSendPatch;
snapshotDecoder = new M();
onModelCreate;
onGridReady;
onStatusChange;
constructor(o) {
const {
replicaId: e,
url: s,
onGridReady: t,
onStatusChange: n,
onModelCreate: r,
maxPayloadSizeBytes: d,
socket: h,
model: i,
patchThrottleMs: l
} = o;
this.messageCounter = new y(), this.replicaId = e, this.maxPayloadSizeBytes = d ?? v, this.socket = h ?? new WebSocket(s), this.onModelCreate = r ?? c, this.onGridReady = t ?? c, this.onStatusChange = n ?? c, i && (this.model = i), this.attachSocketHandlers(), this.throttledSendPatch = C(
() => this.sendPatchImmediate(),
l ?? R,
{ leading: !1, trailing: !0 }
);
}
attachSocketHandlers() {
this.socket.onopen = () => {
console.debug("Connected to the WebSocket server"), this.onStatusChange(!0, this);
}, this.socket.onmessage = (o) => {
typeof o.data == "string" ? this.handleMessage(o.data) : console.error("Received non-string message data:", o.data);
}, this.socket.onclose = () => {
console.debug("Disconnected from the WebSocket server"), this.onStatusChange(!1, this);
}, this.socket.onerror = (o) => {
console.error("WebSocket error:", o);
};
}
disconnect() {
this.throttledSendPatch.flush(), this.throttledSendPatch.cancel(), this.socket.readyState === WebSocket.OPEN ? (this.socket.close(), console.debug("WebSocket connection closed")) : console.warn("WebSocket is not open. No action taken.");
}
/**
* Flushes the local model clock and sends the resulting patch (or a clock-sync
* message) to the server so that both sides converge.
*/
sendClockSync() {
if (!this.model) {
console.error("Model is not initialized. Cannot sync model.");
return;
}
if (!this.sendPatchImmediate()) {
const e = this.verboseEncoder.encode(this.model);
this.sendSyncMessage(e.time);
}
}
handleMessage(o) {
const e = m.fromJson(JSON.parse(o));
e instanceof f ? this.handleResponse(
e
) : e instanceof g ? this.handleResponseComplete() : e instanceof p ? this.handleNotification(e) : console.warn("Unexpected WebSocket message format:", e);
}
handleResponse(o) {
const e = o.getPayload();
switch (e.type) {
case "patch":
this.handlePatchPayload([e.body]);
break;
case "patches":
this.handlePatchPayload(e.body);
break;
case "snapshot":
this.handleSnapshotPayload(e.body);
break;
default:
console.warn("Unknown payload type:", e);
break;
}
}
handlePatchPayload(o) {
try {
const e = o.map(k);
this.model ? this.model.applyBatch(e) : (this.model = S.fromPatches(e).fork(
this.replicaId
), this.onModelCreate(this.model)), this.sendClockSync();
} catch (e) {
console.error("Failed to apply patches or send clock:", e);
}
}
async handleSnapshotPayload(o) {
console.debug("Received snapshot URL from server:", o);
try {
const e = await this.fetchAndDecodeSnapshot(o);
this.model = e.fork(this.replicaId), this.onModelCreate(this.model), this.sendClockSync();
} catch (e) {
console.error("Failed to fetch or decode snapshot", e);
}
}
handleResponseComplete() {
this.onGridReady(), console.debug(
"Clocks synchronized with server. Incrementing sequence number."
);
}
handleNotification(o) {
const e = o.getMethodName();
switch (console.debug("Notification received from server:", e), e) {
case "ping":
console.debug("Received ping from server");
break;
case "connected":
console.debug("Server ready to receive patches"), this.sendSyncMessage(
this.model ? this.verboseEncoder.encode(this.model).time : void 0
);
break;
case "error":
console.warn("Error from server:", o.getPayload());
break;
case "new-patch":
if (!this.model) {
console.warn(
"Model is not initialized. Cannot handle 'new-patch' message."
);
break;
}
{
const s = this.verboseEncoder.encode(this.model);
console.debug("New patch received, syncing data:", s.time);
const t = new a(
this.messageCounter.getNext(),
"synchronize-clock",
s.time
);
this.sendMessage(t);
}
break;
default:
console.warn("Unknown notification method:", e);
break;
}
}
sendMessage(o) {
this.socket.readyState === WebSocket.OPEN ? this.socket.send(JSON.stringify(o.getJson())) : console.error(
"WebSocket is not open. Unable to send message. Current state:",
this.socket.readyState
);
}
sendPatch() {
this.throttledSendPatch();
}
/**
* @returns true if one or more patches were sent
*/
sendPatchImmediate() {
if (!this.model)
return console.warn("Model is not initialized. Cannot send patch."), !1;
const o = this.model.api.flush(), e = o.ops.length > 0;
return e && b(o, this.maxPayloadSizeBytes).forEach((t) => {
console.debug("Sending patch to server:", t);
const n = new a(
this.messageCounter.getNext(),
"patch",
t
);
this.sendMessage(n);
}), e;
}
sendSyncMessage(o) {
const e = new a(
this.messageCounter.getNext(),
"synchronize-clock",
o ?? []
);
this.sendMessage(e);
}
async fetchAndDecodeSnapshot(o) {
const t = await (await (await w(o)).blob()).arrayBuffer(), n = P(new Uint8Array(t));
return this.snapshotDecoder.decode(
n
);
}
}
export {
L as DataGridWebSocket
};
//# sourceMappingURL=DataGridWebSocket.js.map