import { KyInstance } from 'ky'; import { Observable, BehaviorSubject } from 'rxjs'; import { StateUnit, ConnectionState, SemiontError, TransportErrorCode, ITransport, IBackendOperations, BaseUrl, AccessToken, Logger, EventMap, ResourceId, EventBus, Email, components, GoogleCredential, RefreshToken, UserResponse, ListUsersResponse, UserDID, UpdateUserRequest, UpdateUserResponse, BackendDownload, ProgressEvent, HealthCheckResponse, StatusResponse, IContentTransport, PutBinaryRequest, PutBinaryOptions, ExtractionOutcome } from '@semiont/core'; interface BusEvent { channel: string; payload: Record; scope?: string; } interface ActorStateUnitOptions { baseUrl: string; token: string | (() => string); channels: string[]; reconnectMs?: number; /** * Remove-side reconnect hysteresis (MULTI-RESOURCE-SCOPE). Scope * additions need liveness quickly (100 ms debounce), but a removal only * narrows delivery — extra events for a just-released scope are * idempotent locally — so remove-only changes wait this long before * reconnecting. Keeps hover-churn (transient per-citation previews) * from turning every mouse pass into a reconnect storm; any addition * flushes pending removals with it on the fast path. */ lazyRemoveMs?: number; /** * B17 (LOCAL-STORAGE) — IO-abstracted persistence of the last seen * PERSISTED event id PER SCOPE, so a reloaded client resumes each * scope's replay instead of gapping. `load` runs once at construction; * `save` fires per persisted (`p-*`) id with that frame's scope — * ephemeral (`e-*`) ids are never saved: they carry no replay meaning, * and letting them displace a scope's watermark was exactly the silent * replay-loss hole the single-id design had. The transport stays * storage-free; callers wrap their own adapter in these thunks. */ loadLastEventIds?: () => Record | null; saveLastEventId?: (scope: string, id: string) => void; } /** Time in the `reconnecting` state before transitioning to `degraded`. */ declare const DEGRADED_THRESHOLD_MS = 3000; interface ActorStateUnit extends StateUnit { on$>(channel: string): Observable; emit(channel: string, payload: Record, emitScope?: string): Promise; state$: Observable; /** With `scope`: upsert channels into that scope's matrix entry. Without: global channels. */ addChannels(channels: string[], scope?: string): void; /** With `scope`: remove channels from that scope's entry (empty entry drops the scope). Without: global channels. */ removeChannels(channels: string[], scope?: string): void; /** * Correlated-reply retention, client side (BUS-RESUMPTION Phase 2 / * SDK-DEBT S1): register a busRequest correlationId as awaiting its * reply. Every connect body includes the currently-tracked set as * `pendingReplies`, so a reply published while the connection was down * is replayed from the server's retention buffer. The returned disposer * (idempotent) removes the id on settle. */ trackReply(correlationId: string): () => void; start(): void; stop(): void; } declare function createActorStateUnit(options: ActorStateUnitOptions): ActorStateUnit; /** * HttpTransport — the HTTP/SSE implementation of ITransport. * * Phase 1 of TRANSPORT-ABSTRACTION. Owns everything that crosses the wire * in remote mode: the bus actor (SSE + POST /bus/emit), auth/admin/exchange/ * system HTTP endpoints, and connection-state plumbing. * * Does NOT own the local coordination bus — that lives on `SemiontClient`. * `bridgeInto(bus)` wires SSE-received events into the caller-supplied bus * once at construction. */ type AuthResponse = components['schemas']['AuthResponse']; type TokenRefreshResponse = components['schemas']['TokenRefreshResponse']; type AdminUserStatsResponse = components['schemas']['AdminUserStatsResponse']; type OAuthConfigResponse = components['schemas']['OAuthConfigResponse']; declare class APIError extends SemiontError { code: TransportErrorCode; readonly status: number; readonly statusText: string; constructor(message: string, status: number, statusText: string, body?: unknown); } type TokenRefresher = () => Promise; interface HttpTransportConfig { baseUrl: BaseUrl; /** Observable token source; headers read the current value. */ token$?: BehaviorSubject; timeout?: number; retry?: number; logger?: Logger; /** Optional 401-recovery hook. See {@link TokenRefresher}. */ tokenRefresher?: TokenRefresher; /** * B17 — persistence thunks for the last seen persisted SSE id PER * SCOPE, passed through to the actor state unit. See * {@link ActorStateUnitOptions}. */ loadLastEventIds?: () => Record | null; saveLastEventId?: (scope: string, id: string) => void; } declare class HttpTransport implements ITransport, IBackendOperations { readonly baseUrl: BaseUrl; private readonly http; private readonly token$; private readonly logger?; private readonly errorsSubject; /** * Stream of `APIError` instances surfaced from any HTTP request just * before the transport throws to the caller. Satisfies the `ITransport` * `errors$` contract — see `@semiont/core/transport.ts`. */ readonly errors$: Observable; private _actor; private _actorStarted; private disposed; /** * Per-resource subscription ref-counts (MULTI-RESOURCE-SCOPE). Distinct * resources COMPOSE — each key's first subscribe adds its scoped channels * to the actor's matrix, its last release removes them; keys are fully * independent. Local fan-out for scoped channels is a SINGLETON wired in * the actor getter (one delivery per event regardless of how many scopes * are held), so entries here are counts only. */ private readonly scopeRefCounts; /** Buses we've been asked to bridge wire events into. */ private readonly bridges; private readonly config; constructor(config: HttpTransportConfig); get actor(): ActorStateUnit; emit(channel: K, payload: EventMap[K], resourceScope?: ResourceId): Promise; on(channel: K, handler: (payload: EventMap[K]) => void): () => void; stream(channel: K): Observable; /** * Wire this transport's SSE fan-in into the given bus. Every channel * in `BRIDGED_CHANNELS` (and subsequently per-resource scoped channels * opened by `subscribeToResource`) is published on the bus. Safe to * call multiple times — each bus is added to the fan-out list. */ bridgeInto(bus: EventBus): void; subscribeToResource(resourceId: ResourceId): () => void; get state$(): Observable; /** * Correlated-reply retention, client side (BUS-RESUMPTION Phase 2 / * SDK-DEBT S1): `busRequest` registers its cid here before emitting; * the actor carries the tracked set as `pendingReplies` on every * subscribe body, so a reply published while the connection was down * replays from the server's retention buffer on reconnect. */ trackReply(correlationId: string): () => void; dispose(): void; /** * Route a transport-level error onto `errors$`. Used by sibling adapters * (e.g. `HttpContentTransport`'s XHR upload path) that don't go through * the `ky` `beforeError` hook and need to surface failures on the same * stream the rest of the transport publishes to. */ pushError(error: SemiontError): void; private authHeaders; authenticatePassword(email: Email, password: string): Promise; authenticateGoogle(credential: GoogleCredential): Promise; refreshAccessToken(token: RefreshToken): Promise; logout(): Promise; acceptTerms(): Promise; getCurrentUser(): Promise; getMediaToken(resourceId: ResourceId): Promise<{ token: string; }>; listUsers(): Promise; getUserStats(): Promise; updateUser(id: UserDID, data: UpdateUserRequest): Promise; getOAuthConfig(): Promise; backupKnowledgeBase(): Promise; restoreKnowledgeBase(file: File): Observable; exportKnowledgeBase(params?: { includeArchived?: boolean; }): Promise; importKnowledgeBase(file: File): Observable; /** * POST a file to a server-sent-events endpoint and surface each `data:` * frame as an Observable emission. Completes when the stream closes; * errors if the request itself fails or the SSE stream is aborted. * The returned Observable is cold — the POST happens on subscribe and * is aborted via `AbortController` on unsubscribe. */ private sseProgressStream; healthCheck(): Promise; getStatus(): Promise; /** * Temporary escape hatch for the ongoing transport migration: namespaces * that still need to issue ad-hoc HTTP calls (e.g. legacy browse/mark * HTTP fallbacks) can borrow the configured `ky` instance here. Will be * deleted once all namespaces route through bus channels or through * typed methods on this transport. */ get rawHttp(): KyInstance; /** * Current access token (synchronously read from the BehaviorSubject). * Used by content-transport and legacy namespace HTTP fallbacks that * need to pass `auth: token` through some code paths. */ getToken(): AccessToken | undefined; } /** * HttpContentTransport — binary I/O over HTTP. * * Phase 1 of TRANSPORT-ABSTRACTION. Narrow by design because binary has * different backpressure and streaming characteristics than typed command * payloads. Uses the HttpTransport's underlying ky instance + token, so * retries, logging, and auth behave identically to the rest of the wire. * * Two `putBinary` paths live side by side, selected by runtime * environment + caller intent: * - **ky path (default + Node)** — the original `ky.post(...)` path. * Keeps retry-with-refresh, beforeError → APIError, observability * spans intact. Hits when no `onProgress`/`signal` is passed, OR * when `XMLHttpRequest` isn't available in the runtime (Node * workers, the CLI). On Node-side `signal`-aborts: the in-flight * `fetch` continues in the background and the `cancelled` flag in * `yield.resource` suppresses the resolve/reject callbacks. * - **XHR path (browsers with `onProgress` or `signal`)** — hand-rolled * because `ky` wraps `fetch` which can't observe upload byte- * progress today (`Request({ duplex: 'half' })` is the long-term * direction; not yet widely available across the webviews this * codepath needs to run in). Threads auth + traceparent headers, * emits `onProgress` from `xhr.upload.onprogress`, supports * cancellation via the `signal` option (calling `xhr.abort()`), * and routes failures onto the same `transport.errors$` stream * the ky path uses. * * The runtime check on `XMLHttpRequest` is the load-bearing seam: a * Node worker calling `client.yield.resource(...)` (which always passes * a `signal` for unsubscribe-aborts) must NOT take the XHR path — * `XMLHttpRequest` is undefined and the upload throws synchronously. * Browsers always have it; Node does not. * * v1 limitation: the XHR path does NOT auto-refresh on 401. Mitigation: * the session's proactive refresh fires before token expiry, so an * upload that *starts* with a fresh token usually completes. An upload * spanning the narrow window between expiry and proactive-refresh would * fail; the existing `errors$` → modal path surfaces it as session- * expired. If retry-with-refresh on the upload path becomes a real * complaint, wire a manual retry loop here that reads `token$` afresh. */ type GetResourceResponse = components['schemas']['GetResourceResponse']; declare class HttpContentTransport implements IContentTransport { private readonly transport; constructor(transport: HttpTransport); putBinary(request: PutBinaryRequest, options?: PutBinaryOptions): Promise<{ resourceId: ResourceId; }>; getBinary(resourceId: ResourceId, options?: { auth?: AccessToken; }): Promise<{ data: ArrayBuffer; contentType: string; }>; getBinaryStream(resourceId: ResourceId, options?: { auth?: AccessToken; }): Promise<{ stream: ReadableStream; contentType: string; }>; /** * Dereference the resource's JSON-LD graph over HTTP — the LD face an * external linked-data client sees. Deliberately HTTP, not the bus * (SIMPLER-JSON-LD.md §5). */ getResourceGraph(resourceId: ResourceId, options?: { auth?: AccessToken; }): Promise; /** * Store a resource's derived coordinate map (ANCHORED-TEXT-CACHE Lane 5). * * The Smelter is the producer and runs as its own process, which is why this * crosses the wire at all: the map goes to the one store the KnowledgeSystem * owns, rather than to a volume shared between service images. */ putAnchoredText(checksum: string, outcome: ExtractionOutcome, options?: { auth?: AccessToken; }): Promise; /** * The resource's coordinate map, or `null` when none has been derived — * which is the common case and not an error: callers degrade to no quoted * text. * * 204 is that answer, and the body is empty, so it must be taken before * `.json()` is reached — parsing an empty body throws, which would turn the * ordinary case into a failure. A 404 degrades the same way, though it is a * different fact: the resource itself is absent, and a resource that does * not exist has no map either. */ getAnchoredText(resourceId: ResourceId, options?: { auth?: AccessToken; }): Promise; /** * The cache-consult read (PERSIST-ANCHORS P2c) — checksum-addressed and * barrier-free; 204 is the ordinary miss. This is how an out-of-process * extraction seam hits the cache at all. */ getAnchoredTextByChecksum(checksum: string, options?: { auth?: AccessToken; }): Promise; /** * The store's would-hit keys — the reconcile planner's bulk existence read * (PERSIST-ANCHORS P0). One request per reconcile; keys only, never the * maps themselves, which is the point of the dedicated route. */ listAnchoredTextKeys(options?: { auth?: AccessToken; }): Promise; dispose(): void; /** Auth header + W3C trace propagation for the active span. */ private requestHeaders; } export { APIError, DEGRADED_THRESHOLD_MS, HttpContentTransport, HttpTransport, createActorStateUnit }; export type { ActorStateUnit, ActorStateUnitOptions, BusEvent, HttpTransportConfig, TokenRefresher };