UNPKG

jinaga

Version:

Data management for web and mobile applications.

193 lines 10.3 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.AuthorizationWebSocketHandler = void 0; const inverse_1 = require("../specification/inverse"); const serializer_1 = require("../http/serializer"); class AuthorizationWebSocketHandler { constructor(authorization, resolveFeed, inverseEngine, bookmarks, distributionEngine, resolveFeedInfo) { this.authorization = authorization; this.resolveFeed = resolveFeed; this.inverseEngine = inverseEngine; this.bookmarks = bookmarks; this.distributionEngine = distributionEngine; this.resolveFeedInfo = resolveFeedInfo; this.subscriptions = new Map(); this.buffers = new WeakMap(); } handleConnection(socket, userIdentity) { this.buffers.set(socket, ""); socket.on("message", (data) => __awaiter(this, void 0, void 0, function* () { const text = typeof data === "string" ? data : String(data); yield this.pushChunk(socket, userIdentity, text); })); socket.on("close", () => { // Cleanup all listeners on disconnect for (const sub of this.subscriptions.values()) { for (const token of sub.listeners) { this.inverseEngine.removeSpecificationListener(token); } } this.subscriptions.clear(); }); } pushChunk(socket, userIdentity, chunk) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { // Append to per-socket buffer and attempt to parse complete frames const existing = (_a = this.buffers.get(socket)) !== null && _a !== void 0 ? _a : ""; let buffer = existing + chunk; const parts = buffer.split(/\r?\n/); buffer = (_b = parts.pop()) !== null && _b !== void 0 ? _b : ""; // remainder without trailing newline let i = 0; while (i < parts.length) { const line = parts[i]; if (line === "SUB" || line === "UNSUB") { const keyword = line; i++; const payload = []; while (i < parts.length) { const next = parts[i]; if (next === "") { break; } payload.push(next); i++; } // If we have a blank line terminator, ensure we have enough payload lines; otherwise treat as incomplete if (i >= parts.length || parts[i] !== "") { // No terminator present; reconstruct remainder and exit // Preserve line break so next chunk starts on a new line const remainder = [keyword, ...payload].join("\n") + "\n"; buffer = remainder + (buffer ? buffer : ""); break; } const required = keyword === "SUB" ? 2 : 1; if (payload.length < required) { // Not enough payload yet; push back without consuming terminator. // Preserve line break so the next incoming payload line does not concatenate with the keyword or prior payload. const remainder = [keyword, ...payload].join("\n") + "\n"; buffer = remainder + (buffer ? buffer : ""); break; } // Consume blank terminator i++; try { if (keyword === "SUB") { const feed = JSON.parse(payload[0] || '""'); const bookmark = JSON.parse(payload[1] || '""'); yield this.handleSub(socket, userIdentity, feed, bookmark); } else { const feed = JSON.parse(payload[0] || '""'); this.handleUnsub(feed); } } catch (_c) { // Ignore malformed frame } continue; } // Unknown line; ignore i++; } // Save updated buffer this.buffers.set(socket, buffer); }); } handleSub(socket, userIdentity, feed, bookmark) { return __awaiter(this, void 0, void 0, function* () { try { const specification = this.resolveFeed(feed); const start = []; // Optional distribution enforcement: if engine and resolver provided, validate access if (this.distributionEngine && this.resolveFeedInfo) { try { const { specification: feedSpec, namedStart } = this.resolveFeedInfo(feed); let userRef = null; if (userIdentity) { const userFact = yield this.authorization.getOrCreateUserFact(userIdentity); userRef = { type: userFact.type, hash: userFact.hash }; } const result = yield this.distributionEngine.canDistributeToAll([feedSpec], namedStart, userRef); if (result.type === "failure") { const message = `Not authorized: ${result.reason}`; socket.send(`ERR\n${JSON.stringify(feed)}\n${JSON.stringify(message)}\n\n`); return; // Do not proceed with subscription } } catch (e) { const message = e && e.message ? e.message : String(e); socket.send(`ERR\n${JSON.stringify(feed)}\n${JSON.stringify(message)}\n\n`); return; } } // If server already has a more recent bookmark for this feed, sync it to client const serverKnown = this.bookmarks.syncBookmarkIfMismatch(feed, bookmark); if (serverKnown) { socket.send(`BOOK\n${JSON.stringify(feed)}\n${JSON.stringify(serverKnown)}\n\n`); } const factFeed = yield this.authorization.feed(userIdentity, specification, start, bookmark); if (factFeed.tuples.length > 0) { const references = factFeed.tuples.flatMap(t => t.facts); const envelopes = yield this.authorization.load(userIdentity, references); socket.send((0, serializer_1.serializeGraph)(envelopes)); } // Set initial bookmark if changed const nextBookmark = factFeed.bookmark || bookmark; if (nextBookmark && nextBookmark !== bookmark) { this.bookmarks.setBookmark(feed, nextBookmark); socket.send(`BOOK\n${JSON.stringify(feed)}\n${JSON.stringify(nextBookmark)}\n\n`); } // Register inverse specification listeners for reactive updates const inverses = (0, inverse_1.invertSpecification)(specification); const listenerTokens = []; for (const inv of inverses) { const token = this.inverseEngine.addSpecificationListener(inv.inverseSpecification, (results) => __awaiter(this, void 0, void 0, function* () { if (inv.operation === "add") { const refs = results.flatMap(r => Object.values(r.tuple)); if (refs.length > 0) { const envs = yield this.authorization.load(userIdentity, refs); socket.send((0, serializer_1.serializeGraph)(envs)); } const advanced = yield this.bookmarks.advanceBookmark(feed); socket.send(`BOOK\n${JSON.stringify(feed)}\n${JSON.stringify(advanced)}\n\n`); } else if (inv.operation === "remove") { // No facts to send; just advance bookmark to signal change const advanced = yield this.bookmarks.advanceBookmark(feed); socket.send(`BOOK\n${JSON.stringify(feed)}\n${JSON.stringify(advanced)}\n\n`); } })); listenerTokens.push(token); } this.subscriptions.set(feed, { feed, listeners: listenerTokens }); // Send ACK to confirm subscription is active socket.send(`ACK\n${JSON.stringify(feed)}\n\n`); } catch (e) { const message = e && e.message ? e.message : String(e); socket.send(`ERR\n${JSON.stringify(feed)}\n${JSON.stringify(message)}\n\n`); } }); } handleUnsub(feed) { const sub = this.subscriptions.get(feed); if (sub) { for (const token of sub.listeners) { this.inverseEngine.removeSpecificationListener(token); } this.subscriptions.delete(feed); } } } exports.AuthorizationWebSocketHandler = AuthorizationWebSocketHandler; //# sourceMappingURL=authorization-websocket-handler.js.map