/** * Event-driven sync wiring — Phase 3 of event-driven-sync-menubar * (US-017 publish / US-018 receive / US-019 rollout gate). * * Phases 1–2 built every piece but left them unconnected: the watcher runs * targeted *push passes* (S3 upload) but never publishes a PushEvent, and the * runner's receiver seam defaults to {@link NoopPushReceiver}. This module is * the connective tissue that turns both on after a server grant: * * - {@link resolveEventSync} — a negative-only local kill switch. A server * grant remains the only positive authority for publish and receive. * - {@link subscribeSyncReceive} — `POST /v1/sync/subscribe` (US-015/US-016): * mints the per-device queue and returns `{queueUrl, region, credentials}`, * where `credentials` are short-lived STS creds scoped to receive/delete on * exactly that queue. * - {@link sqsClientFromAwsSdk} — the doc-promised thin adapter from the AWS * SDK `SQSClient` to the receiver's narrow {@link SqsClientLike} seam. * - {@link createRefreshingSqsClient} — wraps the adapter with credential * lifecycle: proactive re-vend before expiry (skew window) + one reactive * retry on an expiry-class error. The queue URL is stable across re-vends * (same device → same queue, idempotent endpoint); only creds rotate. * - {@link startEventSync} — the wiring entry the runner calls from the * `--event-push` watch block unless locally disabled. Resolves tenant + device * identity, builds the {@link HttpPushTransport} + {@link PushEventEmitter} * (publish leg) and the {@link SqsPushReceiver} (receive leg, self-echo * filtered), and returns handles. Any startup failure degrades to * poll-only — it NEVER takes the daemon down. * * The 10-minute `--poll-remote-ms` pass remains the correctness backstop for * every path here; event delivery is best-effort by design. */ import { SQSClient } from "@aws-sdk/client-sqs"; import { HttpPushTransport, RealtimeUnavailableError, type AuthTokenSource } from "./push-transport.js"; import { type PushReceiver, type RealtimeWakeSource, type SqsClientLike, type SyncBatchEngineFn, type SyncEngineFn } from "./push-receiver.js"; import { type Clock, type TreeChangeBatch } from "../watcher.js"; import { type CloudTelemetryClient, type TelemetryClaims } from "../telemetry-events.js"; import { type SyncMetricCredentials } from "./metrics.js"; /** * Decide whether the client may request event-sync authority from the server. * * This is negative-only. An unset value or a former force-on value merely * permits the authenticated server request; it cannot authorize event sync. */ export declare function resolveEventSync(override: string | undefined): boolean; export interface SubscribeSyncCredentials { accessKeyId: string; secretAccessKey: string; sessionToken: string; /** ISO8601 expiry of the vended STS credentials. */ expiration: string; } export interface SubscribeSyncResponse { /** The caller's own per-device queue URL (stable across calls). */ queueUrl: string; /** Region the queue lives in. */ region: string; /** Short-lived creds scoped to receive/delete on exactly this queue. */ credentials: SubscribeSyncCredentials; /** Optional short-lived creds scoped to publish sync latency CloudWatch metrics. */ cloudWatch?: { region: string; credentials: SyncMetricCredentials; }; } /** Minimal fetch seam (matches push-transport.ts's FetchLike posture). */ type FetchLike = (url: string, init: { method: string; headers: Record; body: string; signal?: AbortSignal; }) => Promise<{ ok: boolean; status: number; text(): Promise; }>; /** * `POST /v1/sync/subscribe` — provision (idempotently) this device's queue * and vend fresh receive credentials. Auth mirrors HttpPushTransport: Bearer * token resolved per-call via the supplied source. */ export declare function subscribeSyncReceive(opts: { apiUrl: string; authToken: AuthTokenSource; deviceId: string; timeoutMs?: number; fetchImpl?: FetchLike; }): Promise; /** * Adapt the AWS SDK `SQSClient` to the receiver's narrow {@link SqsClientLike} * seam (the doc-promised `sqsClientFromAwsSdk` from push-receiver.ts). The * abort signal is forwarded so `dispose()` can cut a 20s long-poll short. */ export declare function sqsClientFromAwsSdk(client: Pick): SqsClientLike; export interface RefreshingSqsClientOptions { /** The initial subscribe response (creds + region + queue URL). */ initial: SubscribeSyncResponse; /** Re-vend: called when creds are near/past expiry. Idempotent server-side. */ subscribe: () => Promise; /** * Build the underlying narrow client from a subscribe response. Default: * AWS SDK `SQSClient` via {@link sqsClientFromAwsSdk}. Tests inject a fake. */ buildSqs?: (resp: SubscribeSyncResponse) => SqsClientLike; /** Clock seam (tests). Default `Date.now`. */ now?: () => number; /** Receives a deliberate server denial before it reaches the receiver loop. */ onRealtimeUnavailable?: (error: RealtimeUnavailableError) => void; } /** * An {@link SqsClientLike} that owns the vended-credential lifecycle: * * - PROACTIVE: before each call, if the recorded expiry is within the skew * window, re-subscribe (re-vend) and rebuild the inner client first. * - REACTIVE: if a call still fails with an expiry-class error (clock skew, * revocation), re-vend once and retry the call once. Anything else — or a * second failure — propagates to the receiver's own backoff/reconnect * loop, whose retention-backed redelivery makes the miss recoverable. * * Concurrent refreshes collapse onto one in-flight subscribe promise. */ export declare function createRefreshingSqsClient(opts: RefreshingSqsClientOptions): SqsClientLike; /** Structured line logger seam — the runner passes its stderr logger. */ export type EventSyncLog = (message: string) => void; export interface StartEventSyncOptions { hqRoot: string; /** Vault API base URL (the runner's DEFAULT_VAULT_API_URL). */ apiUrl: string; /** Cognito access-token source (getter for long-running daemons). */ authToken: AuthTokenSource; /** This device's stable id (getOrCreateMachineId). */ deviceId: string; /** * Resolve the caller's tenant id (canonical person `prs_*` uid). The server * rejects publishes whose `originTenantId` mismatches the JWT principal, so * this MUST be the same identity the JWT resolves to. */ resolveTenantId: () => Promise; /** * The already-routed targeted-pull bridge (the runner's receiverSyncBatchFn, * funneled through its runGuarded mutex). */ syncBatchFn?: SyncBatchEngineFn; /** @deprecated Use `syncBatchFn` so one receive batch becomes one pull pass. */ syncFn?: SyncEngineFn; /** Diagnostic logger (one line per lifecycle event). Default: console.error. */ log?: EventSyncLog; subscribe?: (deviceId: string) => Promise; buildSqs?: (resp: SubscribeSyncResponse) => SqsClientLike; transport?: HttpPushTransport; telemetryClient?: CloudTelemetryClient | null; telemetryClaims?: TelemetryClaims | null; now?: () => number; /** Clock seam for deferred publish cleanup tests. */ clock?: Clock; /** Test seam for event-sync startup retry scheduling. */ retryInitialMs?: number; /** Test seam for the capped event-sync startup retry delay. */ retryMaxMs?: number; /** Test seam for full-jitter event-sync startup retries. */ retryRandom?: () => number; /** Test seam for the poll-only realtime-eligibility recheck interval. */ realtimeUnavailableRecheckMs?: number; /** Override the per-path trailing-publish interval. */ publishMinIntervalMs?: number; /** Existing runner state directory for restart-durable publish suppression. */ publishedContentHashStateDir?: string; /** Legacy eager baseline used only to seed a missing durable store. */ initialPublishedContentHashes?: Readonly>; /** Lazy authoritative lookup used by the runner to avoid retaining all rows. */ publishedContentHashLookup?: (relativePath: string) => string | undefined; /** Receiver activity feeds the watch loop's poll-cadence controller. */ onReceiveActivity?: (hasMessages: boolean) => void; } export interface EventSyncHandles { /** * Publish PushEvents for a settled change batch. Called by the runner * AFTER the targeted push pass succeeds — an event must never announce * bytes that are not in S3 yet. Fire-and-forget: failures are logged by * the emitter's onError and the cadence poll covers the miss. */ publishBatch: (batch: TreeChangeBatch) => void; /** * Mark a remote upsert applied by the pull leg so the local watcher does not * immediately echo those exact bytes back through the push transport. * Optional for compatibility with injected legacy test handles. */ markContentPublished?: (relativePath: string, contentHash: string) => void; /** * Live entry counts of the emitter's long-lived per-path maps, for the runner * heap census. Fixed identifier keys, numeric values only. Optional for * compatibility with injected legacy test handles. */ censusSizes?: () => Record; /** Subscribe to the point at which this handle has a live emitter. */ onLive?: (listener: () => void) => () => void; /** The live receiver (already started). */ receiver: PushReceiver; /** This device's id — the runner uses it nowhere else, exposed for logs. */ ownDeviceId: string; /** Tear down transport + receiver (runner shutdown path). */ dispose: () => Promise; } /** Startup retry defaults: full jitter over a 30s → 10min exponential cap. */ export declare const DEFAULT_EVENT_SYNC_RETRY_INITIAL_MS = 30000; export declare const DEFAULT_EVENT_SYNC_RETRY_MAX_MS: number; /** Poll-only accounts recheck server realtime eligibility at this interval. */ export declare const DEFAULT_EVENT_SYNC_REALTIME_UNAVAILABLE_RECHECK_MS: number; /** Every v2 signal has identical correctness semantics: schedule one drain. */ export type RealtimeDrainSignal = "watcher" | "wake" | "high-water" | "reconnect" | "retry"; export interface UnifiedRealtimeSchedulerOptions { /** * The runner supplies its existing U07 guarded queue here. It is the only * place a V2 drain may enter, so no signal source can create a parallel pass. */ runGuarded: (drain: () => Promise) => Promise; /** One bounded fixed-H drain. It must authorize again before I/O. */ drain: () => Promise; /** High-water is a hint check; `true` means set the same drain bit. */ checkHighWater?: () => Promise; /** Bounded retry delay after a failed drain. Defaults to one second. */ retryDelayMs?: number; /** Named outcome sink. It must not receive wake envelope values. */ onError?: (signal: RealtimeDrainSignal, error: unknown) => void; } export interface UnifiedRealtimeScheduler { signal(signal: RealtimeDrainSignal): void; /** Called by the runner's five-minute cadence, never by a separate loop. */ checkHighWater(): Promise; /** Bind a receiver to the exact active identity; a changed identity disposes it. */ replaceIdentity(identityFingerprint: string, source: RealtimeWakeSource): Promise; dispose(): Promise; } /** * Coalesces all v2 notifications into a single drain bit. The cursor carried * by a wake is intentionally ignored: duplicate, stale, and reordered wakes * all request the same fixed-H drain. Failures stay in V2's retry path; this * class has no V1 runner or legacy callback by construction. */ export declare function createUnifiedRealtimeScheduler(options: UnifiedRealtimeSchedulerOptions): UnifiedRealtimeScheduler; /** * Bring up the publish + receive legs. A failed initial subscribe deliberately * keeps the watch runner poll-only, but returns an offline handle that retries * the complete startup sequence. A coded realtime-unavailable denial instead * stays poll-only and only rechecks eligibility on a long cadence. Once a * retry succeeds, the same handle forwards publishes and disposes the live * legs just like an immediate start. */ export declare function startEventSync(opts: StartEventSyncOptions): Promise; export {}; //# sourceMappingURL=event-sync.d.ts.map