import { LessonId, CourseId, TelemetryEvent } from '@lessonkit/core'; type XAPIVerbIri = "http://adlnet.gov/expapi/verbs/initialized" | "http://adlnet.gov/expapi/verbs/completed" | "http://adlnet.gov/expapi/verbs/answered" | "http://adlnet.gov/expapi/verbs/experienced"; type XAPIScore = { raw?: number; max?: number; min?: number; scaled?: number; }; type XAPIResult = { duration?: string; success?: boolean; score?: XAPIScore; completion?: boolean; }; type XAPIObjectDefinition = { name?: Record; description?: Record; type?: string; }; type XAPIStatement = { id: string; timestamp: string; verb: XAPIVerbIri; object: { id: string; definition?: XAPIObjectDefinition; }; result?: XAPIResult; context?: Record; }; type XAPITransport = (statement: XAPIStatement) => void | Promise; type XAPIQueue = { enqueue: (statement: XAPIStatement) => void; /** Remove a queued statement by id (e.g. after successful direct transport). */ removeById: (id: string) => void; flush: (transport: XAPITransport) => Promise; flushOnExit: (exitTransport: XAPIExitTransport) => void; size: () => number; /** Statement id currently being delivered via flush, if any. */ getHeadInFlightId?: () => string | undefined; /** Remove and return all queued statements (does not affect in-flight direct transport). */ drainAll: () => XAPIStatement[]; }; type XAPIExitTransport = (statement: XAPIStatement) => void | Promise; type XAPIClient = { send: (statement: XAPIStatement) => void; flush: () => Promise; /** Best-effort synchronous flush for pagehide using keepalive transport when configured. */ flushOnExit?: () => void; /** * Persist any queued (undelivered) statements to sessionStorage dead-letter storage. * Used when a client is discarded after a failed final flush (e.g. course switch). */ abandonUndelivered?: () => void; queueSize: () => number; startedLesson: (opts: { lessonId: LessonId; }) => void; completeLesson: (opts: { lessonId: LessonId; durationMs?: number; success?: boolean; score?: number; maxScore?: number; }) => void; completeCourse: () => void; }; type InMemoryXAPIQueueOptions = { /** Maximum queued statements (default 1000). Oldest entries are dropped when full. */ maxSize?: number; /** Called after enqueue with the current queue size. */ onDepth?: (size: number) => void; /** Called when an oldest statement is dropped because the queue is at maxSize. */ onCap?: () => void; /** Called when a statement cannot be enqueued because the queue is full and the head is in-flight. */ onOverflow?: (statement: XAPIStatement) => void; /** Failures at queue head before skipping (default 10). */ maxHeadFailures?: number; /** Called when the queue head is skipped after repeated transport failures. */ onHeadSkipped?: (statement: XAPIStatement, err: unknown) => void; }; declare function createInMemoryXAPIQueue(opts?: InMemoryXAPIQueueOptions): XAPIQueue; /** * Imperative xAPI client with in-memory queue, retry flush, and optional pagehide delivery. * Prefer wiring transport via `LessonkitProvider` config from `@lessonkit/react` in React apps. * * @example * ```ts * import { createXAPIClient, createFetchTransport } from "@lessonkit/xapi"; * * const client = createXAPIClient({ * courseId: "my-course", * transport: createFetchTransport({ url: "/api/xapi/statements" }), * onTransportError: (err) => console.error("LRS delivery failed", err), * }); * * await client.trackTelemetryEvent({ * name: "quiz_answered", * courseId: "my-course", * lessonId: "lesson-1", * checkId: "q1", * }); * ``` */ declare function createXAPIClient(opts?: { transport?: XAPITransport; /** Keepalive transport for pagehide flush (e.g. from createFetchTransport). */ exitTransport?: XAPIExitTransport; /** Abort in-flight transport by statement id (e.g. from createFetchTransport). */ abortInFlight?: (statementId: string) => void; courseId?: CourseId; queue?: XAPIQueue; /** When creating the default in-memory queue (max size 1000 unless overridden). */ maxQueueSize?: number; /** Consecutive head failures before skip (default queue only). */ maxHeadFailures?: number; onQueueDepth?: (size: number) => void; onQueueCap?: () => void; /** Called when dead-letter storage drops older entries beyond the cap (200). */ onDeadLetterTruncated?: (droppedCount: number) => void; /** Called when a statement cannot be persisted to sessionStorage dead-letter storage. */ onDeadLetterPersistError?: (err: unknown, ctx: { statement: XAPIStatement; }) => void; onHeadSkipped?: (statement: XAPIStatement, err: unknown) => void; /** Called when transport fails after retries (statement is re-queued). */ onTransportError?: (err: unknown) => void; /** Called when telemetry → xAPI mapping fails. */ onMappingError?: (err: unknown) => void; }): XAPIClient; /** @internal Reset dead-letter storage between tests. */ declare function resetXAPIDeadLetterForTests(): void; type AssertSafeLrsUrlOptions = { /** Allow loopback, RFC1918, link-local, and metadata IPs (default false). */ allowPrivateHosts?: boolean; }; /** Validate an LRS or analytics proxy URL before browser fetch transport use. */ declare function assertSafeLrsUrl(url: string, opts?: AssertSafeLrsUrlOptions): void; type CreateFetchTransportOptions = { /** LRS or proxy endpoint (POST). */ url: string; /** Allow loopback and private-network hosts (default false). */ allowPrivateHosts?: boolean; /** Per-request timeout (default 30_000 ms). Uses AbortSignal.timeout when available. */ timeoutMs?: number; /** Static headers merged into each request (e.g. Authorization from a short-lived token). */ headers?: Record | (() => Record); /** Retries after transport failure (default 2). */ retries?: number; /** Initial backoff in ms (default 250). Doubles each retry up to maxBackoffMs. */ backoffMs?: number; /** Maximum backoff in ms (default 5_000). */ maxBackoffMs?: number; /** Extra fetch init merged into each request. */ init?: Omit; }; type FetchTransportBundle = { transport: XAPITransport; /** Best-effort synchronous delivery for pagehide (keepalive fetch). */ exitTransport: (statement: XAPIStatement) => void; /** Abort an in-flight transport request by statement id (used on pagehide). */ abortInFlight: (statementId: string) => void; }; /** HTTP error from fetch transport with status for retry policy. */ declare class FetchHttpError extends Error { readonly status: number; constructor(status: number, statusText: string, kind?: "xapi" | "batch"); } /** Retry 429 and 5xx; do not retry other 4xx (auth/config errors). */ declare function isRetryableFetchHttpStatus(status: number): boolean; declare function isRetryableFetchError(err: unknown): boolean; /** * Creates an xAPI transport backed by fetch with timeout, retry backoff, and a * keepalive exit transport for pagehide delivery. * * @example * ```ts * import { createFetchTransport } from "@lessonkit/xapi"; * * const { transport, exitTransport, abortInFlight } = createFetchTransport({ * url: import.meta.env.VITE_XAPI_PROXY_URL, * headers: () => ({ Authorization: "Bearer …" }), * }); * ``` * * @throws When `url` points at a private/loopback host without `allowPrivateHosts: true`. */ declare function createFetchTransport(opts: CreateFetchTransportOptions): FetchTransportBundle; type CreateFetchBatchSinkOptions = CreateFetchTransportOptions; type FetchBatchSinkBundle = { batchSink: (events: unknown[]) => Promise; /** Best-effort keepalive POST for pagehide (JSON array body). */ exitBatchSink: (events: unknown[]) => void; }; /** * Batch analytics sink with timeout, retry backoff, and keepalive exit delivery. * Wire as `config.tracking.batchSink` and `config.tracking.exitBatchSink` in production. * * @example * ```ts * import { createFetchBatchSink } from "@lessonkit/xapi"; * * const { batchSink, exitBatchSink } = createFetchBatchSink({ * url: import.meta.env.VITE_ANALYTICS_URL, * }); * ``` * * @throws When `url` points at a private/loopback host without `allowPrivateHosts: true`. */ declare function createFetchBatchSink(opts: CreateFetchBatchSinkOptions): FetchBatchSinkBundle; type PersistDeadLetterOptions = { onTruncated?: (droppedCount: number) => void; onPersistError?: (err: unknown, ctx: { statement: XAPIStatement; }) => void; }; declare function loadDeadLetterStatements(): XAPIStatement[]; declare function persistDeadLetterStatement(statement: XAPIStatement, opts?: PersistDeadLetterOptions): void; /** * Map a LessonKit telemetry event to an xAPI statement, or null if the event should not emit xAPI. * `lesson_time_on_task` returns null (companion metric; lesson_completed carries duration). */ declare function telemetryEventToXAPIStatement(event: TelemetryEvent): XAPIStatement | null; export { type AssertSafeLrsUrlOptions, type CreateFetchBatchSinkOptions, type CreateFetchTransportOptions, type FetchBatchSinkBundle, FetchHttpError, type FetchTransportBundle, type InMemoryXAPIQueueOptions, type XAPIClient, type XAPIExitTransport, type XAPIObjectDefinition, type XAPIQueue, type XAPIResult, type XAPIScore, type XAPIStatement, type XAPITransport, type XAPIVerbIri, assertSafeLrsUrl, createFetchBatchSink, createFetchTransport, createInMemoryXAPIQueue, createXAPIClient, isRetryableFetchError, isRetryableFetchHttpStatus, loadDeadLetterStatements, persistDeadLetterStatement, resetXAPIDeadLetterForTests, telemetryEventToXAPIStatement };