jinaga
Version:
Data management for web and mobile applications.
243 lines • 10.4 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WsGraphClient = void 0;
const deserializer_1 = require("../http/deserializer");
const trace_1 = require("../util/trace");
const protocol_router_1 = require("./protocol-router");
const control_frame_handler_1 = require("./control-frame-handler");
class WsGraphClient {
constructor(getWsUrl, store, onBookmark, onErrorGlobal, getUserIdentity, onFactsAdded) {
this.getWsUrl = getWsUrl;
this.store = store;
this.onBookmark = onBookmark;
this.onErrorGlobal = onErrorGlobal;
this.getUserIdentity = getUserIdentity;
this.onFactsAdded = onFactsAdded;
this.socket = null;
this.activeFeeds = new Map();
this.reconnectTimer = null;
this.reconnectAttempt = 0;
this.connecting = false;
this.hasEverConnected = false;
this.pendingLines = [];
this.waitingResolver = null;
this.router = null;
this.lastSavePromise = Promise.resolve();
}
subscribe(feed, bookmark, feedRefreshIntervalSeconds) {
this.activeFeeds.set(feed, { feed, bookmark });
this.ensureConnected().then(() => this.sendSub(feed, bookmark)).catch(err => this.onErrorGlobal(err instanceof Error ? err : new Error(String(err))));
return () => {
this.activeFeeds.delete(feed);
this.sendUnsub(feed).catch(() => { });
};
}
ensureConnected() {
return __awaiter(this, void 0, void 0, function* () {
if (this.socket && this.socket.readyState === 1)
return;
if (this.connecting) {
yield new Promise(resolve => {
const check = () => {
if (!this.connecting)
resolve();
else
setTimeout(check, 50);
};
check();
});
return;
}
if (typeof WebSocket === "undefined") {
throw new Error("WebSocket is not available in this environment");
}
this.connecting = true;
try {
yield this.openSocket();
}
finally {
this.connecting = false;
}
});
}
openSocket() {
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
try {
let url = yield this.getWsUrl();
// Optionally append identity as query param if not already included
if (this.getUserIdentity) {
try {
const id = yield this.getUserIdentity();
if (id) {
const parsed = new URL(url);
if (!parsed.searchParams.has("uid")) {
parsed.searchParams.set("uid", `${encodeURIComponent(id.provider)}:${encodeURIComponent(id.id)}`);
url = parsed.toString();
}
}
}
catch ( /* ignore */_a) { /* ignore */ }
}
const socket = new WebSocket(url);
this.socket = socket;
socket.onopen = () => {
this.hasEverConnected = true;
this.reconnectAttempt = 0;
// Instantiate router and handler per-connection
const handler = new control_frame_handler_1.ControlFrameHandler((feed, bookmark) => {
// Defer BOOK processing to next macrotask, then await latest save
setTimeout(() => {
this.lastSavePromise
.catch(() => { })
.then(() => {
const active = this.activeFeeds.get(feed);
if (active) {
active.bookmark = bookmark;
this.onBookmark(feed, bookmark);
}
});
}, 0);
}, (feed, message) => {
trace_1.Trace.warn(`Feed error for ${feed}: ${message}`);
});
this.router = new protocol_router_1.WebSocketMessageRouter({
onGraphLine: (line) => {
this.pendingLines.push(line);
this.pumpWaiting();
},
}, handler);
// Start graph reader
this.startGraphReader();
// Resubscribe all feeds
for (const { feed, bookmark } of this.activeFeeds.values()) {
this.sendSub(feed, bookmark).catch(err => trace_1.Trace.warn(String(err)));
}
resolve();
};
socket.onmessage = (event) => {
var _a;
const chunk = typeof event.data === "string" ? event.data : String(event.data);
(_a = this.router) === null || _a === void 0 ? void 0 : _a.pushChunk(chunk);
};
socket.onerror = () => {
if (!this.hasEverConnected) {
this.onErrorGlobal(new Error("WebSocket error"));
}
};
socket.onclose = () => {
this.scheduleReconnect();
};
}
catch (err) {
reject(err);
}
}));
}
startGraphReader() {
const deserializer = new deserializer_1.GraphDeserializer(() => __awaiter(this, void 0, void 0, function* () {
return yield this.readLine();
}),
// In a live WebSocket stream there is no natural end-of-stream; flush each fact immediately
1);
(() => __awaiter(this, void 0, void 0, function* () {
try {
yield deserializer.read((envelopes) => __awaiter(this, void 0, void 0, function* () {
const savePromise = this.store.save(envelopes);
// Track the latest save so control frames can wait for persistence
this.lastSavePromise = savePromise.then(() => { });
const saved = yield savePromise;
if (saved.length > 0) {
trace_1.Trace.counter("facts_saved", saved.length);
// Phase 3.4: Notify facts added listener for observer notifications
if (this.onFactsAdded) {
yield this.onFactsAdded(saved);
}
}
}));
}
catch (err) {
this.onErrorGlobal(err);
}
}))();
}
pumpWaiting() {
if (this.waitingResolver && this.pendingLines.length > 0) {
const resolver = this.waitingResolver;
this.waitingResolver = null;
const line = this.pendingLines.shift();
resolver(line);
}
}
readLine() {
return new Promise((resolve) => {
const tryDequeue = () => {
if (this.pendingLines.length === 0) {
// If socket closed and buffer empty, signal EOF
if (!this.socket || this.socket.readyState === 3 /* CLOSED */) {
resolve(null);
return;
}
this.waitingResolver = resolve;
return;
}
const line = this.pendingLines.shift();
resolve(line);
};
tryDequeue();
});
}
scheduleReconnect() {
if (this.reconnectTimer)
return;
if (this.activeFeeds.size === 0) {
return; // No active subscriptions; do not reconnect
}
const delayMs = Math.min(30000, 1000 * Math.pow(2, this.reconnectAttempt));
this.reconnectAttempt = Math.min(this.reconnectAttempt + 1, 15);
this.reconnectTimer = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
this.reconnectTimer = null;
try {
yield this.ensureConnected();
}
catch (err) {
this.scheduleReconnect();
}
}), delayMs);
}
sendSub(feed, bookmark) {
return __awaiter(this, void 0, void 0, function* () {
yield this.sendFramed(["SUB", JSON.stringify(feed), JSON.stringify(bookmark), ""]);
});
}
sendUnsub(feed) {
return __awaiter(this, void 0, void 0, function* () {
yield this.sendFramed(["UNSUB", JSON.stringify(feed), ""]);
});
}
sendFramed(lines) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.socket || this.socket.readyState !== 1) {
yield this.ensureConnected();
}
try {
for (const l of lines) {
this.socket.send(l + "\n");
}
}
catch (err) {
throw err instanceof Error ? err : new Error(String(err));
}
});
}
}
exports.WsGraphClient = WsGraphClient;
//# sourceMappingURL=ws-graph-client.js.map