jinaga
Version:
Data management for web and mobile applications.
123 lines (112 loc) • 4.09 kB
text/typescript
import { Network } from "../managers/NetworkManager";
import { Specification } from "../specification/specification";
import { FactEnvelope, FactReference, Storage } from "../storage";
import { FeedResponse } from "../http/messages";
import { HttpNetwork } from "../http/httpNetwork";
import { WsGraphClient } from "./ws-graph-client";
import { UserIdentity } from "../user-identity";
export class WsGraphNetwork implements Network {
private readonly wsClient: WsGraphClient;
private factsAddedListener?: (envelopes: FactEnvelope[]) => Promise<void>;
constructor(
private readonly httpNetwork: HttpNetwork,
store: Storage,
wsEndpoint: string,
getUserIdentity?: () => Promise<UserIdentity | null>,
getAuthorizationHeader?: () => Promise<string | null>
) {
const getWsUrl = async () => {
try {
const url = new URL(wsEndpoint);
// Append Authorization token if provided (browsers cannot set custom WS headers)
if (getAuthorizationHeader) {
try {
const auth = await getAuthorizationHeader();
if (auth) {
url.searchParams.set("authorization", auth);
}
}
catch {
// ignore auth retrieval failures
}
}
// Optionally append user identity
if (getUserIdentity) {
try {
const id = await getUserIdentity();
if (id) {
url.searchParams.set("uid", `${encodeURIComponent(id.provider)}:${encodeURIComponent(id.id)}`);
}
}
catch {
// ignore identity retrieval failures
}
}
return url.toString();
}
catch {
return wsEndpoint;
}
};
this.wsClient = new WsGraphClient(
getWsUrl,
store,
(feed, bookmark) => this.onBookmarkAdvance(feed, bookmark),
(err) => this.onGlobalError(err),
getUserIdentity,
(envelopes) => this.onFactsAdded(envelopes)
);
}
feeds(start: FactReference[], specification: Specification): Promise<string[]> {
return this.httpNetwork.feeds(start, specification);
}
fetchFeed(feed: string, bookmark: string): Promise<FeedResponse> {
return this.httpNetwork.fetchFeed(feed, bookmark);
}
streamFeed(
feed: string,
bookmark: string,
onResponse: (factReferences: FactReference[], nextBookmark: string) => Promise<void>,
onError: (err: Error) => void,
feedRefreshIntervalSeconds: number
): () => void {
// Register a temporary handler for BOOK events for this feed
this.onResponseHandlers.set(feed, onResponse);
this.onErrorHandlers.set(feed, onError);
const unsubscribe = this.wsClient.subscribe(feed, bookmark, feedRefreshIntervalSeconds);
return () => {
this.onResponseHandlers.delete(feed);
this.onErrorHandlers.delete(feed);
unsubscribe();
};
}
load(factReferences: FactReference[]): Promise<FactEnvelope[]> {
return this.httpNetwork.load(factReferences);
}
// Internal per-feed event maps
private readonly onResponseHandlers = new Map<string, (factReferences: FactReference[], nextBookmark: string) => Promise<void>>();
private readonly onErrorHandlers = new Map<string, (err: Error) => void>();
private async onBookmarkAdvance(feed: string, bookmark: string) {
const handler = this.onResponseHandlers.get(feed);
if (handler) {
// Facts already persisted via graph stream, notify empty refs with updated bookmark
await handler([], bookmark);
}
}
private onGlobalError(err: Error) {
// Broadcast error to all active feeds
for (const h of this.onErrorHandlers.values()) {
h(err);
}
}
// Phase 3.4: Observer-notification bridge
setFactsAddedListener(listener: (envelopes: FactEnvelope[]) => Promise<void>) {
this.factsAddedListener = listener;
}
// Called by WsGraphClient when facts are added via WS
async onFactsAdded(envelopes: FactEnvelope[]) {
if (this.factsAddedListener) {
await this.factsAddedListener(envelopes);
}
}
}