UNPKG

jinaga

Version:

Data management for web and mobile applications.

243 lines 10.4 kB
"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