jinaga
Version:
Data management for web and mobile applications.
193 lines • 10.3 kB
JavaScript
;
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