@spacelr/sdk 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -1
- package/dist/index.d.mts +236 -1
- package/dist/index.d.ts +236 -1
- package/dist/index.js +247 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +239 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../libs/sdk/src/core/errors.ts","../../../../libs/sdk/src/core/http-client.ts","../../../../libs/sdk/src/core/token-storage.ts","../../../../libs/sdk/src/core/token-manager.ts","../../../../libs/sdk/src/types/common.ts","../../../../libs/sdk/src/core/pkce.ts","../../../../libs/sdk/src/core/realtime.ts","../../../../libs/sdk/src/core/cursor-storage.ts","../../../../libs/sdk/src/modules/auth.module.ts","../../../../libs/sdk/src/modules/storage.module.ts","../../../../libs/sdk/src/modules/database.module.ts","../../../../libs/sdk/src/modules/notifications.module.ts","../../../../libs/sdk/src/modules/functions.module.ts","../../../../libs/sdk/src/client.ts"],"sourcesContent":["export class SpacelrError extends Error {\n readonly code: string;\n readonly statusCode?: number;\n readonly details?: Record<string, unknown>;\n\n constructor(\n message: string,\n code: string,\n statusCode?: number,\n details?: Record<string, unknown>\n ) {\n super(message);\n this.name = 'SpacelrError';\n this.code = code;\n this.statusCode = statusCode;\n this.details = details;\n }\n}\n\nexport class SpacelrAuthError extends SpacelrError {\n constructor(\n message: string,\n statusCode = 401,\n details?: Record<string, unknown>\n ) {\n super(message, 'AUTH_ERROR', statusCode, details);\n this.name = 'SpacelrAuthError';\n }\n}\n\nexport class SpacelrNetworkError extends SpacelrError {\n constructor(message: string, details?: Record<string, unknown>) {\n super(message, 'NETWORK_ERROR', undefined, details);\n this.name = 'SpacelrNetworkError';\n }\n}\n\nexport class SpacelrTimeoutError extends SpacelrError {\n constructor(timeoutMs: number) {\n super(\n `Request timed out after ${timeoutMs}ms`,\n 'TIMEOUT_ERROR',\n undefined,\n { timeoutMs }\n );\n this.name = 'SpacelrTimeoutError';\n }\n}\n\nexport class SpacelrTwoFactorRequiredError extends SpacelrError {\n readonly twoFactorToken: string;\n\n constructor(twoFactorToken: string, details?: Record<string, unknown>) {\n super(\n 'Two-factor authentication required',\n 'TWO_FACTOR_REQUIRED',\n 200,\n details\n );\n this.name = 'SpacelrTwoFactorRequiredError';\n this.twoFactorToken = twoFactorToken;\n }\n}\n\n/**\n * Thrown when `collection.search()` is called without the pre-filter the\n * collection requires (issue #165). `details.missingFields` lists the\n * top-level filter keys the server expected.\n *\n * Required filters protect against unindexable full-collection regex\n * scans on large collections — see the collection's `searchConfig` for\n * which fields are required.\n */\nexport class SpacelrSearchFilterRequiredError extends SpacelrError {\n readonly missingFields: string[];\n\n constructor(message: string, details?: Record<string, unknown>) {\n super(message, 'SEARCH_FILTER_REQUIRED', 400, details);\n this.name = 'SpacelrSearchFilterRequiredError';\n const fields = details?.['missingFields'];\n this.missingFields = Array.isArray(fields)\n ? fields.filter((f): f is string => typeof f === 'string')\n : [];\n }\n}\n\nexport class SpacelrEmailVerificationRequiredError extends SpacelrError {\n readonly emailSent: boolean;\n\n constructor(emailSent: boolean, details?: Record<string, unknown>) {\n super(\n emailSent\n ? 'Email verification required. A new verification email has been sent.'\n : 'Email verification required. Please check your inbox for the verification email.',\n 'EMAIL_VERIFICATION_REQUIRED',\n 200,\n details\n );\n this.name = 'SpacelrEmailVerificationRequiredError';\n this.emailSent = emailSent;\n }\n}\n","import { SpacelrClientConfig, ApiResponse } from '../types';\nimport {\n SpacelrError,\n SpacelrAuthError,\n SpacelrNetworkError,\n SpacelrTimeoutError,\n SpacelrTwoFactorRequiredError,\n SpacelrEmailVerificationRequiredError,\n SpacelrSearchFilterRequiredError,\n} from './errors';\nimport { TokenManager } from './token-manager';\n\nexport interface HttpRequestOptions {\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n path: string;\n body?: unknown;\n authenticated?: boolean;\n headers?: Record<string, string>;\n query?: Record<string, string | number | undefined>;\n /** Send cookies cross-origin (credentials: 'include'). Defaults to true for /auth/ paths. */\n withCredentials?: boolean;\n /**\n * External AbortSignal that lets callers cancel an in-flight request before\n * the configured timeout elapses (e.g. `subscribeWithSnapshot` aborting its\n * snapshot find() when the user calls unsubscribe()). Forwarded into the\n * internal AbortController so cancellation surfaces as a DOMException with\n * name 'AbortError' — the caller is responsible for distinguishing it from\n * the timeout-driven abort if it matters.\n */\n signal?: AbortSignal;\n}\n\nexport class HttpClient {\n private config: SpacelrClientConfig;\n private tokenManager: TokenManager;\n\n constructor(config: SpacelrClientConfig, tokenManager: TokenManager) {\n this.config = config;\n this.tokenManager = tokenManager;\n }\n\n async request<T>(options: HttpRequestOptions): Promise<T> {\n return this.requestWithRetry<T>(options, false);\n }\n\n private async requestWithRetry<T>(\n options: HttpRequestOptions,\n isRetry: boolean,\n ): Promise<T> {\n const url = this.buildUrl(options.path, options.query);\n const headers = await this.buildHeaders(options);\n const timeout = this.config.timeout ?? 30000;\n const includeCredentials = options.withCredentials ?? options.path.startsWith('/auth/');\n\n const controller = new AbortController();\n // Track timeout-vs-external precedence so the catch block can't be\n // fooled by a late-arriving external abort listener firing AFTER the\n // timeout already aborted (race window: timeout fires synchronously,\n // catch runs, then the external listener also fires from microtask\n // queue). Without `timeoutFired`, an external abort on top of a\n // timeout-driven abort would misclassify as caller-cancellation.\n let timeoutFired = false;\n const timeoutId = setTimeout(() => {\n timeoutFired = true;\n controller.abort();\n }, timeout);\n\n // Forward an external AbortSignal so callers can cancel the in-flight\n // request before the timeout. We track whether the abort was caller-driven\n // so the catch block can preserve the AbortError instead of masking it as\n // SpacelrTimeoutError.\n let externalAbort = false;\n let onExternalAbort: (() => void) | undefined;\n if (options.signal) {\n if (options.signal.aborted) {\n externalAbort = true;\n controller.abort();\n } else {\n onExternalAbort = () => {\n // Set the flag BEFORE controller.abort() so the catch block\n // reads the correct branch deterministically — abort() can\n // synchronously reject the in-flight fetch and run the catch,\n // and we want `externalAbort === true` to be observable there.\n externalAbort = true;\n controller.abort();\n };\n options.signal.addEventListener('abort', onExternalAbort);\n }\n }\n\n try {\n const response = await fetch(url, {\n method: options.method,\n headers,\n body: options.body ? JSON.stringify(options.body) : undefined,\n signal: controller.signal,\n ...(includeCredentials && { credentials: 'include' as RequestCredentials }),\n });\n\n const responseBody = await this.parseResponse(response);\n\n if (!response.ok) {\n if (options.authenticated && response.status === 401) {\n if (await this.recoverFromAuthFailure(isRetry)) {\n return this.requestWithRetry<T>(options, true);\n }\n }\n this.throwHttpError(response.status, responseBody);\n }\n\n return this.extractData<T>(responseBody);\n } catch (error) {\n if (error instanceof SpacelrError) throw error;\n\n if (error instanceof DOMException && error.name === 'AbortError') {\n // Preserve external aborts as AbortError so callers (e.g.\n // subscribeWithSnapshot) can silence them. Only timeout-driven aborts\n // get rewrapped as SpacelrTimeoutError. `!timeoutFired` is the\n // precedence guard: if the timeout already aborted, classify as\n // timeout regardless of whether the external listener also fired\n // afterwards.\n if (externalAbort && !timeoutFired) throw error;\n throw new SpacelrTimeoutError(timeout);\n }\n\n throw new SpacelrNetworkError(\n error instanceof Error ? error.message : 'Network request failed'\n );\n } finally {\n clearTimeout(timeoutId);\n if (options.signal && onExternalAbort) {\n options.signal.removeEventListener('abort', onExternalAbort);\n }\n }\n }\n\n async uploadForm<T>(\n path: string,\n formData: FormData,\n onProgress?: (event: { loaded: number; total: number }) => void,\n ): Promise<T> {\n return this.uploadFormWithRetry<T>(path, formData, onProgress, false);\n }\n\n private async uploadFormWithRetry<T>(\n path: string,\n formData: FormData,\n onProgress: ((event: { loaded: number; total: number }) => void) | undefined,\n isRetry: boolean,\n ): Promise<T> {\n const url = this.buildUrl(path);\n const headers = await this.buildFormHeaders();\n const timeoutMs = this.config.timeout ?? 30000;\n\n try {\n if (onProgress) {\n return await this.uploadFormWithProgress<T>(url, headers, formData, onProgress, timeoutMs);\n }\n return await this.uploadFormOnce<T>(url, headers, formData, timeoutMs);\n } catch (error) {\n if (error instanceof SpacelrAuthError && error.statusCode === 401) {\n if (await this.recoverFromAuthFailure(isRetry)) {\n return this.uploadFormWithRetry<T>(path, formData, onProgress, true);\n }\n }\n throw error;\n }\n }\n\n /**\n * Shared handler for a 401 on an authenticated request. Returns true if the\n * caller should retry with a refreshed token. 403 is never routed here —\n * it means \"forbidden for this action\" and must not trigger logout.\n *\n * `emitAuthLost('unauthorized')` is deduped inside TokenManager, so it's a\n * no-op if `executeRefresh` already emitted 'refresh-failed'.\n */\n private async recoverFromAuthFailure(isRetry: boolean): Promise<boolean> {\n if (isRetry) {\n this.tokenManager.emitAuthLost('unauthorized');\n return false;\n }\n const refreshed = await this.tokenManager.forceRefresh();\n if (refreshed) return true;\n this.tokenManager.emitAuthLost('unauthorized');\n return false;\n }\n\n private async uploadFormOnce<T>(\n url: string,\n headers: Record<string, string>,\n formData: FormData,\n timeoutMs: number,\n ): Promise<T> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: formData,\n signal: controller.signal,\n });\n\n const responseBody = await this.parseResponse(response);\n\n if (!response.ok) {\n this.throwHttpError(response.status, responseBody);\n }\n\n return this.extractData<T>(responseBody);\n } catch (error) {\n if (error instanceof SpacelrError) throw error;\n\n if (error instanceof DOMException && error.name === 'AbortError') {\n throw new SpacelrTimeoutError(timeoutMs);\n }\n\n throw new SpacelrNetworkError(\n error instanceof Error ? error.message : 'Network request failed'\n );\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n private uploadFormWithProgress<T>(\n url: string,\n headers: Record<string, string>,\n formData: FormData,\n onProgress: (event: { loaded: number; total: number }) => void,\n timeoutMs: number,\n ): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open('POST', url);\n xhr.timeout = timeoutMs;\n\n for (const [key, value] of Object.entries(headers)) {\n xhr.setRequestHeader(key, value);\n }\n\n xhr.upload.addEventListener('progress', (e) => {\n if (e.lengthComputable) {\n onProgress({ loaded: e.loaded, total: e.total });\n }\n });\n\n xhr.addEventListener('load', () => {\n try {\n const contentType = xhr.getResponseHeader('content-type') ?? '';\n if (!contentType.includes('application/json')) {\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve({ success: true, data: xhr.responseText } as unknown as T);\n } else if (xhr.status === 401 || xhr.status === 403) {\n // Surface as SpacelrAuthError for consistent typing with the\n // JSON path below. Only 401 actually triggers the upload retry\n // wrapper (uploadFormWithRetry filters on statusCode === 401);\n // 403 propagates unchanged as a permission denial.\n reject(new SpacelrAuthError(`HTTP ${xhr.status}`, xhr.status));\n } else {\n reject(new SpacelrNetworkError(`HTTP ${xhr.status}: ${xhr.responseText.slice(0, 200)}`));\n }\n return;\n }\n const body = JSON.parse(xhr.responseText);\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve(this.extractData<T>(body));\n } else {\n this.throwHttpError(xhr.status, body);\n }\n } catch (error) {\n if (error instanceof SpacelrError) {\n reject(error);\n } else {\n reject(new SpacelrNetworkError('Failed to parse response'));\n }\n }\n });\n\n xhr.addEventListener('error', () => {\n reject(new SpacelrNetworkError('Network request failed'));\n });\n\n xhr.addEventListener('timeout', () => {\n xhr.abort();\n reject(new SpacelrTimeoutError(timeoutMs));\n });\n\n xhr.send(formData);\n });\n }\n\n private buildUrl(\n path: string,\n query?: Record<string, string | number | undefined>\n ): string {\n const cleanPath = path.startsWith('/') ? path : `/${path}`;\n // .well-known endpoints are served at the origin without the API prefix\n const base = cleanPath.startsWith('/.well-known')\n ? new URL(this.config.apiUrl).origin\n : this.config.apiUrl.replace(/\\/+$/, '');\n const url = new URL(`${base}${cleanPath}`);\n\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined) {\n url.searchParams.set(key, String(value));\n }\n }\n }\n\n return url.toString();\n }\n\n private async buildHeaders(\n options: HttpRequestOptions\n ): Promise<Record<string, string>> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'x-client-id': this.config.clientId,\n 'x-project-id': this.config.projectId,\n ...options.headers,\n };\n\n if (options.authenticated) {\n const token = await this.tokenManager.getAccessToken();\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n }\n\n return headers;\n }\n\n private async buildFormHeaders(): Promise<Record<string, string>> {\n const headers: Record<string, string> = {\n 'x-client-id': this.config.clientId,\n 'x-project-id': this.config.projectId,\n };\n\n const token = await this.tokenManager.getAccessToken();\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n\n return headers;\n }\n\n private async parseResponse(\n response: Response\n ): Promise<ApiResponse | Record<string, unknown>> {\n const contentType = response.headers.get('content-type') ?? '';\n if (contentType.includes('application/json')) {\n return response.json();\n }\n const text = await response.text();\n return { success: response.ok, data: text };\n }\n\n private throwHttpError(\n statusCode: number,\n body: ApiResponse | Record<string, unknown>\n ): never {\n const apiBody = body as ApiResponse;\n const message =\n apiBody.error?.message ?? `HTTP ${statusCode}`;\n const code = apiBody.error?.code ?? `HTTP_${statusCode}`;\n const details = apiBody.error?.details;\n\n // #165: Nest BadRequestException with object body produces a flat\n // envelope `{ statusCode, message, errorCode, ... }`. Surface\n // SEARCH_FILTER_REQUIRED as a typed error so callers can discriminate\n // via `instanceof` instead of message-string matching.\n //\n // The `statusCode === 400` guard prevents an adversarial proxy or\n // misrouted response from masking a 401/403 by smuggling\n // `errorCode: 'SEARCH_FILTER_REQUIRED'` into the body — keep the\n // 401/403 → SpacelrAuthError path authoritative.\n const flatBody = body as Record<string, unknown>;\n if (statusCode === 400 && flatBody['errorCode'] === 'SEARCH_FILTER_REQUIRED') {\n const flatMessage =\n typeof flatBody['message'] === 'string'\n ? (flatBody['message'] as string)\n : message;\n const missingFields = Array.isArray(flatBody['missingFields'])\n ? (flatBody['missingFields'] as unknown[])\n : [];\n throw new SpacelrSearchFilterRequiredError(flatMessage, { missingFields });\n }\n\n if (statusCode === 401 || statusCode === 403) {\n throw new SpacelrAuthError(message, statusCode, details);\n }\n\n throw new SpacelrError(message, code, statusCode, details);\n }\n\n private extractData<T>(body: ApiResponse | Record<string, unknown>): T {\n const apiBody = body as ApiResponse;\n // If the response wraps data in a standard envelope, extract it\n if ('success' in apiBody && apiBody.data !== undefined) {\n const data = apiBody.data as Record<string, unknown>;\n // Check if the response indicates email verification is required\n if (data['emailVerificationRequired'] === true) {\n throw new SpacelrEmailVerificationRequiredError(data['emailSent'] === true);\n }\n // Check if the response indicates a 2FA challenge\n if (data['twoFactorRequired'] === true && typeof data['twoFactorToken'] === 'string') {\n throw new SpacelrTwoFactorRequiredError(data['twoFactorToken']);\n }\n return apiBody.data as T;\n }\n // Check unwrapped responses\n const rawBody = body as Record<string, unknown>;\n if (rawBody['emailVerificationRequired'] === true) {\n throw new SpacelrEmailVerificationRequiredError(rawBody['emailSent'] === true);\n }\n if (rawBody['twoFactorRequired'] === true && typeof rawBody['twoFactorToken'] === 'string') {\n throw new SpacelrTwoFactorRequiredError(rawBody['twoFactorToken']);\n }\n // Otherwise return the whole body (e.g. OIDC endpoints return raw objects)\n return body as T;\n }\n}\n","import { StoredTokens, TokenStorage } from '../types';\n\nexport type { TokenStorage };\n\nexport class MemoryTokenStorage implements TokenStorage {\n private tokens: StoredTokens | null = null;\n\n async getTokens(): Promise<StoredTokens | null> {\n return this.tokens;\n }\n\n async setTokens(tokens: StoredTokens): Promise<void> {\n this.tokens = tokens;\n }\n\n async clearTokens(): Promise<void> {\n this.tokens = null;\n }\n}\n\nexport class BrowserTokenStorage implements TokenStorage {\n private readonly storageKey: string;\n\n constructor(storageKey = 'spacelr_tokens') {\n this.storageKey = storageKey;\n }\n\n async getTokens(): Promise<StoredTokens | null> {\n try {\n const raw = localStorage.getItem(this.storageKey);\n if (!raw) return null;\n return JSON.parse(raw) as StoredTokens;\n } catch {\n return null;\n }\n }\n\n async setTokens(tokens: StoredTokens): Promise<void> {\n try {\n localStorage.setItem(this.storageKey, JSON.stringify(tokens));\n } catch {\n // Quota exceeded or private browsing — silently ignore\n }\n }\n\n async clearTokens(): Promise<void> {\n try {\n localStorage.removeItem(this.storageKey);\n } catch {\n // localStorage unavailable — silently ignore\n }\n }\n}\n","import { StoredTokens, TokenStorage } from '../types';\nimport { MemoryTokenStorage } from './token-storage';\n\nexport type RefreshCallback = (refreshToken: string) => Promise<StoredTokens>;\nexport type AuthLostReason = 'refresh-failed' | 'unauthorized';\nexport type TokenRefreshedListener = (tokens: StoredTokens) => void;\nexport type AuthLostListener = (reason: AuthLostReason) => void;\n\nexport class TokenManager {\n private storage: TokenStorage;\n private refreshBufferSeconds: number;\n private refreshCallback: RefreshCallback | null = null;\n private refreshPromise: Promise<StoredTokens> | null = null;\n private tokenRefreshedListeners = new Set<TokenRefreshedListener>();\n private authLostListeners = new Set<AuthLostListener>();\n // Guard so callbacks that run during auth-loss handling (e.g. a logout()\n // that hits `/auth/logout` with a dead token) don't re-emit and loop.\n // Cleared by setTokens() / clearTokens() to re-arm for the next session.\n private authLostEmitted = false;\n\n constructor(storage?: TokenStorage, refreshBufferSeconds = 60) {\n this.storage = storage ?? new MemoryTokenStorage();\n this.refreshBufferSeconds = refreshBufferSeconds;\n }\n\n setRefreshCallback(callback: RefreshCallback): void {\n this.refreshCallback = callback;\n }\n\n async getAccessToken(): Promise<string | null> {\n const tokens = await this.storage.getTokens();\n if (!tokens) return null;\n\n if (this.isTokenExpired(tokens)) {\n const refreshed = await this.tryRefresh(tokens);\n return refreshed?.accessToken ?? null;\n }\n\n if (this.shouldRefresh(tokens)) {\n // Trigger background refresh but return current token\n this.tryRefresh(tokens).catch(() => {\n // Ignore background refresh failures (onAuthLost will have fired)\n });\n }\n\n return tokens.accessToken;\n }\n\n async setTokens(tokens: StoredTokens): Promise<void> {\n // Defensive default: when a refreshable session comes in without an\n // explicit expiry (typical of bridge code that forgot to thread\n // `expires_in`), treat the access-token as already due. The next\n // `getAccessToken()` then takes the BLOCKING-refresh branch and the\n // very next authenticated request hits the gateway with a fresh\n // token. Worst case: one extra refresh round-trip — vs. silently\n // letting an unrefreshable session run forever client-side until a\n // 401 surfaces the expiry.\n //\n // Skip the default when no refreshToken is present: there's nothing\n // to refresh with, so forcing an \"expired\" state would just produce\n // useless `null` returns from getAccessToken. This also keeps\n // ephemeral test fixtures working.\n //\n // Use the same falsy-guard as tryRefresh (`!tokens.refreshToken`) so a\n // `refreshToken: ''` (some servers return an empty string when refresh\n // is unavailable for this session) doesn't trip the default and brick\n // an otherwise-valid ephemeral session by routing it into a refresh\n // path that immediately bails on the empty-string check.\n const needsDefault =\n tokens.expiresAt === undefined && !!tokens.refreshToken;\n const normalised: StoredTokens = needsDefault\n ? { ...tokens, expiresAt: Math.floor(Date.now() / 1000) }\n : tokens;\n await this.storage.setTokens(normalised);\n this.authLostEmitted = false;\n }\n\n async clearTokens(): Promise<void> {\n await this.storage.clearTokens();\n this.refreshPromise = null;\n this.authLostEmitted = false;\n }\n\n async getStoredTokens(): Promise<StoredTokens | null> {\n return this.storage.getTokens();\n }\n\n /**\n * Force a refresh using the current stored refresh token.\n * Returns the new access token, or null if refresh is not possible or fails.\n * A rejecting custom `TokenStorage.getTokens()` is treated as \"no tokens\"\n * so callers (notably `HttpClient`'s 401 retry path) see a clean `null`\n * rather than an exception that would get wrapped as a network error.\n */\n async forceRefresh(): Promise<string | null> {\n // Once auth is lost, further refresh attempts are pointless and would\n // spam the refresh endpoint while the consumer is handling the logout.\n if (this.authLostEmitted) return null;\n let tokens: StoredTokens | null;\n try {\n tokens = await this.storage.getTokens();\n } catch {\n return null;\n }\n if (!tokens) return null;\n const refreshed = await this.tryRefresh(tokens);\n return refreshed?.accessToken ?? null;\n }\n\n onTokenRefreshed(listener: TokenRefreshedListener): () => void {\n this.tokenRefreshedListeners.add(listener);\n return () => this.tokenRefreshedListeners.delete(listener);\n }\n\n onAuthLost(listener: AuthLostListener): () => void {\n this.authLostListeners.add(listener);\n return () => this.authLostListeners.delete(listener);\n }\n\n emitAuthLost(reason: AuthLostReason): void {\n if (this.authLostEmitted) return;\n this.authLostEmitted = true;\n for (const listener of this.authLostListeners) {\n try {\n listener(reason);\n } catch {\n // Ignore listener errors\n }\n }\n }\n\n private isTokenExpired(tokens: StoredTokens): boolean {\n if (!tokens.expiresAt) return false;\n return Date.now() >= tokens.expiresAt * 1000;\n }\n\n private shouldRefresh(tokens: StoredTokens): boolean {\n if (!tokens.expiresAt || !tokens.refreshToken) return false;\n const bufferMs = this.refreshBufferSeconds * 1000;\n return Date.now() >= tokens.expiresAt * 1000 - bufferMs;\n }\n\n private async tryRefresh(\n tokens: StoredTokens\n ): Promise<StoredTokens | null> {\n if (!tokens.refreshToken || !this.refreshCallback) return null;\n\n // Deduplicate concurrent refresh calls — both the leader and any followers\n // must get `null` on failure, not a rejected promise. Otherwise parallel\n // callers of getAccessToken/forceRefresh will throw unexpectedly while the\n // leader gracefully returns null.\n if (this.refreshPromise) {\n try {\n return await this.refreshPromise;\n } catch {\n return null;\n }\n }\n\n this.refreshPromise = this.executeRefresh(tokens.refreshToken);\n\n try {\n return await this.refreshPromise;\n } catch {\n return null;\n } finally {\n this.refreshPromise = null;\n }\n }\n\n private async executeRefresh(refreshToken: string): Promise<StoredTokens> {\n // `tryRefresh` guards against a missing callback before reaching here.\n const callback = this.refreshCallback as RefreshCallback;\n try {\n const newTokens = await callback(refreshToken);\n await this.storage.setTokens(newTokens);\n this.emitTokenRefreshed(newTokens);\n return newTokens;\n } catch (error) {\n this.emitAuthLost('refresh-failed');\n throw error;\n }\n }\n\n private emitTokenRefreshed(tokens: StoredTokens): void {\n for (const listener of this.tokenRefreshedListeners) {\n try {\n listener(tokens);\n } catch {\n // Ignore listener errors\n }\n }\n }\n}\n","export enum FileVisibility {\n PRIVATE = 'private',\n SHARED = 'shared',\n PUBLIC = 'public',\n}\n\nexport enum GrantType {\n AUTHORIZATION_CODE = 'authorization_code',\n CLIENT_CREDENTIALS = 'client_credentials',\n REFRESH_TOKEN = 'refresh_token',\n}\n\nexport enum CodeChallengeMethod {\n PLAIN = 'plain',\n S256 = 'S256',\n}\n\nexport enum SharePermission {\n READ = 'read',\n WRITE = 'write',\n}\n\nexport interface ApiResponse<T = unknown> {\n success: boolean;\n data?: T;\n error?: {\n message: string;\n code: string;\n details?: Record<string, unknown>;\n };\n}\n","import { PKCEChallenge, CodeChallengeMethod } from '../types';\n\nfunction generateRandomBytes(length: number): Uint8Array {\n if (\n typeof globalThis.crypto !== 'undefined' &&\n globalThis.crypto.getRandomValues\n ) {\n const bytes = new Uint8Array(length);\n globalThis.crypto.getRandomValues(bytes);\n return bytes;\n }\n\n // Node.js fallback\n const nodeCrypto = require('crypto') as typeof import('crypto');\n return new Uint8Array(nodeCrypto.randomBytes(length));\n}\n\nfunction base64UrlEncode(buffer: Uint8Array): string {\n let binary = '';\n for (let i = 0; i < buffer.length; i++) {\n binary += String.fromCharCode(buffer[i]);\n }\n\n const base64 = btoa(binary);\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nasync function sha256(input: string): Promise<Uint8Array> {\n if (\n typeof globalThis.crypto !== 'undefined' &&\n globalThis.crypto.subtle\n ) {\n const encoder = new TextEncoder();\n const data = encoder.encode(input);\n const hash = await globalThis.crypto.subtle.digest('SHA-256', data);\n return new Uint8Array(hash);\n }\n\n // Node.js fallback\n const nodeCrypto = require('crypto') as typeof import('crypto');\n const hash = nodeCrypto.createHash('sha256').update(input).digest();\n return new Uint8Array(hash);\n}\n\nexport async function generatePKCEChallenge(): Promise<PKCEChallenge> {\n const randomBytes = generateRandomBytes(32);\n const codeVerifier = base64UrlEncode(randomBytes);\n\n const hashBytes = await sha256(codeVerifier);\n const codeChallenge = base64UrlEncode(hashBytes);\n\n return {\n codeVerifier,\n codeChallenge,\n codeChallengeMethod: CodeChallengeMethod.S256,\n };\n}\n","import { io, Socket } from 'socket.io-client';\nimport { SpacelrError } from './errors';\n\n// Wait this long before retrying a failed socket rebuild. Short enough that\n// a transient network stumble heals without waiting for a user-visible wake\n// event, long enough not to hammer an unreachable server.\nconst REBUILD_RETRY_DELAY_MS = 5000;\n\n/**\n * Live (post-construction) handle to socket.io-client's Manager. The `reconnection`,\n * `reconnectionDelay`, `reconnectionDelayMax`, and `reconnectionAttempts` setters\n * mutate the Manager's private `_reconnection` / backoff state — `socket.io.opts.*`\n * mutations are inert post-construction (manager.js:52,72-79). `skipReconnect` is\n * a private flag the constructor sets to `true` when `reconnection: false` is\n * passed; the `reconnection(true)` setter does NOT clear it, so we have to.\n */\ninterface ManagerReconnectionAPI {\n reconnection: (v: boolean) => void;\n reconnectionDelay: (v: number) => void;\n reconnectionDelayMax: (v: number) => void;\n reconnectionAttempts: (v: number) => void;\n skipReconnect: boolean;\n}\n\n/** Ack error codes that indicate a permanent server-side rejection — evict\n * the subscription on reconnect rather than retrying forever. Transient\n * codes (redis-unavailable, replay-error, disconnected) stay in the map so\n * the next reconnect tries again. */\nconst PERMANENT_STREAM_ACK_ERRORS = new Set<string>([\n 'not-stream-collection',\n 'Collection not found',\n 'Invalid sinceId format',\n 'Not a member of this project',\n 'Subscribe denied',\n]);\n\nexport type ConnectionState = 'connected' | 'reconnecting' | 'disconnected';\n\n// NOTE: This is the public, client-facing event shape. It intentionally\n// diverges from `DatabaseChangeEvent` in\n// @spacelr-workspace/shared-types/database-events.ts, which still carries\n// the `_source`, `_sourceFunctionId`, and `_chainDepth` orchestration\n// fields used by function-service for loop prevention. The api-gateway\n// strips those fields before forwarding to SDK clients (see\n// `toPublicEvent` in apps/api-gateway-public/.../database-events.gateway.ts),\n// so the SDK type accurately reflects what subscribers actually receive.\nexport interface DatabaseChangeEvent {\n type: 'insert' | 'update' | 'delete';\n projectId: string;\n collectionName: string;\n documentId: string;\n document?: Record<string, unknown>;\n timestamp: number;\n /**\n * Redis Stream entry ID (e.g. \"1745234123-0\") for stream-mode events, or a\n * synthesised `${projectId}:${collectionName}:${timestamp}-${documentId}`\n * for pubsub-mode events. Used by function triggers as an idempotency key.\n */\n eventId?: string;\n}\n\nexport interface RealtimeConfig {\n baseUrl: string;\n getToken: () => Promise<string | null>;\n /**\n * Optional subscription to token-refresh events. When wired, the realtime\n * client will push the fresh access token to the server via a\n * `reauthenticate` message so the open socket stays authenticated without\n * dropping the connection.\n */\n onTokenRefreshed?: (listener: (accessToken: string) => void) => () => void;\n}\n\nexport type GapReason =\n | 'outside-retention-window'\n | 'redis-unavailable'\n | 'replay-error'\n | 'replay-truncated';\n\nexport interface StreamGapInfo {\n projectId: string;\n collectionName: string;\n /** Configured retention as a human-readable string (e.g. \"maxLen: 10000\", \"24h\", or \"unknown\"). */\n retentionWindow: string;\n reason: GapReason;\n}\n\nexport interface StreamSubscriptionOptions {\n projectId: string;\n collectionName: string;\n sinceId?: string;\n where?: Record<string, string | number | boolean>;\n /**\n * Called for each delivered event. Awaited — the cursor does NOT advance\n * until this resolves. If the callback throws, the cursor is NOT advanced\n * and `onError` is called; the event is not retried (at-most-once on\n * handler failure, consistent with the server-side StreamReaderService).\n */\n onEvent: (entry: { eventId: string; event: DatabaseChangeEvent }) => void | Promise<void>;\n /** Called when the server emits an event-gap. See GapReason for the four codes. */\n onGap?: (info: StreamGapInfo) => void;\n /** Called on handshake rejection (not-stream-collection, invalid sinceId, etc.) and handler failures. */\n onError?: (error: Error) => void;\n}\n\ninterface StreamSubscriptionState {\n options: StreamSubscriptionOptions;\n /** Last eventId successfully delivered to onEvent. Advances after each resolved handler. */\n lastDeliveredId: string | null;\n /** `stream:<projectId>:<collectionName>` — used as the Map key. */\n streamKey: string;\n /** Serializes onEvent calls for THIS subscriber so cursor advances in order\n * even when the socket delivers multiple events before the previous handler resolves. */\n dispatchQueue: Promise<void>;\n}\n\ninterface StreamSubscribeAck {\n error?: string;\n message?: string;\n subscribed?: string;\n lastDeliveredId?: string | null;\n // NEW for #99: server returns these on every subscribe-events ack\n // so the SDK can fail-fast on pubsub collections and anchor the\n // snapshot baseline atomically with the reader-attach. Both fields\n // are optional so older servers that don't yet send them don't\n // break the SDK build — the metadata-aware wrapper defaults to\n // pubsub mode so the fail-fast still fires.\n realtimeMode?: 'pubsub' | 'stream';\n latestStreamId?: string | null;\n}\n\nexport class RealtimeClient {\n private socket: Socket | null = null;\n private config: RealtimeConfig;\n private subscriptions = new Map<string, Set<(event: DatabaseChangeEvent) => void>>();\n private connecting: Promise<void> | null = null;\n // Store original where objects per room for reconnect resubscription\n private roomWhereMap = new Map<string, Record<string, string | number | boolean>>();\n private unsubscribeFromTokenRefreshed: (() => void) | null = null;\n // Wake-up listeners (browser only) — recover from long OS suspends where\n // socket.io's internal reconnect loop may have already given up.\n private onVisibilityChange: (() => void) | null = null;\n private onOnline: (() => void) | null = null;\n // Set by `disconnect()`; checked by async paths (rebuildSocket, deferred\n // retries) to avoid reviving a client the consumer has torn down.\n private disposed = false;\n // Scheduled retry after a failed `rebuildSocket()`; cleared when it fires\n // or when `disconnect()` tears down.\n private rebuildRetryTimer: ReturnType<typeof setTimeout> | null = null;\n private connectionState: ConnectionState = 'disconnected';\n private connectionStateListeners = new Set<(state: ConnectionState) => void>();\n private streamSubscriptions = new Map<string, Set<StreamSubscriptionState>>();\n\n constructor(config: RealtimeConfig) {\n this.config = config;\n }\n\n async subscribe(\n projectId: string,\n collectionName: string,\n callback: (event: DatabaseChangeEvent) => void,\n onError?: (error: Error) => void,\n where?: Record<string, string | number | boolean>,\n ): Promise<() => void> {\n this.ensureWakeListeners();\n if (this.connectionState === 'disconnected') {\n this.setConnectionState('reconnecting');\n }\n await this.ensureConnected();\n\n const room = this.buildRoomKey(projectId, collectionName, where);\n\n // Track the callback\n if (!this.subscriptions.has(room)) {\n this.subscriptions.set(room, new Set());\n }\n const callbacks = this.subscriptions.get(room);\n callbacks?.add(callback);\n\n // If this is the first subscription for this room, tell the server\n if (callbacks?.size === 1) {\n if (where && Object.keys(where).length > 0) {\n this.roomWhereMap.set(room, where);\n }\n const payload: Record<string, unknown> = { projectId, collectionName };\n if (where && Object.keys(where).length > 0) {\n payload['where'] = where;\n }\n this.socket?.emit(\n 'subscribe',\n payload,\n (response: { subscribed?: string; error?: string }) => {\n if (response.error && onError) {\n onError(new Error(response.error));\n }\n },\n );\n }\n\n // Return unsubscribe function\n return () => {\n const callbacks = this.subscriptions.get(room);\n if (callbacks) {\n callbacks.delete(callback);\n if (callbacks.size === 0) {\n this.subscriptions.delete(room);\n this.roomWhereMap.delete(room);\n const payload: Record<string, unknown> = { projectId, collectionName };\n if (where && Object.keys(where).length > 0) {\n payload['where'] = where;\n }\n this.socket?.emit('unsubscribe', payload);\n }\n }\n };\n }\n\n /**\n * Subscribe to a stream-mode collection using Redis Streams replay +\n * cursor-based delivery. Parallel to `subscribe()` (which targets\n * pubsub-mode collections). The server validates that the collection is\n * configured in stream mode and rejects the handshake otherwise.\n *\n * The per-subscription cursor (`lastDeliveredId`) advances after each\n * `onEvent` promise resolves, so reconnects resume from the last delivered\n * event rather than the originally-subscribed `sinceId`.\n */\n async subscribeWithCursor(\n options: StreamSubscriptionOptions,\n ): Promise<() => void> {\n const { unsubscribe } = await this._subscribeInternal(options);\n return unsubscribe;\n }\n\n /**\n * Metadata-aware variant of `subscribeWithCursor()` — see #99.\n *\n * Identical to `subscribeWithCursor` except it also returns the\n * `realtimeMode` and `latestStreamId` from the server's\n * subscribe-events ack so callers (notably `subscribeWithSnapshot`)\n * can fail-fast on pubsub-mode collections and anchor the snapshot\n * baseline atomically with the reader-attach.\n *\n * Defaults: if the server didn't send `realtimeMode` (older\n * gateway), the SDK treats the collection as pubsub-mode so\n * downstream fail-fasts still fire correctly. `latestStreamId`\n * defaults to `null` when absent.\n *\n * Dedup-path note: when a sibling local subscription already holds\n * the server-side reader slot for this streamKey, no new handshake\n * is emitted — the SDK piggy-backs on the existing slot. In that\n * case we synthesise `{ realtimeMode: 'stream', latestStreamId: null }`\n * because the existing slot's prior ack is no longer available.\n * Callers that need a fresh `latestStreamId` (e.g. snapshot anchor)\n * MUST handle the `null` case.\n */\n async subscribeWithCursorWithMeta(\n options: StreamSubscriptionOptions,\n ): Promise<{\n unsubscribe: () => void;\n realtimeMode: 'pubsub' | 'stream';\n latestStreamId: string | null;\n }> {\n const { unsubscribe, ack } = await this._subscribeInternal(options);\n return {\n unsubscribe,\n // Default to pubsub if the server didn't send the field — older\n // gateways that haven't been updated yet are treated as pubsub-mode\n // so the SDK helper's fail-fast still fires correctly.\n realtimeMode: ack.realtimeMode ?? 'pubsub',\n latestStreamId: ack.latestStreamId ?? null,\n };\n }\n\n /**\n * Shared body for `subscribeWithCursor` and\n * `subscribeWithCursorWithMeta`. Owns the handshake, dedup path,\n * pre-ack registration, and error eviction. Returns the unsubscribe\n * function plus the subscribe-events ack so metadata-aware callers\n * can read `realtimeMode` / `latestStreamId`.\n *\n * Behaviour-preserving contract — DO NOT change without re-running\n * the singleflight tests in `realtime.spec.ts`:\n *\n * 1. Dedup branch BEFORE registration: if a sibling subscriber for\n * this streamKey is already registered, the new caller piggy-backs\n * on its server-side slot. Re-emitting the handshake here would\n * tear the prior subscriber's reader down.\n * 2. Registration BEFORE handshake: `streamSubscriptions.set` must\n * fire before `emitSubscribeEvents` so replay/event-gap frames\n * delivered DURING the handshake fanout find this subscriber.\n * 3. evictStreamKey on ack-error MUST sweep dedup-path siblings:\n * they have no independent server-side state and would silently\n * miss every event until the next full reconnect.\n * 4. The catch block is a safety net for the re-thrown ack-error\n * only — `emitSubscribeEvents` itself always resolves, so this\n * catch should never fire on the success path.\n */\n private async _subscribeInternal(\n options: StreamSubscriptionOptions,\n ): Promise<{ unsubscribe: () => void; ack: StreamSubscribeAck }> {\n this.ensureWakeListeners();\n if (this.connectionState === 'disconnected') {\n this.setConnectionState('reconnecting');\n }\n await this.ensureConnected();\n\n const streamKey = `stream:${options.projectId}:${options.collectionName}`;\n const state: StreamSubscriptionState = {\n options,\n lastDeliveredId: options.sinceId ?? null,\n streamKey,\n dispatchQueue: Promise.resolve(),\n };\n\n // Dedup path: if another local subscriber already holds the server-side\n // slot for this streamKey, just piggy-back on the existing fan-out. The\n // gateway collapses duplicate `subscribe-events` per socket per streamKey\n // (replacing the prior reader), so re-emitting would tear down the other\n // subscriber's server-side state.\n const existing = this.streamSubscriptions.get(streamKey);\n if (existing && existing.size > 0) {\n existing.add(state);\n // Synthesised ack for the dedup path — there is no fresh server\n // ack to read. The existing slot was successfully established as\n // a stream-mode subscription (otherwise the prior subscribe would\n // have failed and torn the entry down), so `realtimeMode: 'stream'`\n // is safe. `latestStreamId` is null because we don't have a fresh\n // server-side anchor for this caller.\n const syntheticAck: StreamSubscribeAck = {\n subscribed: streamKey,\n realtimeMode: 'stream',\n latestStreamId: null,\n };\n return {\n unsubscribe: () => this.unsubscribeStream(state),\n ack: syntheticAck,\n };\n }\n\n // Register BEFORE the handshake so replay `event` / `event-gap` frames\n // emitted during subscribe fanout find this subscriber. If the ack comes\n // back with an error, we tear down the registration in the catch block.\n const set = new Set<StreamSubscriptionState>();\n set.add(state);\n this.streamSubscriptions.set(streamKey, set);\n\n try {\n const ack = await this.emitSubscribeEvents(state);\n if (ack.error) {\n // The primary handshake failed. Evict this subscriber AND any other\n // subscribers that joined via the dedup path for the same streamKey —\n // they have no active server subscription, so they would silently miss\n // events until the next full reconnect.\n //\n // Throw a SpacelrError that preserves `ack.error` as the `code`\n // property so higher layers can discriminate by ack-error code\n // (e.g. `subscribeWithSnapshot` maps `'not-stream-collection'` to\n // `SUBSCRIBE_WITH_SNAPSHOT_REQUIRES_STREAM`) without string-matching\n // on the human message.\n const message = ack.message ?? ack.error;\n const err = new SpacelrError(message, ack.error);\n this.evictStreamKey(streamKey, err, state);\n options.onError?.(err);\n throw err;\n }\n return {\n unsubscribe: () => this.unsubscribeStream(state),\n ack,\n };\n } catch (err) {\n // Safety net: emitSubscribeEvents always resolves, so this catch only\n // fires when `throw err` from the ack-error branch re-runs here. evictStreamKey\n // is idempotent — the second call is a no-op if the streamKey entry was already\n // deleted. Kept in case a future edit introduces a throw before the error branch.\n const error = err instanceof Error ? err : new Error(String(err));\n this.evictStreamKey(streamKey, error, state);\n throw error;\n }\n }\n\n /**\n * Tear down the realtime client permanently. After this call the instance\n * is disposed — subsequent `subscribe()` calls will not re-establish a\n * connection. Create a new `RealtimeClient` (via `createClient`) if you\n * need to reconnect after a logout.\n */\n disconnect(): void {\n this.setConnectionState('disconnected');\n this.disposed = true;\n if (this.rebuildRetryTimer) {\n clearTimeout(this.rebuildRetryTimer);\n this.rebuildRetryTimer = null;\n }\n if (this.unsubscribeFromTokenRefreshed) {\n this.unsubscribeFromTokenRefreshed();\n this.unsubscribeFromTokenRefreshed = null;\n }\n this.detachWakeListeners();\n if (this.socket) {\n this.socket.disconnect();\n this.socket = null;\n }\n this.subscriptions.clear();\n this.roomWhereMap.clear();\n this.streamSubscriptions.clear();\n this.connecting = null;\n }\n\n getConnectionState(): ConnectionState {\n return this.connectionState;\n }\n\n onConnectionStateChanged(listener: (state: ConnectionState) => void): () => void {\n this.connectionStateListeners.add(listener);\n return () => this.connectionStateListeners.delete(listener);\n }\n\n private setConnectionState(next: ConnectionState): void {\n if (this.connectionState === next) return;\n this.connectionState = next;\n for (const listener of this.connectionStateListeners) {\n try {\n listener(next);\n } catch {\n // Ignore listener errors, matching onTokenRefreshed / onAuthLost.\n }\n }\n }\n\n private buildRoomKey(\n projectId: string,\n collectionName: string,\n where?: Record<string, string | number | boolean>,\n ): string {\n const base = `db:${projectId}:${collectionName}`;\n if (!where || Object.keys(where).length === 0) {\n return base;\n }\n const filterStr = Object.keys(where)\n .sort()\n .map((k) => `${k}=${String(where[k])}`)\n .join('&');\n return `${base}?${filterStr}`;\n }\n\n private async ensureConnected(): Promise<void> {\n if (this.socket?.connected) return;\n\n if (this.connecting) {\n return this.connecting;\n }\n\n this.connecting = this.connect();\n try {\n await this.connecting;\n } finally {\n this.connecting = null;\n }\n }\n\n private async connect(): Promise<void> {\n const token = await this.config.getToken();\n if (!token) {\n throw new Error('No authentication token available');\n }\n\n // Derive WebSocket URL from the API URL\n const wsUrl = this.config.baseUrl.replace(/\\/api\\/v\\d+\\/?$/, '');\n\n return new Promise<void>((resolve, reject) => {\n let initialConnect = true;\n // Forward-declared so the `authenticated` handler can deregister it\n // explicitly after first auth — closure-retained `reject` references\n // shouldn't outlive the handshake window. socket.io's `once` would\n // self-remove on first fire, but we'd still pay one no-op fire on the\n // first post-auth disconnect; matching the explicit-`off()` pattern\n // used by `emitSubscribeEvents` keeps the handshake state machine\n // tidy.\n let onInitialDisconnect: ((reason: Socket.DisconnectReason) => void) | null = null;\n\n this.socket = io(`${wsUrl}/database`, {\n auth: async (cb: (data: Record<string, unknown>) => void) => {\n try {\n const freshToken = await this.config.getToken();\n cb({ token: freshToken });\n } catch {\n cb({ token: null });\n }\n },\n transports: ['websocket'],\n // Disable auto-reconnect for the INITIAL connect — caller's promise\n // rejects on connect_error and they decide whether to retry. Once the\n // first authentication succeeds we re-enable it via the Manager's\n // setters (see authenticated handler).\n reconnection: false,\n });\n\n this.socket.on('authenticated', () => {\n // If the consumer called disconnect() while an in-flight authenticate\n // was in progress, don't emit a spurious `connected` on a disposed\n // client. socket.io can deliver a queued event after the socket was\n // detached — the `this.socket = null` and disposed flag are set but\n // the handler is still bound to the old socket instance.\n if (this.disposed) return;\n this.setConnectionState('connected');\n if (initialConnect) {\n initialConnect = false;\n // Enable reconnection AFTER the first successful auth.\n //\n // socket.io-client v4 reads `opts.reconnection` ONLY in the Manager\n // constructor (manager.js:52). Mutating `socket.io.opts.reconnection`\n // post-construction is a no-op — the live setting lives in the\n // private `_reconnection` field, mutable only via the `reconnection(v)`\n // setter (manager.js:72-79). And because `reconnection: false` was\n // passed at construction, the constructor also set `skipReconnect = true`\n // (manager.js:76-78), which the `reconnection(true)` setter does NOT\n // reset — we have to clear it ourselves. Without this, every WS drop\n // (Wi-Fi blip, server restart, NAT timeout) leaves the SDK\n // permanently disconnected.\n if (this.socket) {\n const manager = this.socket.io as unknown as ManagerReconnectionAPI;\n // Defensive: if socket.io-client ever bumps its internal API,\n // any of these setters could be missing (or `skipReconnect`\n // renamed). Wrap the whole sequence in try/catch and `reject()`\n // on failure — a bare throw here would propagate as an unhandled\n // exception in socket.io's dispatch loop (we're inside an event\n // handler, NOT the Promise executor body), leaving connect()\n // hanging forever. Same failure mode the once('disconnect')\n // guard prevents, and that guard wouldn't catch this either —\n // `initialConnect` is already cleared by this point.\n try {\n manager.reconnection(true);\n manager.skipReconnect = false;\n manager.reconnectionDelay(1000);\n manager.reconnectionDelayMax(5000);\n manager.reconnectionAttempts(50);\n } catch (err) {\n // Drop the handshake-disconnect handler before rejecting so the\n // closure retaining `reject` doesn't linger on the socket\n // emitter. The `if (initialConnect)` guard inside the handler\n // already makes it a no-op (initialConnect is false here), but\n // explicit cleanup keeps the symmetry with the success path.\n if (onInitialDisconnect && this.socket) {\n this.socket.off('disconnect', onInitialDisconnect);\n onInitialDisconnect = null;\n }\n // Tear the socket down. We already flipped state to 'connected'\n // before entering the setter block, so listeners saw a brief\n // 'connected' event — undo that with an explicit\n // 'disconnected' transition. Without this, a subsequent\n // `subscribe()` would see `this.socket?.connected === true`,\n // skip `ensureConnected()`, and keep using a socket that can\n // never reconnect on drop. (Exceptional path: only fires if\n // socket.io-client drops its internal Manager API.)\n this.socket?.disconnect();\n this.socket = null;\n this.setConnectionState('disconnected');\n reject(\n new Error(\n 'socket.io-client Manager reconnection setters failed — internal API may have changed; ' +\n 'auto-reconnect cannot be enabled. Check socket.io-client version compatibility. ' +\n `Underlying error: ${err instanceof Error ? err.message : String(err)}`,\n ),\n );\n return;\n }\n }\n // Initial handshake is past — drop the `disconnect` rejection\n // handler so its closure stops retaining `reject` (and so we don't\n // pay one no-op fire on the next live-session drop).\n if (onInitialDisconnect && this.socket) {\n this.socket.off('disconnect', onInitialDisconnect);\n onInitialDisconnect = null;\n }\n // Wire up token-refresh → reauthenticate so the open socket picks up\n // new JWTs without disconnecting.\n if (this.config.onTokenRefreshed && !this.unsubscribeFromTokenRefreshed) {\n this.unsubscribeFromTokenRefreshed = this.config.onTokenRefreshed(\n (accessToken) => {\n this.socket?.emit('reauthenticate', { token: accessToken });\n },\n );\n }\n resolve();\n } else {\n // Reconnect path — server has now set socket.data.userId. It is\n // safe to re-subscribe. (Doing this on 'connect' races the async\n // handleConnection and produces silent \"Not authenticated\" errors.)\n this.resubscribeAll();\n void this.resubscribeAllStreams();\n }\n });\n\n this.socket.on('connect_error', (err) => {\n if (initialConnect) {\n reject(new Error(`WebSocket connection failed: ${err.message}`));\n }\n });\n\n // Mid-handshake disconnect: the transport connected (TCP up) but the\n // socket dropped before `authenticated` fired (Wi-Fi blip during the\n // server-side auth handshake, server-side socket.disconnect(), etc.).\n // Without this guard, neither `authenticated` nor `connect_error` ever\n // runs and the caller's promise hangs forever. With `reconnection: false`\n // for the initial connect there's also no auto-retry to recover, so we\n // have to surface the failure ourselves. The handler is deregistered\n // by the `authenticated` branch above once the handshake completes;\n // live-session drops flow through the second `disconnect` listener\n // below as the start of an auto-reconnect.\n onInitialDisconnect = (reason: Socket.DisconnectReason): void => {\n if (!initialConnect) return;\n // Mirror the setter-failure cleanup: tear down the dead socket and\n // flip state to 'disconnected' so getConnectionState() reflects\n // reality. `subscribe()` set state to 'reconnecting' when this\n // attempt began; without this transition, listeners would see\n // 'reconnecting' indefinitely after the connect() promise rejects.\n //\n // We deliberately do NOT clear `initialConnect` here. The permanent\n // `disconnect` handler below skips the 'reconnecting' transition\n // while `initialConnect` is true; flipping it false would defeat\n // that guard and cause a spurious 'reconnecting' event after our\n // 'disconnected' transition. The success path's\n // `initialConnect = false` runs only on `authenticated`, never\n // here — there's no second initial-handshake to gate.\n this.socket?.disconnect();\n this.socket = null;\n this.setConnectionState('disconnected');\n reject(new Error(`WebSocket disconnected during handshake: ${reason}`));\n };\n this.socket.on('disconnect', onInitialDisconnect);\n\n // socket.io has exhausted its reconnection attempts (e.g. after a long\n // laptop suspend where all retries burned in a few seconds on resume).\n // Build a fresh socket so the session can recover.\n this.socket.io.on('reconnect_failed', () => {\n void this.rebuildSocket();\n });\n\n this.socket.on('db:event', (event: DatabaseChangeEvent) => {\n const base = `db:${event.projectId}:${event.collectionName}`;\n\n // Dispatch to whole-collection subscribers via direct Map lookup\n const baseCallbacks = this.subscriptions.get(base);\n if (baseCallbacks) {\n for (const cb of baseCallbacks) {\n try { cb(event); } catch { /* ignore callback errors */ }\n }\n }\n\n // Dispatch to matching filtered subscriptions\n for (const [room, callbacks] of this.subscriptions) {\n if (room.startsWith(`${base}?`) && this.eventMatchesRoom(event, room)) {\n for (const cb of callbacks) {\n try { cb(event); } catch { /* ignore callback errors */ }\n }\n }\n }\n });\n\n this.socket.on('event', (payload: DatabaseChangeEvent & { eventId?: string }) => {\n this.dispatchStreamEvent(payload).catch(() => undefined);\n });\n\n this.socket.on('event-gap', (info: StreamGapInfo) => {\n const streamKey = `stream:${info.projectId}:${info.collectionName}`;\n const set = this.streamSubscriptions.get(streamKey);\n if (!set) return;\n for (const state of set) {\n state.options.onGap?.(info);\n }\n });\n\n this.socket.on('disconnect', () => {\n if (this.disposed) return;\n // Suppress the 'reconnecting' transition during the initial-handshake\n // window. If the socket dropped before `authenticated`, the\n // onInitialDisconnect handler above has already rejected the\n // connect() promise — flipping state to 'reconnecting' here would\n // mislead listeners about a reconnect that never gets scheduled.\n // Live-session drops (post-auth) reach this branch with\n // initialConnect=false and DO transition to 'reconnecting' to\n // signal the auto-reconnect cycle.\n if (initialConnect) return;\n // Skip if the socket was already torn down programmatically by the\n // setter-failure catch (`this.socket = null` before `reject()`).\n // socket.io-client may emit `disconnect` asynchronously after our\n // explicit `socket.disconnect()` call returns, by which point the\n // catch block has already flipped state to 'disconnected' — we\n // mustn't undo that with a spurious 'reconnecting'. Same guard\n // applies if a future code path nulls `this.socket` while leaving\n // listeners attached.\n if (!this.socket) return;\n this.setConnectionState('reconnecting');\n });\n });\n }\n\n private eventMatchesRoom(event: DatabaseChangeEvent, room: string): boolean {\n const base = `db:${event.projectId}:${event.collectionName}`;\n if (room === base) return true;\n if (!room.startsWith(`${base}?`)) return false;\n\n // For filtered rooms, check the document against the where filter\n const where = this.roomWhereMap.get(room);\n if (!where) return true; // No filter stored for this room\n if (!event.document) return false; // Delete events without document can't match a filter\n\n for (const [key, value] of Object.entries(where)) {\n const docValue = event.document[key];\n if (Array.isArray(docValue)) {\n if (!docValue.some((item) => String(item) === String(value))) {\n return false;\n }\n } else if (String(docValue) !== String(value)) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * Tear down the current socket and create a fresh one. Used by both\n * `reconnect_failed` (socket.io gave up) and the visibility/online wake-up\n * listeners. Preserves the `subscriptions` map so existing rooms can be\n * replayed to the server on the new connection. If the rebuild itself\n * fails it schedules one retry after 5 s — beyond that we rely on the\n * next wake-up event (visibility/online) to try again.\n */\n private async rebuildSocket(): Promise<void> {\n if (this.disposed || this.connecting) return;\n\n const previous = this.socket;\n this.socket = null;\n if (previous) previous.disconnect();\n\n this.setConnectionState('reconnecting');\n\n try {\n await this.ensureConnected();\n this.handlePostRebuild();\n } catch {\n this.scheduleRebuildRetry();\n }\n }\n\n private handlePostRebuild(): void {\n // Capture into a local first — TS narrows `this.socket` to `null` after\n // the earlier assignment and doesn't re-widen through the await above.\n const newSocket: Socket | null = this.socket;\n if (this.disposed) {\n this.socket = null;\n newSocket?.disconnect();\n return;\n }\n // The server has no record of the subscriptions we held on the old\n // (dead) socket, so replay them here. The new socket's `authenticated`\n // handler took the `initialConnect = true` branch and doesn't resubscribe.\n if (this.subscriptions.size > 0) {\n this.resubscribeAll();\n }\n if (this.streamSubscriptions.size > 0) {\n void this.resubscribeAllStreams();\n }\n }\n\n private scheduleRebuildRetry(): void {\n // A transient rebuild failure (token fetch rejected, connect_error) —\n // retry once so recovery doesn't stall waiting for a wake event.\n if (this.disposed || this.rebuildRetryTimer) return;\n this.rebuildRetryTimer = setTimeout(() => {\n this.rebuildRetryTimer = null;\n void this.rebuildSocket();\n }, REBUILD_RETRY_DELAY_MS);\n (this.rebuildRetryTimer as unknown as { unref?: () => void }).unref?.();\n }\n\n private ensureWakeListeners(): void {\n if (typeof document === 'undefined' || typeof window === 'undefined') return;\n if (this.onVisibilityChange !== null) return;\n\n const wakeIfUnhealthy = () => {\n if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;\n if (this.socket?.connected) return;\n if (this.connecting) return;\n void this.rebuildSocket();\n };\n\n this.onVisibilityChange = wakeIfUnhealthy;\n this.onOnline = wakeIfUnhealthy;\n document.addEventListener('visibilitychange', this.onVisibilityChange);\n window.addEventListener('online', this.onOnline);\n }\n\n private detachWakeListeners(): void {\n if (typeof document !== 'undefined' && this.onVisibilityChange) {\n document.removeEventListener('visibilitychange', this.onVisibilityChange);\n }\n if (typeof window !== 'undefined' && this.onOnline) {\n window.removeEventListener('online', this.onOnline);\n }\n this.onVisibilityChange = null;\n this.onOnline = null;\n }\n\n private resubscribeAll(): void {\n for (const [room] of this.subscriptions) {\n // Parse room format: db:{projectId}:{collectionName} or db:{projectId}:{collectionName}?filter\n const queryIdx = room.indexOf('?');\n const basePart = queryIdx >= 0 ? room.substring(0, queryIdx) : room;\n const parts = basePart.split(':');\n if (parts.length >= 3) {\n const projectId = parts[1];\n const collectionName = parts.slice(2).join(':');\n const payload: Record<string, unknown> = { projectId, collectionName };\n\n // Use stored where object to preserve original types (number, boolean)\n const where = this.roomWhereMap.get(room);\n if (where) {\n payload['where'] = where;\n }\n\n this.socket?.emit(\n 'subscribe',\n payload,\n (response: { subscribed?: string; error?: string }) => {\n if (response?.error) {\n // Server rejected resubscription — clean up the stale subscription\n this.subscriptions.delete(room);\n this.roomWhereMap.delete(room);\n }\n },\n );\n }\n }\n }\n\n private emitSubscribeEvents(\n state: StreamSubscriptionState,\n ): Promise<StreamSubscribeAck> {\n return new Promise((resolve) => {\n if (!this.socket) {\n resolve({ error: 'disconnected' });\n return;\n }\n const socket = this.socket;\n\n // socket.io does not call ack callbacks when the socket disconnects mid-handshake.\n // Resolve on disconnect so the Promise can't hang.\n const onDisconnect = (): void => {\n socket.off('disconnect', onDisconnect);\n resolve({ error: 'disconnected' });\n };\n socket.once('disconnect', onDisconnect);\n\n socket.emit(\n 'subscribe-events',\n this.buildStreamPayload(state),\n (ack: StreamSubscribeAck) => {\n socket.off('disconnect', onDisconnect);\n resolve(ack);\n },\n );\n });\n }\n\n private buildStreamPayload(state: StreamSubscriptionState): Record<string, unknown> {\n const { projectId, collectionName, where } = state.options;\n const payload: Record<string, unknown> = { projectId, collectionName };\n // Use the current cursor (advanced by onEvent) so a reconnect picks up\n // from the last delivered event, not the originally-subscribed sinceId.\n if (state.lastDeliveredId) payload['sinceId'] = state.lastDeliveredId;\n if (where && Object.keys(where).length > 0) payload['where'] = where;\n return payload;\n }\n\n private unsubscribeStream(state: StreamSubscriptionState): void {\n const set = this.streamSubscriptions.get(state.streamKey);\n if (!set) return;\n set.delete(state);\n if (set.size === 0) {\n this.streamSubscriptions.delete(state.streamKey);\n // Only release the server-side reader slot when no local subscriber\n // remains for this streamKey. Avoids tearing down a shared slot when\n // one of several local consumers unsubscribes.\n this.socket?.emit('unsubscribe-events', {\n projectId: state.options.projectId,\n collectionName: state.options.collectionName,\n });\n }\n }\n\n private dispatchStreamEvent(\n payload: DatabaseChangeEvent & { eventId?: string },\n ): Promise<void> {\n if (!payload.eventId) return Promise.resolve();\n const streamKey = `stream:${payload.projectId}:${payload.collectionName}`;\n const set = this.streamSubscriptions.get(streamKey);\n if (!set) return Promise.resolve();\n\n const entryId = payload.eventId;\n // Per-subscriber queue — serializes onEvent calls within a subscriber while\n // allowing different subscribers to run in parallel. Without this, two\n // events arriving while the first handler is still awaiting could advance\n // the cursor out of order.\n for (const state of set) {\n state.dispatchQueue = state.dispatchQueue.then(async () => {\n // Guard: subscription may have been cancelled while this was queued.\n const liveSet = this.streamSubscriptions.get(state.streamKey);\n if (!liveSet?.has(state)) return;\n\n try {\n await state.options.onEvent({ eventId: entryId, event: payload });\n state.lastDeliveredId = entryId;\n } catch (err) {\n // At-most-once on handler failure — don't advance the cursor.\n // Surface via onError if provided.\n const error = err instanceof Error ? err : new Error(String(err));\n try {\n state.options.onError?.(error);\n } catch {\n // onError must not break the dispatch chain — swallow any throw.\n }\n }\n });\n }\n return Promise.resolve();\n }\n\n private async resubscribeAllStreams(): Promise<void> {\n // Re-subscribe any active stream subscriptions with the CURRENT cursor so\n // reconnects pick up exactly where we left off. Emit ONE handshake per\n // streamKey — the gateway collapses duplicate `subscribe-events` per\n // socket per streamKey, so re-emitting for every local subscriber would\n // tear down prior state. Replay events delivered over the single\n // handshake fan out to all local subscribers via dispatchStreamEvent.\n //\n // For shared-slot subscriptions, use the EARLIEST `lastDeliveredId` across\n // subscribers so none miss events they had not yet observed.\n const work: Promise<void>[] = [];\n for (const set of this.streamSubscriptions.values()) {\n const states = Array.from(set);\n if (states.length === 0) continue;\n const primary = this.pickEarliestCursorState(states);\n work.push(this.resubscribeOne(primary, set));\n }\n try {\n await Promise.all(work);\n } catch {\n // Defensive — each resubscribeOne has its own try/catch,\n // but add a belt-and-suspenders here for the void call site.\n }\n }\n\n private async resubscribeOne(\n primary: StreamSubscriptionState,\n set: Set<StreamSubscriptionState>,\n ): Promise<void> {\n try {\n const ack = await this.emitSubscribeEvents(primary);\n if (ack.error) {\n // Match the SpacelrError shape used by `_subscribeInternal` so\n // callers that discriminate by `.code` see the ack-error code on\n // reconnect rejections too (e.g. `not-stream-collection`).\n const err = new SpacelrError(ack.message ?? ack.error, ack.error);\n for (const state of [...set]) {\n state.options.onError?.(err);\n }\n if (PERMANENT_STREAM_ACK_ERRORS.has(ack.error)) {\n // Permanent rejection — don't retry on the next reconnect.\n this.streamSubscriptions.delete(primary.streamKey);\n }\n return;\n }\n\n // Happy path — ack succeeded. If the local set was fully torn down during\n // the await (last unsubscribe raced with the reconnect), release the freshly\n // acquired server-side reader slot so we don't leak it.\n if (!this.streamSubscriptions.has(primary.streamKey)) {\n this.socket?.emit('unsubscribe-events', {\n projectId: primary.options.projectId,\n collectionName: primary.options.collectionName,\n });\n }\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n for (const state of [...set]) {\n state.options.onError?.(error);\n }\n // Thrown path is exceptional (emitSubscribeEvents always resolves), treat\n // as transient and let the next reconnect retry.\n }\n }\n\n private pickEarliestCursorState(\n states: StreamSubscriptionState[],\n ): StreamSubscriptionState {\n // Stream entry IDs look like \"<ms>-<seq>\"; compare numerically so lexical\n // ordering doesn't produce wrong results once the ms digit count changes.\n const parse = (id: string | null): [number, number] => {\n // null = \"no cursor preference\" — never overrides a real cursor.\n if (!id) return [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];\n const [ms, seq = '0'] = id.split('-');\n return [Number(ms), Number(seq)];\n };\n let earliest = states[0];\n let [eMs, eSeq] = parse(earliest.lastDeliveredId);\n for (let i = 1; i < states.length; i++) {\n const [ms, seq] = parse(states[i].lastDeliveredId);\n if (ms < eMs || (ms === eMs && seq < eSeq)) {\n earliest = states[i];\n eMs = ms;\n eSeq = seq;\n }\n }\n return earliest;\n }\n\n /**\n * Evict the primary subscriber AND every sibling that joined the same\n * streamKey via the dedup path. Fires `onError` on all siblings, then\n * removes the entire streamKey entry from `streamSubscriptions`.\n */\n private evictStreamKey(\n streamKey: string,\n err: Error,\n primary: StreamSubscriptionState,\n ): void {\n const set = this.streamSubscriptions.get(streamKey);\n if (!set) return;\n for (const sibling of [...set]) {\n if (sibling !== primary) {\n sibling.options.onError?.(err);\n }\n set.delete(sibling);\n }\n this.streamSubscriptions.delete(streamKey);\n }\n}\n","/**\n * Persist the stream-mode resume cursor across app restarts and reconnects.\n *\n * Pass an implementation to `subscribeEvents({ cursorStorage })` and the SDK\n * will load the previous cursor before subscribing and save after each event\n * is handled. All methods may be sync or async — the SDK awaits the returned\n * value so React Native's AsyncStorage and other async backends work without\n * extra adapter code.\n */\nexport interface CursorStorage {\n /**\n * Return the saved cursor for this key, or `null` if none.\n *\n * An empty string is also treated as \"no cursor\" — Redis stream entry ids\n * are never empty, so an empty value is always the result of a misconfigured\n * adapter or a zero-length write. Returning `null` is preferred for clarity.\n */\n load(key: string): Promise<string | null> | string | null;\n /** Persist the cursor for this key. Called after each delivered event. */\n save(key: string, cursor: string): Promise<void> | void;\n}\n\n/**\n * In-memory cursor store. Useful for tests and short-lived scripts; state is\n * lost on process exit. Pass this to `subscribeEvents({ cursorStorage })`\n * when you want non-persistent in-memory resume behaviour within a single\n * session — omitting `cursorStorage` entirely disables persistence instead.\n */\nexport function memoryCursorStorage(): CursorStorage {\n const store = new Map<string, string>();\n return {\n load(key) {\n return store.get(key) ?? null;\n },\n save(key, cursor) {\n store.set(key, cursor);\n },\n };\n}\n\n/**\n * Browser `localStorage`-backed cursor store. Gracefully no-ops in\n * non-browser environments (Node without a `localStorage` polyfill), so\n * isomorphic code can use this factory unconditionally.\n *\n * @param prefix Key prefix applied to every stored entry. Defaults to\n * `spacelr:cursor:` so multiple SDK consumers on the same origin don't\n * collide.\n */\nexport function localStorageCursorStorage(\n prefix = 'spacelr:cursor:',\n): CursorStorage {\n // Sandboxed cross-origin iframes and browsers with storage disabled throw\n // `SecurityError` on the property *access* itself — not just on method\n // calls — so the guard has to wrap the read, not just typeof-check.\n const storage = ((): Storage | null => {\n try {\n const candidate = (globalThis as { localStorage?: Storage }).localStorage;\n return typeof candidate === 'undefined' ? null : candidate;\n } catch {\n return null;\n }\n })();\n\n return {\n load(key) {\n if (!storage) return null;\n try {\n return storage.getItem(prefix + key);\n } catch {\n return null;\n }\n },\n save(key, cursor) {\n if (!storage) return;\n try {\n storage.setItem(prefix + key, cursor);\n } catch {\n // Quota exceeded or storage disabled — silently drop; cursor\n // persistence is a best-effort resume hint, not a correctness\n // guarantee.\n }\n },\n };\n}\n","import { HttpClient, TokenManager, generatePKCEChallenge } from '../core';\nimport {\n SpacelrClientConfig,\n GrantType,\n LoginParams,\n LoginResponse,\n RegisterParams,\n RegisterResponse,\n TokenResponse,\n UserInfo,\n UserProfile,\n AuthorizationUrlParams,\n ExchangeCodeParams,\n PKCEChallenge,\n OpenIDConfiguration,\n JWKSResponse,\n TwoFactorVerifyParams,\n} from '../types';\n\nexport type AuthState = 'authenticated' | 'unauthenticated';\nexport type AuthStateListener = (state: AuthState) => void | Promise<void>;\n\nexport class AuthModule {\n private http: HttpClient;\n private tokenManager: TokenManager;\n private config: SpacelrClientConfig;\n private stateListeners = new Set<AuthStateListener>();\n private unsubscribeAuthLost!: () => void;\n private lastEmittedState: AuthState | null = null;\n\n constructor(\n http: HttpClient,\n tokenManager: TokenManager,\n config: SpacelrClientConfig\n ) {\n this.http = http;\n this.tokenManager = tokenManager;\n this.config = config;\n\n // Wire up refresh callback to avoid circular deps\n this.tokenManager.setRefreshCallback(async (refreshToken: string) => {\n const result = await this.refresh(refreshToken);\n const expiresAt = result.expires_in\n ? Math.floor(Date.now() / 1000) + result.expires_in\n : undefined;\n return {\n accessToken: result.access_token,\n refreshToken: result.refresh_token,\n expiresAt,\n };\n });\n\n // A failed refresh (triggered by 401 / refresh endpoint error) drops the\n // session — mirror that into our auth-state subscribers so UIs can react.\n // Keep the unsubscribe so `dispose()` can detach if this AuthModule is\n // ever discarded while the TokenManager lives on (shared client, HMR).\n this.unsubscribeAuthLost = this.tokenManager.onAuthLost(() =>\n this.emitState('unauthenticated'),\n );\n }\n\n /**\n * Returns true if a non-expired access token is currently in storage.\n * Does NOT make a network request — safe for route guards and other\n * hot paths that run on every navigation.\n *\n * A token within the refresh buffer (about to expire) still counts as\n * authenticated because the next protected request will auto-refresh it.\n *\n * Any error from the underlying TokenStorage (corrupt JSON, quota, etc.)\n * is treated as \"not authenticated\" rather than propagated, so route\n * guards can't be crashed by a misbehaving storage backend.\n */\n async isAuthenticated(): Promise<boolean> {\n try {\n const tokens = await this.tokenManager.getStoredTokens();\n if (!tokens?.accessToken) return false;\n if (tokens.expiresAt && tokens.expiresAt * 1000 <= Date.now()) return false;\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Subscribe to auth-state transitions. The callback fires:\n * - 'authenticated' after a successful login/register/exchange/2FA-verify\n * - 'unauthenticated' after logout or when a token refresh fails\n *\n * Only fires for transitions that happen after the subscription. If the\n * user is already logged in at subscribe time (e.g. tokens restored from\n * storage on app boot), no 'authenticated' event is emitted — call\n * `isAuthenticated()` once up-front for the initial state.\n *\n * Silent token refreshes do NOT produce an event (auth state is\n * unchanged). Subscribe to `spacelrClient.onTokenRefreshed(...)` if you\n * need to observe successful refreshes.\n *\n * Listener may return `void` or `Promise<void>`. Rejections are swallowed\n * so one broken subscriber can't poison others or the auth flow. The\n * dispatch is fire-and-forget: `logout()` / `login()` resolve as soon as\n * the dispatch loop returns, without awaiting async listeners.\n *\n * Returns an unsubscribe function.\n */\n onAuthStateChange(listener: AuthStateListener): () => void {\n this.stateListeners.add(listener);\n return () => {\n this.stateListeners.delete(listener);\n };\n }\n\n /**\n * Detach this AuthModule from the TokenManager. Call when discarding the\n * client (tests, HMR, multi-client setups) to avoid leaking the internal\n * onAuthLost subscription. Idempotent — safe to call more than once.\n */\n dispose(): void {\n this.unsubscribeAuthLost();\n this.unsubscribeAuthLost = () => {\n // Second and later dispose() calls no-op.\n };\n this.stateListeners.clear();\n // Defensive reset: if this instance is ever reused (HMR, test reuse),\n // the dedup guard shouldn't swallow the first post-dispose emission\n // just because it happens to match the state we last emitted.\n this.lastEmittedState = null;\n }\n\n private emitState(state: AuthState): void {\n // Dedupe consecutive identical emits: a failed logout call can both\n // trigger onAuthLost (via 401 in the request) and then fall through to\n // our explicit emit — without dedup, subscribers would see two\n // unauthenticated events for a single user action.\n if (state === this.lastEmittedState) return;\n this.lastEmittedState = state;\n\n for (const listener of this.stateListeners) {\n try {\n const result = listener(state);\n // Duck-type via `.then` (spec definition of a thenable) so any\n // Promise-like return, including non-native thenables from JS\n // callers that bypass the TS types, is handled uniformly.\n if (result && typeof (result as Promise<void>).then === 'function') {\n (result as Promise<void>).then(undefined, () => {\n // Async listener rejections must not break siblings or callers.\n });\n }\n } catch {\n // Synchronous listener throws must not break siblings or callers.\n }\n }\n }\n\n async login(params: LoginParams): Promise<LoginResponse> {\n const response = await this.http.request<LoginResponse>({\n method: 'POST',\n path: '/auth/login',\n body: params,\n });\n\n await this.storeTokensFromLogin(response);\n return response;\n }\n\n async register(params: RegisterParams): Promise<RegisterResponse> {\n const response = await this.http.request<RegisterResponse>({\n method: 'POST',\n path: '/auth/register',\n body: params,\n });\n\n // Only store tokens if they were returned (not when email verification is required)\n if (response.access_token) {\n await this.storeTokensFromRegister(response);\n }\n return response;\n }\n\n async refresh(refreshToken: string): Promise<TokenResponse> {\n return this.http.request<TokenResponse>({\n method: 'POST',\n path: '/auth/refresh',\n body: { refreshToken },\n });\n }\n\n async getProfile(): Promise<UserProfile> {\n return this.http.request<UserProfile>({\n method: 'GET',\n path: '/auth/me',\n authenticated: true,\n });\n }\n\n async logout(): Promise<void> {\n try {\n await this.http.request<void>({\n method: 'POST',\n path: '/auth/logout',\n authenticated: true,\n });\n } catch {\n // Server-side revocation is best-effort. If the token is already invalid\n // the server has effectively revoked the session anyway — always clear\n // the local state so the UI doesn't get stuck.\n }\n await this.tokenManager.clearTokens();\n this.emitState('unauthenticated');\n }\n\n async verifyEmail(token: string): Promise<{ message: string }> {\n return this.http.request<{ message: string }>({\n method: 'GET',\n path: '/auth/verify-email',\n query: { token },\n });\n }\n\n async resendVerification(email: string): Promise<{ message: string }> {\n return this.http.request<{ message: string }>({\n method: 'POST',\n path: '/auth/resend-verification',\n body: { email },\n });\n }\n\n async getUserInfo(): Promise<UserInfo> {\n return this.http.request<UserInfo>({\n method: 'GET',\n path: this.config.userInfoEndpoint ?? '/auth/userinfo',\n authenticated: true,\n });\n }\n\n getAuthorizationUrl(params: AuthorizationUrlParams): string {\n const baseUrl = this.config.apiUrl.replace(/\\/+$/, '');\n const endpoint =\n this.config.authorizationEndpoint ?? '/auth/authorize';\n const url = new URL(`${baseUrl}${endpoint}`);\n\n url.searchParams.set('client_id', this.config.clientId);\n url.searchParams.set('redirect_uri', params.redirectUri);\n url.searchParams.set('response_type', params.responseType ?? 'code');\n\n const scope =\n params.scope ?? this.config.scopes?.join(' ') ?? 'openid';\n url.searchParams.set('scope', scope);\n\n if (params.state) {\n url.searchParams.set('state', params.state);\n }\n if (params.codeChallenge) {\n url.searchParams.set('code_challenge', params.codeChallenge);\n }\n if (params.codeChallengeMethod) {\n url.searchParams.set(\n 'code_challenge_method',\n params.codeChallengeMethod\n );\n }\n\n return url.toString();\n }\n\n async exchangeCode(params: ExchangeCodeParams): Promise<TokenResponse> {\n const body: Record<string, string> = {\n grant_type: params.grantType ?? GrantType.AUTHORIZATION_CODE,\n code: params.code,\n redirect_uri: params.redirectUri,\n client_id: this.config.clientId,\n };\n\n if (params.clientSecret) {\n body['client_secret'] = params.clientSecret;\n }\n if (params.codeVerifier) {\n body['code_verifier'] = params.codeVerifier;\n }\n\n const tokenEndpoint =\n this.config.tokenEndpoint ?? '/auth/token';\n\n const response = await this.http.request<TokenResponse>({\n method: 'POST',\n path: tokenEndpoint,\n body,\n });\n\n const expiresAt = response.expires_in\n ? Math.floor(Date.now() / 1000) + response.expires_in\n : undefined;\n\n await this.tokenManager.setTokens({\n accessToken: response.access_token,\n refreshToken: response.refresh_token,\n expiresAt,\n });\n this.emitState('authenticated');\n\n return response;\n }\n\n async generatePKCE(): Promise<PKCEChallenge> {\n return generatePKCEChallenge();\n }\n\n async getOpenIDConfiguration(): Promise<OpenIDConfiguration> {\n return this.http.request<OpenIDConfiguration>({\n method: 'GET',\n path: '/.well-known/openid-configuration',\n });\n }\n\n async getJWKS(): Promise<JWKSResponse> {\n return this.http.request<JWKSResponse>({\n method: 'GET',\n path: '/.well-known/jwks.json',\n });\n }\n\n /**\n * Request a password reset email.\n * Always returns a generic message regardless of whether the email exists.\n */\n async requestPasswordReset(email: string): Promise<{ message: string }> {\n return this.http.request<{ message: string }>({\n method: 'POST',\n path: '/auth/request-password-reset',\n body: { email },\n });\n }\n\n /**\n * Reset password using a token received via email.\n */\n async resetPassword(\n token: string,\n password: string,\n ): Promise<{ message: string }> {\n return this.http.request<{ message: string }>({\n method: 'POST',\n path: '/auth/reset-password',\n body: { token, password },\n });\n }\n\n /**\n * Exchange a one-time verification code for tokens.\n * Use this after email verification redirects the user with a ?loginCode= parameter.\n */\n async exchangeVerificationCode(code: string): Promise<LoginResponse> {\n const response = await this.http.request<LoginResponse>({\n method: 'POST',\n path: '/auth/exchange-code',\n body: { code },\n });\n\n await this.storeTokensFromLogin(response);\n return response;\n }\n\n /**\n * Resend a two-factor authentication code email.\n * Call this when the user hasn't received the code or it expired.\n */\n async resendTwoFactorCode(token: string): Promise<{ message: string }> {\n return this.http.request<{ message: string }>({\n method: 'POST',\n path: '/auth/resend-two-factor-code',\n body: { token },\n });\n }\n\n /**\n * Verify a two-factor authentication code.\n * Call this after catching SpacelrTwoFactorRequiredError from login().\n */\n async verifyTwoFactor(params: TwoFactorVerifyParams): Promise<LoginResponse> {\n const response = await this.http.request<LoginResponse>({\n method: 'POST',\n path: '/auth/verify-two-factor',\n body: { token: params.token, code: params.code },\n });\n\n await this.storeTokensFromLogin(response);\n return response;\n }\n\n private async storeTokensFromLogin(response: LoginResponse): Promise<void> {\n const expiresAt = response.expires_in\n ? Math.floor(Date.now() / 1000) + response.expires_in\n : undefined;\n\n await this.tokenManager.setTokens({\n accessToken: response.access_token,\n refreshToken: response.refresh_token,\n expiresAt,\n });\n this.emitState('authenticated');\n }\n\n private async storeTokensFromRegister(\n response: RegisterResponse\n ): Promise<void> {\n // Guard mirrors the optional `access_token?: string` in RegisterResponse:\n // the server omits tokens when email verification is required. The\n // caller already pre-checks, but keeping the guard here means the helper\n // stays correct against its declared input type rather than relying on\n // the single current call site.\n if (!response.access_token) return;\n await this.tokenManager.setTokens({\n accessToken: response.access_token,\n refreshToken: response.refresh_token,\n });\n this.emitState('authenticated');\n }\n}\n","import { HttpClient, TokenManager } from '../core';\nimport {\n SpacelrClientConfig,\n FileVisibility,\n FileInfo,\n FileListResponse,\n ListFilesParams,\n InitMultipartUploadParams,\n InitMultipartUploadResponse,\n PartEtag,\n ShareFileParams,\n UnshareFileParams,\n QuotaInfo,\n UploadFileParams,\n UploadLargeFileParams,\n UploadProgress,\n DownloadUrlResponse,\n} from '../types';\n\nexport class StorageModule {\n private http: HttpClient;\n private tokenManager: TokenManager;\n private config: SpacelrClientConfig;\n\n constructor(\n http: HttpClient,\n tokenManager: TokenManager,\n config: SpacelrClientConfig\n ) {\n this.http = http;\n this.tokenManager = tokenManager;\n this.config = config;\n }\n\n /**\n * Upload a file through the gateway (no direct S3 access).\n * Accepts a Blob/File (browser) or ArrayBuffer/Uint8Array (Node).\n */\n async uploadFile(\n file: Blob | ArrayBuffer | Uint8Array,\n params: UploadFileParams,\n onProgress?: (progress: UploadProgress) => void,\n ): Promise<FileInfo> {\n const formData = new FormData();\n const blob =\n file instanceof Blob\n ? file\n : new Blob([file as BlobPart], { type: params.mimeType });\n formData.append('file', blob, params.filename);\n if (params.visibility) formData.append('visibility', params.visibility);\n if (params.description) formData.append('description', params.description);\n\n const progressHandler = onProgress\n ? (e: { loaded: number; total: number }) => {\n onProgress({\n loaded: e.loaded,\n total: e.total,\n percentage: e.total > 0 ? Math.round((e.loaded / e.total) * 100) : 0,\n });\n }\n : undefined;\n\n return this.http.uploadForm<FileInfo>('/storage/files', formData, progressHandler);\n }\n\n /**\n * Upload a large file using multipart upload through the gateway.\n * Splits into parts, uploads concurrently, and completes.\n */\n async uploadLargeFile(\n file: Blob | ArrayBuffer | Uint8Array,\n params: UploadLargeFileParams,\n onProgress?: (progress: UploadProgress) => void\n ): Promise<FileInfo> {\n const fileSize = file instanceof Blob ? file.size : file.byteLength;\n const concurrency = params.concurrency ?? 3;\n\n const init = await this.initMultipartUpload({\n filename: params.filename,\n mimeType: params.mimeType,\n totalSizeBytes: fileSize,\n visibility: params.visibility,\n description: params.description,\n });\n\n const { partSize, totalParts, fileId } = init;\n const completedParts: PartEtag[] = [];\n let completedBytes = 0;\n const partProgressMap = new Map<number, number>();\n\n try {\n const allPartNumbers = Array.from(\n { length: totalParts },\n (_, i) => i + 1\n );\n\n for (let i = 0; i < allPartNumbers.length; i += concurrency) {\n const batch = allPartNumbers.slice(i, i + concurrency);\n\n const uploads = batch.map(async (partNumber) => {\n const start = (partNumber - 1) * partSize;\n const end = Math.min(start + partSize, fileSize);\n const chunkSize = end - start;\n const chunk =\n file instanceof Blob\n ? file.slice(start, end)\n : new Blob([file.slice(start, end)]);\n\n const formData = new FormData();\n formData.append('file', chunk, `part-${partNumber}`);\n formData.append('partNumber', String(partNumber));\n\n const partProgress = onProgress && typeof XMLHttpRequest !== 'undefined'\n ? (e: { loaded: number; total: number }) => {\n const ratio = e.total > 0 ? e.loaded / e.total : 0;\n partProgressMap.set(partNumber, Math.min(ratio * chunkSize, chunkSize));\n const inFlightBytes = Array.from(partProgressMap.values())\n .reduce((sum, v) => sum + v, 0);\n const totalLoaded = Math.min(completedBytes + inFlightBytes, fileSize);\n onProgress({\n loaded: totalLoaded,\n total: fileSize,\n percentage: fileSize > 0\n ? Math.min(100, Math.round((totalLoaded / fileSize) * 100))\n : 0,\n });\n }\n : undefined;\n\n const result = await this.http.uploadForm<{ etag: string }>(\n `/storage/files/${fileId}/multipart/upload-part`,\n formData,\n partProgress,\n );\n\n partProgressMap.delete(partNumber);\n completedBytes += chunkSize;\n\n if (onProgress) {\n onProgress({\n loaded: Math.min(completedBytes, fileSize),\n total: fileSize,\n percentage: fileSize > 0\n ? Math.min(100, Math.round((completedBytes / fileSize) * 100))\n : 0,\n });\n }\n\n completedParts.push({\n partNumber,\n etag: result.etag,\n });\n });\n\n await Promise.all(uploads);\n partProgressMap.clear();\n }\n\n // Sort parts by number before completing\n completedParts.sort((a, b) => a.partNumber - b.partNumber);\n\n await this.completeMultipartUpload(fileId, completedParts);\n return this.getFileInfo(fileId);\n } catch (error) {\n // Clean up the incomplete multipart upload on failure\n try {\n await this.abortMultipartUpload(fileId);\n } catch {\n // Abort is best-effort; ignore cleanup failures\n }\n throw error;\n }\n }\n\n async initMultipartUpload(\n params: InitMultipartUploadParams\n ): Promise<InitMultipartUploadResponse> {\n return this.http.request<InitMultipartUploadResponse>({\n method: 'POST',\n path: '/storage/files/multipart/init',\n body: params,\n authenticated: true,\n });\n }\n\n async completeMultipartUpload(\n fileId: string,\n parts: PartEtag[]\n ): Promise<FileInfo> {\n return this.http.request<FileInfo>({\n method: 'POST',\n path: `/storage/files/${fileId}/multipart/complete`,\n body: { parts },\n authenticated: true,\n });\n }\n\n async abortMultipartUpload(fileId: string): Promise<void> {\n await this.http.request<void>({\n method: 'POST',\n path: `/storage/files/${fileId}/multipart/abort`,\n authenticated: true,\n });\n }\n\n async listFiles(params?: ListFilesParams): Promise<FileListResponse> {\n return this.http.request<FileListResponse>({\n method: 'GET',\n path: '/storage/files',\n query: params as Record<string, string | number | undefined>,\n authenticated: true,\n });\n }\n\n async listSharedFiles(params?: ListFilesParams): Promise<FileListResponse> {\n return this.http.request<FileListResponse>({\n method: 'GET',\n path: '/storage/shared',\n query: params as Record<string, string | number | undefined>,\n authenticated: true,\n });\n }\n\n async getFileInfo(fileId: string): Promise<FileInfo> {\n return this.http.request<FileInfo>({\n method: 'GET',\n path: `/storage/files/${fileId}`,\n authenticated: true,\n });\n }\n\n async downloadFile(fileId: string): Promise<Blob> {\n const baseUrl = this.config.apiUrl.replace(/\\/+$/, '');\n const url = `${baseUrl}/storage/files/${fileId}/download`;\n\n const headers: Record<string, string> = {\n 'x-client-id': this.config.clientId,\n 'x-project-id': this.config.projectId,\n };\n\n const token = await this.tokenManager.getAccessToken();\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n\n const response = await fetch(url, { headers });\n\n if (!response.ok) {\n throw new Error(`Download failed: ${response.status}`);\n }\n\n return response.blob();\n }\n\n async getDownloadUrl(fileId: string): Promise<DownloadUrlResponse> {\n return this.http.request<DownloadUrlResponse>({\n method: 'GET',\n path: `/storage/files/${fileId}/download-url`,\n authenticated: true,\n });\n }\n\n async deleteFile(fileId: string): Promise<void> {\n await this.http.request<void>({\n method: 'DELETE',\n path: `/storage/files/${fileId}`,\n authenticated: true,\n });\n }\n\n async shareFile(\n fileId: string,\n params: ShareFileParams\n ): Promise<void> {\n await this.http.request<void>({\n method: 'POST',\n path: `/storage/files/${fileId}/share`,\n body: params,\n authenticated: true,\n });\n }\n\n async unshareFile(\n fileId: string,\n params: UnshareFileParams\n ): Promise<void> {\n await this.http.request<void>({\n method: 'POST',\n path: `/storage/files/${fileId}/unshare`,\n body: params,\n authenticated: true,\n });\n }\n\n async updateVisibility(\n fileId: string,\n visibility: FileVisibility\n ): Promise<FileInfo> {\n return this.http.request<FileInfo>({\n method: 'PATCH',\n path: `/storage/files/${fileId}/visibility`,\n body: { visibility },\n authenticated: true,\n });\n }\n\n async getQuota(): Promise<QuotaInfo> {\n return this.http.request<QuotaInfo>({\n method: 'GET',\n path: '/storage/quota',\n authenticated: true,\n });\n }\n\n async getPublicFileUrl(fileId: string, projectId?: string): Promise<string> {\n const baseUrl = this.config.apiUrl.replace(/\\/+$/, '');\n const resolvedProjectId = projectId ?? this.config.projectId;\n return `${baseUrl}/public/files/${resolvedProjectId}/${fileId}`;\n }\n}\n","import { HttpClient } from '../core';\nimport { RealtimeClient, DatabaseChangeEvent, SpacelrError } from '../core';\nimport type { GapReason, StreamGapInfo } from '../core/realtime';\nimport type { CursorStorage } from '../core/cursor-storage';\n\n// Re-export for downstream consumers so they don't need to reach into core.\nexport type { GapReason, StreamGapInfo, CursorStorage };\n\nexport interface PopulateOption {\n field: string;\n collection: string;\n foreignField?: string;\n}\n\nexport interface DocumentResult {\n _id: string;\n [key: string]: unknown;\n}\n\nexport interface OffsetResult<T = Record<string, unknown>> {\n mode: 'offset';\n documents: (T & { _id: string })[];\n total: number;\n limit: number;\n offset: number;\n}\n\nexport interface CursorResult<T = Record<string, unknown>> {\n mode: 'cursor';\n documents: (T & { _id: string })[];\n limit: number;\n /**\n * `_id` of the last document in `documents`, or `null` if `documents` is empty.\n * Independent of `hasMore` — the cursor reflects the page contents, not\n * whether more pages exist.\n */\n nextCursor: string | null;\n /**\n * True when more documents exist past the returned page. Computed via the\n * `limit + 1` server-side fetch trick.\n */\n hasMore: boolean;\n}\n\nexport type FindResult<T = Record<string, unknown>> = OffsetResult<T> | CursorResult<T>;\n\n// SearchResult retains the pre-discriminator shape; search() does not yet\n// support cursor pagination.\nexport interface SearchResult<T = Record<string, unknown>> {\n documents: (T & { _id: string })[];\n total: number;\n limit: number;\n offset: number;\n}\n\nexport interface InsertResult {\n insertedCount: number;\n insertedIds: string[];\n}\n\nexport interface UpdateResult {\n matchedCount: number;\n modifiedCount: number;\n}\n\nexport interface DeleteResult {\n deletedCount: number;\n}\n\nexport interface CountResult {\n count: number;\n}\n\nexport interface FindByIdOptions {\n populate?: PopulateOption[];\n}\n\n/**\n * Options for a server-side substring search.\n *\n * Limits: `query` 1–200 chars, `fields` 1–10 entries (each matching\n * `/^[a-zA-Z0-9_.]+$/`, max 64 chars), `limit` max 100.\n */\nexport interface SearchOptions {\n query: string;\n fields: string[];\n filter?: Record<string, unknown>;\n sort?: Record<string, 1 | -1>;\n limit?: number;\n offset?: number;\n select?: string[];\n}\n\n/**\n * Options for `CollectionRef.subscribeWithSnapshot()` — see #99.\n *\n * The helper combines an initial query, a live stream subscription,\n * and outside-retention-window gap recovery into one idempotent\n * contract. Apps must implement `onChange` as state mutation keyed\n * by `_id` (Map/Set semantics): events MAY arrive twice (snapshot\n * vs stream overlap, gap recovery replay window, reconnect replay\n * from persisted cursor).\n *\n * Performance: `onChange` is awaited; a slow handler stalls the\n * stream for THIS subscriber. Keep it fast (synchronous state\n * updates only).\n *\n * Requires a stream-mode collection. Calling on a pubsub collection\n * throws SpacelrError with code SUBSCRIBE_WITH_SNAPSHOT_REQUIRES_STREAM\n * BEFORE any find() runs.\n */\nexport interface SubscribeWithSnapshotOptions<T> {\n /**\n * Filter for both the initial snapshot AND the live `onChange` stream.\n *\n * **Snapshot semantics** (full MongoDB query): supports operators like\n * `{ status: { $in: [...] } }`, `{ ts: { $gt: ... } }`, etc.\n *\n * **Live-stream semantics** (gateway-side equality only): the stream\n * filter only honours top-level primitive equality\n * (`string | number | boolean` values). Operator-shaped values are\n * silently dropped from the stream filter at handshake time — the\n * remaining primitive entries still apply, so a typical chat-style\n * `{ chatId: 'c1' }` filter works identically on both sides. If you\n * pass a mixed `{ chatId: 'c1', status: { $in: [...] } }`, the\n * snapshot is fully filtered but the live stream filters by `chatId`\n * only; you may receive `onChange` events for documents whose `status`\n * is outside your `$in` set. Re-check in the handler if needed.\n */\n where?: Record<string, unknown>;\n sort?: Record<string, 1 | -1>;\n limit?: number;\n select?: string[];\n\n /**\n * Called with the full snapshot:\n * - once after the initial find()\n * - again after every gap-exceeded recovery\n * `cursor` is the stream position at the moment the snapshot was\n * taken — opaque, may be persisted, may be passed to a later\n * subscribeWithCursor as sinceId. Never compare cursor strings.\n *\n * Note on shared readers: when another subscriber on the SAME\n * client+collection is already active, this subscriber piggy-backs\n * on that reader. The returned `cursor` reflects the stream tip at\n * THIS handshake; the shared reader may already have advanced past\n * it, so the duplicate-delivery window between this `cursor` and\n * the next `onChange` event can be wider than a fresh single-\n * subscriber attach. The at-least-once contract still holds —\n * idempotent `onChange` handlers absorb the extra duplicates.\n */\n onSnapshot: (\n docs: (T & { _id: string })[],\n cursor: string | null,\n ) => void | Promise<void>;\n\n /**\n * Per-event callback. Awaited; cursor advances after this resolves.\n * State mutations MUST be idempotent by `_id`.\n */\n onChange: (event: DatabaseChangeEvent) => void | Promise<void>;\n\n /**\n * Fatal stream/subscription errors AFTER the initial handshake has\n * resolved. Subscription is torn down. Pre-handshake failures (e.g.\n * pubsub-mode collection, ack-error from the gateway) DO NOT fire\n * this callback — they reject the helper's promise instead, with a\n * typed `SpacelrError` carrying a discriminator code.\n *\n * If this callback itself throws, the gap-recovery state machine\n * still resets to `'idle'`, but `onError` may be invoked a second\n * time (with the same error) by the dispatch IIFE's safety-net\n * catch. Keep the callback total: log, set state, return.\n */\n onError?: (err: Error) => void;\n\n /**\n * Snapshot read (`find()`) failed. Default: forwards to `onError`.\n * Aborted reads (caused by `unsubscribe()`) NEVER call this — they\n * silently reject the helper's promise with `AbortError`.\n *\n * **Dual-notification on non-abort failures**: a snapshot failure\n * fires this callback AND rejects the helper's outer promise with\n * the same error. Callers that handle both `await ...catch()` AND\n * register `onSnapshotError` will receive the error twice — pick\n * one path. The dual surface is intentional so apps using fire-and-\n * forget callback handlers don't need to also wrap the await.\n */\n onSnapshotError?: (err: Error) => void;\n\n // Note: gap-recovery only fires for `event-gap` frames whose\n // `reason === 'outside-retention-window'`. Other GapReason values\n // (`'redis-unavailable'`, `'replay-error'`, `'replay-truncated'`)\n // are transient or replay-time conditions that the realtime layer\n // either retries on reconnect or surfaces via the underlying\n // subscribe's `onError`. The helper does NOT re-snapshot for them,\n // so a long-running `'replay-truncated'` could mean some events\n // were skipped during initial replay — observable only as a\n // `onError` invocation, not as a fresh snapshot.\n //\n // Note: if a recovery's `subscribe-events` handshake itself fails\n // (transient network blip, expired token), the helper surfaces the\n // failure via `onError` and returns the state machine to `idle`.\n // It does NOT auto-retry — the next `event-gap` will trigger a\n // fresh recovery, but if no further gap arrives the subscription is\n // effectively abandoned. Pair with reconnect-aware token refresh\n // upstream of the SDK if you need stronger recovery guarantees.\n //\n // Note: a SECOND simultaneous `subscribeWithSnapshot()` call for\n // the same collection on the same client takes the realtime layer's\n // dedup path and synthesises `latestStreamId: null` — the second\n // caller's `onSnapshot` will receive `cursor: null` (no resume\n // anchor available) even if the stream is non-empty. The first\n // subscriber gets the real anchor. Avoid simultaneous calls to the\n // same collection.\n}\n\nexport interface SubscribeEventsHandlers<T = Record<string, unknown>> {\n /** Cursor to resume from. Undefined = fresh subscription, deliver only new events. */\n sinceId?: string;\n where?: Record<string, string | number | boolean>;\n /**\n * Optional resume-cursor store. When set, the SDK loads the previous cursor\n * before subscribing and persists the new cursor after each delivered\n * event. An explicit `sinceId` takes precedence over a loaded cursor —\n * `load()` is skipped entirely and saves resume from `sinceId` forward,\n * overwriting any previously persisted cursor.\n *\n * Pair `where`-filtered subscriptions with a unique `cursorKey`; subscribers\n * with different filters must not share a cursor since the stream position\n * represents \"events seen\" — filtered-out entries still advance it.\n */\n cursorStorage?: CursorStorage;\n /**\n * Override the default cursor key. Defaults to `{projectId}:{collectionName}`.\n * Useful when multiple subscriptions on the same collection (e.g. different\n * `where` filters or per-user feeds) must persist independent cursors.\n */\n cursorKey?: string;\n /** Called on inserts. Receives the document merged with `_eventId`. */\n onInsert?: (doc: T & { _id: string; _eventId: string }) => void | Promise<void>;\n onUpdate?: (doc: T & { _id: string; _eventId: string }) => void | Promise<void>;\n /** Called on deletes. `document` may be absent on tombstones — only documentId + eventId are guaranteed. */\n onDelete?: (documentId: string, eventId: string) => void | Promise<void>;\n /** Called when the server emits an event-gap. See GapReason for the four codes. */\n onGap?: (info: StreamGapInfo) => void;\n /** Called on handshake rejection (not-stream-collection, invalid sinceId, etc.) and handler failures. */\n onError?: (error: Error) => void;\n}\n\nexport interface StreamSubscription {\n /**\n * Stop receiving events and release the server-side reader slot. Idempotent.\n *\n * When `cursorStorage` is set, an in-flight async `save()` may still deliver\n * `onError` asynchronously after `unsubscribe()` returns — the fire-and-forget\n * save chain is not cancelled. Callers whose `onError` is tied to a component\n * lifecycle should guard for post-unmount delivery.\n */\n unsubscribe(): void;\n /**\n * Most recently processed eventId — advances after the subscription's\n * onEvent pipeline completes for each event, whether or not a user\n * handler was registered for that event type. `undefined` before any\n * event arrives.\n *\n * Persist this across app restarts and pass it back as `sinceId` on\n * the next `subscribeEvents()` call to catch up on missed events.\n */\n getCursor(): string | undefined;\n}\n\nexport interface SubscribeHandlers<T = Record<string, unknown>> {\n where?: Record<string, string | number | boolean>;\n onInsert?: (doc: T & { _id: string }) => void;\n onUpdate?: (doc: T & { _id: string }) => void;\n onDelete?: (documentId: string) => void;\n onError?: (error: Error) => void;\n}\n\nexport interface PaginateOptions {\n where?: Record<string, unknown>;\n /** Sort direction by `_id`. Defaults to -1 (newest first — chat scroll-back). */\n sort?: { _id: 1 | -1 };\n /** Page size. Default 50. Server typically caps at 100. */\n limit?: number;\n /**\n * Optional initial cursor — paginate from this `_id` exclusively. Useful\n * when the caller already holds a boundary document (e.g. the oldest\n * message currently rendered in a chat view paired with `subscribeEvents`).\n */\n cursor?: string;\n}\n\nexport interface PaginatorPage<T = Record<string, unknown>> {\n documents: (T & { _id: string })[];\n /**\n * True when more documents exist past this page. The paginator also flips\n * `exhausted` to true when this returns false, so callers can poll either\n * `page.hasMore` after each call or `paginator.exhausted` between calls.\n */\n hasMore: boolean;\n}\n\n/**\n * Cursor-based scroll helper around `find()`. Tracks the last-seen `_id`\n * across calls and applies it as `.before()` (descending sort) or\n * `.after()` (ascending sort) on the next page so the keyset stays stable\n * under concurrent inserts. Use `subscribeEvents` for new docs and\n * `paginate` for older — the two compose into a chat-style timeline.\n */\nexport class Paginator<T = Record<string, unknown>> {\n private cursor?: string;\n private _exhausted = false;\n private readonly direction: 1 | -1;\n private readonly pageSize: number;\n private readonly where?: Record<string, unknown>;\n /**\n * Tail of the serialized `next()` chain. Each call appends to this so\n * concurrent invocations from a UI scroll handler (e.g. user spam-tapping\n * \"load more\") don't issue parallel requests with the same cursor — which\n * would return duplicate pages and clobber the cursor based on whichever\n * response settles last. Calls execute strictly in invocation order.\n */\n private chain: Promise<PaginatorPage<T>> = Promise.resolve({\n documents: [],\n hasMore: false,\n });\n\n constructor(\n private readonly http: HttpClient,\n private readonly basePath: string,\n opts: PaginateOptions,\n ) {\n this.cursor = opts.cursor;\n this.direction = opts.sort?._id ?? -1;\n this.pageSize = opts.limit ?? 50;\n this.where = opts.where;\n }\n\n /**\n * True once a `next()` call has returned an empty page. Subsequent\n * `next()` calls return an empty page without hitting the server.\n *\n * Note: only the empty-page signal flips this flag; a server response\n * that returns documents but `hasMore: false` does NOT exhaust. This\n * lets ascending paginators (`sort: { _id: 1 }`) keep polling for\n * documents inserted after the caller caught up to the current tail —\n * the next `.after(lastSeen)` call will simply return zero documents\n * until something new lands.\n */\n get exhausted(): boolean {\n return this._exhausted;\n }\n\n next(): Promise<PaginatorPage<T>> {\n // Serialize: each call resolves only after the previous one settles.\n // We swallow rejections from the predecessor so a single failed page\n // doesn't poison the whole chain — the caller of the failed call\n // already saw the rejection.\n const run = this.chain.then(\n () => this.fetchNextPage(),\n () => this.fetchNextPage(),\n );\n this.chain = run;\n return run;\n }\n\n private async fetchNextPage(): Promise<PaginatorPage<T>> {\n if (this._exhausted) return { documents: [], hasMore: false };\n\n const builder = new QueryBuilder<T>(this.http, this.basePath, this.where)\n .sort({ _id: this.direction })\n .limit(this.pageSize);\n\n let result: FindResult<T>;\n if (this.cursor !== undefined) {\n // Cursor mode: keyset pagination, immune to concurrent inserts.\n const cursorBuilder =\n this.direction === -1\n ? builder.before(this.cursor)\n : builder.after(this.cursor);\n result = await cursorBuilder.execute();\n } else {\n // First page without an initial cursor — offset mode on the server.\n // Sort + limit are honoured the same way; we capture the last `_id`\n // here so all subsequent pages can switch to cursor mode.\n result = await builder.execute();\n }\n\n if (result.documents.length === 0) {\n // Empty page is the only signal that exhausts the paginator. A\n // populated page with `hasMore: false` leaves us re-callable so\n // ascending mode can pick up future inserts past `lastSeen`.\n this._exhausted = true;\n return { documents: [], hasMore: false };\n }\n\n this.cursor = result.documents[result.documents.length - 1]._id;\n\n const hasMore =\n result.mode === 'cursor'\n ? result.hasMore\n : result.documents.length < result.total;\n return { documents: result.documents, hasMore };\n }\n}\n\nexport class QueryBuilder<\n T = Record<string, unknown>,\n M extends 'offset' | 'cursor' = 'offset',\n> {\n private _filter?: Record<string, unknown>;\n private _sort?: Record<string, 1 | -1>;\n private _limit?: number;\n private _offset?: number;\n private _before?: string;\n private _after?: string;\n private _fields?: string[];\n private _populate: PopulateOption[] = [];\n\n constructor(\n private http: HttpClient,\n private basePath: string,\n filter?: Record<string, unknown>,\n ) {\n this._filter = filter;\n }\n\n sort(sort: Record<string, 1 | -1>): this {\n this._sort = sort;\n return this;\n }\n\n limit(limit: number): this {\n this._limit = limit;\n return this;\n }\n\n offset(offset: number): this {\n this._offset = offset;\n return this;\n }\n\n /**\n * **Constraint:** the cursor value must be a 24-hex ObjectId string.\n * Collections using custom non-ObjectId `_id` strings will not work\n * correctly with cursor pagination — the server's `$lt`/`$gt` comparison\n * uses BSON type ordering, mixing string `_id`s with ObjectId comparison\n * produces undefined behaviour. Documented limitation; future work may\n * add opaque cursor tokens that abstract over `_id` types.\n *\n * Switch to cursor-pagination mode: return documents with `_id < id`\n * (in the sort-defined order). The cursor refers to the cursor *value*,\n * not visual UI direction. Requires `.sort()` to be `{ _id: 1 }` or\n * `{ _id: -1 }` (or omitted — server defaults to `{ _id: 1 }`).\n *\n * Narrows the builder's mode parameter so subsequent `.execute()` returns\n * a `CursorResult<T>` instead of `OffsetResult<T>`.\n *\n * **For paginating further:** the `nextCursor` field returned by\n * `execute()` is the `_id` of the last document on the page. To load the\n * next older page, pass it again to `.before()`. (Do NOT pass it to\n * `.after()` — that would request docs newer than this page.)\n *\n * **Cannot be combined with `.offset()`.** Type system allows the chain\n * for ergonomics, but the server rejects it with HTTP 400.\n */\n before(id: string): QueryBuilder<T, 'cursor'> {\n this._before = id;\n return this as QueryBuilder<T, 'cursor'>;\n }\n\n /**\n * Switch to cursor-pagination mode: return documents with `_id > id`.\n * See `.before()` for full semantics — including the ObjectId-only cursor\n * constraint and the `.offset()` incompatibility (server-enforced 400).\n *\n * **For paginating further:** pass the returned `nextCursor` to\n * `.after()` again to load the next newer page.\n */\n after(id: string): QueryBuilder<T, 'cursor'> {\n this._after = id;\n return this as QueryBuilder<T, 'cursor'>;\n }\n\n select(fields: string[]): this {\n this._fields = fields;\n return this;\n }\n\n populate(\n field: string,\n collection: string,\n foreignField?: string,\n ): this {\n this._populate.push({ field, collection, foreignField });\n return this;\n }\n\n /**\n * `signal` is an optional external AbortSignal forwarded into the underlying\n * HTTP request. Used by `subscribeWithSnapshot` so that an unsubscribe()\n * call cancels the in-flight snapshot find(); regular callers can leave it\n * undefined.\n */\n async execute(\n signal?: AbortSignal,\n ): Promise<M extends 'cursor' ? CursorResult<T> : OffsetResult<T>> {\n const query: Record<string, string | number | undefined> = {};\n if (this._filter) query['filter'] = JSON.stringify(this._filter);\n if (this._sort) query['sort'] = JSON.stringify(this._sort);\n if (this._limit !== undefined) query['limit'] = this._limit;\n if (this._offset !== undefined) query['offset'] = this._offset;\n if (this._before !== undefined) query['before'] = this._before;\n if (this._after !== undefined) query['after'] = this._after;\n if (this._fields) query['fields'] = this._fields.join(',');\n if (this._populate.length) {\n query['populate'] = this._populate\n .map((p) =>\n p.foreignField\n ? `${p.field}:${p.collection}:${p.foreignField}`\n : `${p.field}:${p.collection}`,\n )\n .join(',');\n }\n\n return this.http.request<M extends 'cursor' ? CursorResult<T> : OffsetResult<T>>({\n method: 'GET',\n path: this.basePath,\n query,\n authenticated: true,\n signal,\n });\n }\n}\n\nexport class CollectionRef<T = Record<string, unknown>> {\n private basePath: string;\n private collectionName: string;\n\n constructor(\n private http: HttpClient,\n private realtime: RealtimeClient | null,\n private projectId: string,\n collectionName: string,\n ) {\n this.collectionName = collectionName;\n this.basePath = `/db/${collectionName}`;\n }\n\n async insert(\n document: Partial<T> & { _id?: string },\n ): Promise<InsertResult> {\n return this.http.request<InsertResult>({\n method: 'POST',\n path: this.basePath,\n body: { documents: [document] },\n authenticated: true,\n });\n }\n\n async insertMany(\n documents: (Partial<T> & { _id?: string })[],\n ): Promise<InsertResult> {\n return this.http.request<InsertResult>({\n method: 'POST',\n path: this.basePath,\n body: { documents },\n authenticated: true,\n });\n }\n\n find(filter?: Record<string, unknown>): QueryBuilder<T> {\n return new QueryBuilder<T>(this.http, this.basePath, filter);\n }\n\n /**\n * Cursor-based scroll-back helper. Returns a `Paginator` whose `.next()`\n * yields successive pages and tracks the last-seen `_id` internally.\n * Defaults to descending sort and 50 docs per page (chat scroll-back is\n * the canonical use case). See `PaginateOptions` for tuning.\n *\n * **Cursor constraint:** uses `_id` keyset pagination, which requires\n * 24-hex ObjectId `_id`s. Collections with custom string `_id` schemes\n * fall back to comparing strings as ObjectIds — the server's handler\n * documents this limitation on `before` / `after` directly.\n */\n paginate(opts: PaginateOptions = {}): Paginator<T> {\n return new Paginator<T>(this.http, this.basePath, opts);\n }\n\n /**\n * Server-side substring search across the specified fields.\n *\n * The query is regex-escaped server-side and matched case-insensitively via\n * MongoDB `$regex`. Performance note: unanchored case-insensitive regex\n * cannot use a standard B-tree index — on very large collections consider\n * narrowing with `filter` to scope the scan.\n *\n * Required filters: collections may declare `searchConfig.requireFilter`\n * via the admin API. Calls without those keys at the top level of `filter`\n * (or inside a top-level `$and`) reject with\n * `SpacelrSearchFilterRequiredError` carrying `missingFields`. Allowed\n * shapes: `{ field: value }`, `{ field: { $eq: value } }`,\n * `{ field: { $in: [...] } }` (non-empty).\n *\n * Limits: `query` 1–200 chars, `fields` 1–10 entries (each matching\n * `/^[a-zA-Z0-9_.]+$/`, max 64 chars), `limit` max 100.\n */\n async search(opts: SearchOptions): Promise<SearchResult<T>> {\n return this.http.request<SearchResult<T>>({\n method: 'POST',\n path: `${this.basePath}/search`,\n body: opts,\n authenticated: true,\n });\n }\n\n async findById(\n id: string,\n options?: FindByIdOptions,\n ): Promise<T & { _id: string }> {\n const query: Record<string, string> = {};\n if (options?.populate?.length) {\n query['populate'] = options.populate\n .map((p) =>\n p.foreignField\n ? `${p.field}:${p.collection}:${p.foreignField}`\n : `${p.field}:${p.collection}`,\n )\n .join(',');\n }\n\n return this.http.request<T & { _id: string }>({\n method: 'GET',\n path: `${this.basePath}/${id}`,\n query,\n authenticated: true,\n });\n }\n\n async update(id: string, update: Partial<T>): Promise<UpdateResult> {\n return this.http.request<UpdateResult>({\n method: 'PATCH',\n path: `${this.basePath}/${id}`,\n body: { update },\n authenticated: true,\n });\n }\n\n async delete(id: string): Promise<DeleteResult> {\n return this.http.request<DeleteResult>({\n method: 'DELETE',\n path: `${this.basePath}/${id}`,\n authenticated: true,\n });\n }\n\n async count(filter?: Record<string, unknown>): Promise<number> {\n const result = await this.http.request<CountResult>({\n method: 'POST',\n path: `${this.basePath}/count`,\n body: { filter },\n authenticated: true,\n });\n return result.count;\n }\n\n subscribe(handlers: SubscribeHandlers<T>): () => void {\n if (!this.realtime) {\n throw new Error('Realtime not available: no RealtimeClient configured');\n }\n\n let unsubscribeFn: (() => void) | null = null;\n let pendingUnsub = false;\n\n const callback = (event: DatabaseChangeEvent) => {\n switch (event.type) {\n case 'insert':\n if (handlers.onInsert && event.document) {\n handlers.onInsert(event.document as T & { _id: string });\n }\n break;\n case 'update':\n if (handlers.onUpdate && event.document) {\n handlers.onUpdate(event.document as T & { _id: string });\n }\n break;\n case 'delete':\n if (handlers.onDelete) {\n handlers.onDelete(event.documentId);\n }\n break;\n }\n };\n\n this.realtime\n .subscribe(this.projectId, this.collectionName, callback, handlers.onError, handlers.where)\n .then((unsub) => {\n if (pendingUnsub) {\n // Unsubscribe was called before async subscribe resolved\n unsub();\n } else {\n unsubscribeFn = unsub;\n }\n })\n .catch((err) => {\n if (handlers.onError) {\n handlers.onError(err instanceof Error ? err : new Error(String(err)));\n }\n });\n\n // Return synchronous unsubscribe\n return () => {\n if (unsubscribeFn) {\n unsubscribeFn();\n } else {\n pendingUnsub = true;\n }\n };\n }\n\n subscribeEvents(handlers: SubscribeEventsHandlers<T>): StreamSubscription {\n if (!this.realtime) {\n throw new Error('Realtime not available: no RealtimeClient configured');\n }\n\n let lastCursor: string | undefined;\n let unsub: (() => void) | null = null;\n let unsubscribed = false;\n\n const cursorStorage = handlers.cursorStorage;\n const cursorKey =\n handlers.cursorKey ?? `${this.projectId}:${this.collectionName}`;\n\n // Loud warning for the #1 footgun: sharing a persisted cursor across\n // differently-filtered subscriptions silently skips events on reconnect\n // because the stream position represents \"seen\", not \"matched\".\n if (\n cursorStorage &&\n handlers.where &&\n Object.keys(handlers.where).length > 0 &&\n handlers.cursorKey === undefined\n ) {\n console.warn(\n `[spacelr] subscribeEvents on ${this.projectId}:${this.collectionName} uses a 'where' filter with cursorStorage but no explicit cursorKey — ` +\n `filtered subscriptions sharing the default key will silently skip events across reconnects. ` +\n `Pass a unique cursorKey (e.g. include a hash of the filter).`,\n );\n }\n // Capture after the null-guard so the closures below don't need `!`.\n const realtime = this.realtime;\n // Async saves must not complete out of order — that would regress the\n // persisted cursor below an already-emitted event. Chain every save\n // onto the previous so they serialize end-to-end while still being\n // decoupled from event delivery.\n let lastSavePromise: Promise<unknown> = Promise.resolve();\n\n const onEvent = async ({\n eventId,\n event,\n }: {\n eventId: string;\n event: DatabaseChangeEvent;\n }) => {\n try {\n if (event.type === 'insert' && handlers.onInsert && event.document) {\n await handlers.onInsert({\n ...(event.document as T & { _id: string }),\n _eventId: eventId,\n });\n } else if (event.type === 'update' && handlers.onUpdate && event.document) {\n await handlers.onUpdate({\n ...(event.document as T & { _id: string }),\n _eventId: eventId,\n });\n } else if (event.type === 'delete' && handlers.onDelete) {\n await handlers.onDelete(event.documentId, eventId);\n }\n lastCursor = eventId;\n if (cursorStorage) {\n // Chain this save onto the previous one so async backends can't\n // complete out of order and regress the stored cursor. Both\n // fulfilled and rejected continuations still run `save()` —\n // we don't want one failed save to skip the next event's save.\n // Each save's own failure surfaces via onError asynchronously.\n lastSavePromise = lastSavePromise\n .then(\n () => cursorStorage.save(cursorKey, eventId),\n () => cursorStorage.save(cursorKey, eventId),\n )\n .catch((err) => {\n handlers.onError?.(err instanceof Error ? err : new Error(String(err)));\n });\n }\n } catch (err) {\n // Rethrow so RealtimeClient's per-subscriber queue sees the rejection and\n // skips advancing its own `lastDeliveredId`. That same catch calls\n // `handlers.onError` for us — do NOT call it here or the user would see\n // the error twice.\n throw err instanceof Error ? err : new Error(String(err));\n }\n };\n\n const callSubscribe = (sinceId: string | undefined): Promise<() => void> =>\n realtime.subscribeWithCursor({\n projectId: this.projectId,\n collectionName: this.collectionName,\n sinceId,\n where: handlers.where,\n onEvent,\n onGap: handlers.onGap,\n onError: handlers.onError,\n });\n\n // Fast path: no storage (or explicit sinceId) — skip the extra await\n // so consumers that don't opt into cursor persistence keep the same\n // one-microtask handshake timing they had before.\n let promise: Promise<() => void>;\n if (handlers.sinceId !== undefined || !cursorStorage) {\n promise = callSubscribe(handlers.sinceId);\n } else {\n promise = (async () => {\n let resumeFrom: string | undefined;\n try {\n const loaded = await Promise.resolve(cursorStorage.load(cursorKey));\n if (loaded) resumeFrom = loaded;\n } catch (err) {\n // Storage read failure is never a reason to skip the subscription —\n // resume fresh and let the consumer's onError see the warning.\n handlers.onError?.(err instanceof Error ? err : new Error(String(err)));\n }\n return callSubscribe(resumeFrom);\n })();\n }\n\n promise\n .then((u) => {\n if (unsubscribed) {\n // Caller already asked to cancel; release the reader immediately.\n u();\n } else {\n unsub = u;\n }\n })\n .catch(() => undefined); // onError already surfaced inside the realtime layer.\n\n return {\n unsubscribe(): void {\n unsubscribed = true;\n if (unsub) {\n unsub();\n unsub = null;\n }\n },\n getCursor(): string | undefined {\n return lastCursor;\n },\n };\n }\n\n /**\n * Snapshot-aware subscribe — see #99 / SubscribeWithSnapshotOptions.\n *\n * Returns a Promise that resolves with the unsubscribe function only\n * after the first `onSnapshot` has run to completion (initial-load\n * signal). Calling unsubscribe() while the snapshot find() is in flight\n * aborts the request and silences the AbortError. Gap-recovery state\n * machine lands in Task 6.\n */\n async subscribeWithSnapshot(\n opts: SubscribeWithSnapshotOptions<T>,\n ): Promise<() => void> {\n if (!this.realtime) {\n throw new Error('Realtime not available: no RealtimeClient configured');\n }\n\n let isActive = true;\n let currentUnsubscribe: (() => void) | null = null;\n\n // Cancellation contract — see spec §\"Cancellation rules\". The snapshot\n // find() is the only awaited HTTP call inside this helper, and the\n // unsubscribe closure aborts it so a torn-down helper doesn't keep a\n // request in flight (and doesn't fire a stale onSnapshot when it lands).\n // Replaced on each recovery so a fresh find() can be aborted independently\n // of any prior aborted controllers.\n let snapshotAbort = new AbortController();\n\n // Gap-recovery state machine (spec §\"Recovery flow\"). Three states:\n // - idle — no recovery in flight; gap fires runRecovery\n // - running — recovery in flight; further gaps queue ONE re-run\n // - pending-rerun — re-run already queued; further gaps coalesce\n // Net effect: rapid gap bursts collapse into at most one trailing\n // re-run after the in-flight recovery finishes.\n type RecoveryState = 'idle' | 'running' | 'pending-rerun';\n let recoveryState: RecoveryState = 'idle';\n\n // Capture realtime now so the closures don't have to re-narrow the\n // nullable `this.realtime`.\n const realtime = this.realtime;\n\n // Project the helper's `where` (full MongoDB query) into the\n // gateway's stream-side filter shape (top-level primitive equality\n // only). Operator-shaped values (e.g. `{ $in: [...] }`) are\n // silently dropped from the live filter — see the JSDoc on\n // `SubscribeWithSnapshotOptions.where` for the full semantics. The\n // snapshot path keeps the original `where` unchanged.\n const streamWhere = ((): Record<string, string | number | boolean> | undefined => {\n if (!opts.where) return undefined;\n const out: Record<string, string | number | boolean> = {};\n for (const [k, v] of Object.entries(opts.where)) {\n if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {\n out[k] = v;\n }\n }\n return Object.keys(out).length > 0 ? out : undefined;\n })();\n\n // Operator-only `where` (no primitive values to project) means the\n // live stream filter is empty — onChange will receive every event\n // on the collection, not just the filter-matching ones. Snapshot is\n // still correctly filtered. Warn so production runs show why\n // onChange volume is higher than expected.\n if (\n opts.where &&\n streamWhere === undefined &&\n Object.keys(opts.where).length > 0\n ) {\n // eslint-disable-next-line no-console\n console.warn(\n `[spacelr] subscribeWithSnapshot on ${this.projectId}:${this.collectionName}: ` +\n `'where' filter contains only operator-shaped values (e.g. $gt, $in) which ` +\n `are not supported by the live stream filter. onChange will receive ALL ` +\n `collection events; snapshot is still filtered.`,\n );\n }\n\n const runRecovery = async (): Promise<void> => {\n // Tear down the current reader BEFORE starting a new one — frees the\n // server-side reader slot so the dedup branch on the new subscribe\n // doesn't piggy-back on the soon-to-be-replaced state.\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n\n if (!isActive) return;\n\n // Fresh subscribe — server returns a new latestStreamId atomically with\n // the reader-attach. Wire `onGap: handleGap` so a second gap during\n // recovery is also dispatched.\n //\n // Same `subscribeAckResolved` pattern as the initial subscribe: the\n // realtime layer fires `options.onError` synchronously before throwing\n // when the handshake ack carries an error. Without the gate, a\n // transient handshake failure would flip `isActive = false` and\n // permanently abandon the subscription — a subsequent gap would not\n // re-trigger recovery (state machine returns to idle but isActive is\n // already false). The catch below is the right home for handshake\n // failures: it surfaces via opts.onError without flipping isActive.\n let recoveryAckResolved = false;\n\n // Snapshot-ordering buffer for recovery (Codex review P1 #2). Same\n // contract as the initial-path buffer: queue live events arriving\n // between this recovery's subscribe-ack and its onSnapshot, then\n // drain them in order.\n let recoverySnapshotDelivered = false;\n const pendingRecoveryEvents: DatabaseChangeEvent[] = [];\n\n let next: {\n unsubscribe: () => void;\n realtimeMode: 'pubsub' | 'stream';\n latestStreamId: string | null;\n };\n try {\n next = await realtime.subscribeWithCursorWithMeta({\n projectId: this.projectId,\n collectionName: this.collectionName,\n sinceId: undefined,\n where: streamWhere,\n onEvent: async ({ event }) => {\n if (!isActive) return;\n if (!recoverySnapshotDelivered) {\n pendingRecoveryEvents.push(event);\n return;\n }\n await opts.onChange(event);\n },\n onError: (err) => {\n if (!isActive) return;\n // Suppress pre-handshake errors; the catch below surfaces them.\n if (!recoveryAckResolved) return;\n isActive = false;\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n snapshotAbort.abort();\n opts.onError?.(err);\n },\n onGap: handleGap,\n });\n recoveryAckResolved = true;\n } catch (err) {\n if (!isActive) return;\n opts.onError?.(err instanceof Error ? err : new Error(String(err)));\n return;\n }\n\n if (!isActive) {\n next.unsubscribe();\n return;\n }\n\n currentUnsubscribe = next.unsubscribe;\n\n // Fresh AbortController so the unsubscribe path can cancel this find()\n // independently of any earlier (already-resolved) snapshot reads.\n snapshotAbort = new AbortController();\n let recoveryDocs: (T & { _id: string })[];\n try {\n const builder = this.find(opts.where);\n if (opts.sort) builder.sort(opts.sort);\n if (opts.limit !== undefined) builder.limit(opts.limit);\n if (opts.select) builder.select(opts.select);\n const result = await builder.execute(snapshotAbort.signal);\n recoveryDocs = result.documents;\n } catch (err) {\n if (!isActive) return;\n if (err instanceof DOMException && err.name === 'AbortError') return;\n // Codex review P1 #3: a non-abort recovery-snapshot failure means\n // the local state is provably stale (the gap fired BECAUSE\n // retention overflowed) AND the replacement snapshot didn't\n // arrive. Continuing to deliver onChange events on top of the\n // pre-gap state would corrupt the user's local model. Tear down\n // the live subscription so no further `opts.onChange` fires; the\n // user gets onSnapshotError/onError and can decide whether to\n // re-subscribe.\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n isActive = false;\n const error = err instanceof Error ? err : new Error(String(err));\n if (opts.onSnapshotError) opts.onSnapshotError(error);\n else opts.onError?.(error);\n return;\n }\n\n if (!isActive) return;\n try {\n await opts.onSnapshot(recoveryDocs, next.latestStreamId);\n } catch (err) {\n // User's onSnapshot threw during recovery — tear down so the\n // (newly-acquired) server-side reader slot doesn't leak.\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n isActive = false;\n const error = err instanceof Error ? err : new Error(String(err));\n opts.onError?.(error);\n return;\n }\n\n // Drain the recovery's snapshot-ordering buffer (Codex P1 #2).\n // Same shape as the initial path — flip the flag only when the\n // queue is empty so events queued mid-drain are picked up by the\n // same loop.\n while (true) {\n if (!isActive) return;\n if (pendingRecoveryEvents.length === 0) {\n recoverySnapshotDelivered = true;\n break;\n }\n const ev = pendingRecoveryEvents.shift();\n if (ev !== undefined) {\n try {\n await opts.onChange(ev);\n } catch (err) {\n // onChange on a buffered recovery event threw — tear down\n // the live subscription. State is corrupt either way.\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n isActive = false;\n const error = err instanceof Error ? err : new Error(String(err));\n opts.onError?.(error);\n return;\n }\n }\n }\n };\n\n const dispatchRecovery = (): void => {\n if (!isActive) return;\n if (recoveryState === 'running') {\n recoveryState = 'pending-rerun';\n return;\n }\n if (recoveryState === 'pending-rerun') {\n return; // already queued; coalesce\n }\n recoveryState = 'running';\n void (async () => {\n // Yield once so synchronous gap bursts following the dispatch can\n // observe state='running' and queue a pending-rerun before any\n // teardown work begins. Without this, `runRecovery`'s synchronous\n // `currentUnsubscribe?.()` removes the state from\n // `streamSubscriptions` BEFORE later gaps can reach `onGap`,\n // dropping the coalescing signal entirely.\n await Promise.resolve();\n while (true) {\n try {\n await runRecovery();\n } catch (err) {\n // runRecovery routes its own errors via onSnapshotError / onError\n // and returns gracefully. This catch is a final safety net —\n // without it the while-true loop would spin on an unexpected\n // throw and exhaust the event loop. Surface and exit.\n //\n // Reset `recoveryState` FIRST so a throwing user callback (e.g.\n // `opts.onError`) cannot strand the state machine in 'running'\n // — that would silently coalesce every future gap into\n // 'pending-rerun' with no drainer.\n recoveryState = 'idle';\n if (!isActive) return;\n const error = err instanceof Error ? err : new Error(String(err));\n try {\n opts.onError?.(error);\n } catch {\n // User's callback threw — don't mask the original error\n // path. The state machine is already idle so the next gap\n // will recover.\n }\n return;\n }\n if (!isActive) {\n recoveryState = 'idle';\n return;\n }\n // Cast: TS narrows `recoveryState` to 'running' here because the\n // assignment at line 913 is the most recent observed write. TS\n // doesn't track the closure-mutation done by `dispatchRecovery`\n // (which can flip the state to 'pending-rerun' between iterations).\n // The cast widens the type back to RecoveryState so the\n // pending-rerun branch is reachable. Removing this cast yields\n // TS2367 (\"comparison appears unintentional\").\n if ((recoveryState as RecoveryState) === 'pending-rerun') {\n recoveryState = 'running';\n continue;\n }\n recoveryState = 'idle';\n return;\n }\n })();\n };\n\n const handleGap = (info: StreamGapInfo): void => {\n if (!isActive) return;\n // Only \"outside-retention-window\" requires a re-snapshot — the other\n // GapReason values are transient (redis-unavailable, replay-error,\n // replay-truncated) and the realtime layer's resubscribe path handles\n // them without needing a fresh baseline.\n if (info.reason !== 'outside-retention-window') return;\n dispatchRecovery();\n };\n\n // Step 1 of the spec flow: subscribe first so the server returns\n // `latestStreamId` atomically with the reader-attach. The reader is\n // already buffering events when this resolves — anything that arrives\n // during the snapshot find() is queued and replayed via onChange after\n // onSnapshot returns.\n // Tracks whether the initial subscribe-events handshake has resolved\n // successfully. Until then, errors surfaced by the realtime layer's\n // onError closure are HANDSHAKE failures (e.g. ack.error =\n // 'not-stream-collection') — they are also thrown by the helper's\n // try/catch below and remapped to SpacelrError. Forwarding them to\n // opts.onError too would double-notify the caller (review-round-3\n // bug #2). Only forward post-handshake.\n let subscribeAckResolved = false;\n\n // Initial-path snapshot-ordering buffer (Codex review P1 #2).\n // Live events arriving between subscribe-ack and onSnapshot completion\n // would otherwise fire opts.onChange BEFORE opts.onSnapshot, violating\n // the documented \"snapshot first, then live changes\" contract. Queue\n // events while `initialSnapshotDelivered === false`; drain them in\n // arrival order after onSnapshot returns. The drain flips the flag\n // ONLY when the queue is empty, so events that arrive mid-drain are\n // picked up by the same loop (no race window before live delivery).\n let initialSnapshotDelivered = false;\n const pendingInitialEvents: DatabaseChangeEvent[] = [];\n\n let initial: Awaited<ReturnType<typeof realtime.subscribeWithCursorWithMeta>>;\n try {\n initial = await realtime.subscribeWithCursorWithMeta({\n projectId: this.projectId,\n collectionName: this.collectionName,\n sinceId: undefined,\n where: streamWhere,\n onEvent: async ({ event }) => {\n if (!isActive) return;\n // Buffer live events until the initial snapshot has been\n // delivered. Returns immediately so the reader can advance\n // (no blocking the cross-subscriber Promise.all in\n // StreamReaderService).\n if (!initialSnapshotDelivered) {\n pendingInitialEvents.push(event);\n return;\n }\n await opts.onChange(event);\n },\n onError: (err) => {\n if (!isActive) return;\n // Suppress pre-handshake errors; the helper's catch block below\n // surfaces them via the typed SpacelrError throw.\n if (!subscribeAckResolved) return;\n // Post-handshake fatal stream error — flip isActive so an\n // in-flight find() resolution doesn't push a stale onSnapshot,\n // abort the snapshot read so it rejects promptly with\n // AbortError (silenced in the find()-catch), and tear down the\n // reader slot. Same teardown shape as the user-driven\n // unsubscribe() path.\n isActive = false;\n snapshotAbort.abort();\n currentUnsubscribe?.();\n // Null out so the find()-catch doesn't double-tear-down the slot.\n currentUnsubscribe = null;\n opts.onError?.(err);\n },\n onGap: handleGap,\n });\n subscribeAckResolved = true;\n } catch (err) {\n // The handshake itself rejected. The real gateway returns\n // `error: 'not-stream-collection'` for pubsub-mode collections, which\n // `_subscribeInternal` now surfaces as a SpacelrError with that code.\n // Map it to the typed `SUBSCRIBE_WITH_SNAPSHOT_REQUIRES_STREAM` so\n // callers get the SDK-level discriminator regardless of whether the\n // detection happened via the ack-error path or the realtimeMode check\n // below. All other handshake errors propagate unchanged.\n isActive = false;\n // _subscribeInternal throws SpacelrError when the ack carries an\n // error; use instanceof for type-safe code extraction (no\n // unchecked cast).\n const code = err instanceof SpacelrError ? err.code : undefined;\n if (code === 'not-stream-collection') {\n throw new SpacelrError(\n `subscribeWithSnapshot requires a stream-mode collection. ` +\n `Use subscribe() for pubsub collections.`,\n 'SUBSCRIBE_WITH_SNAPSHOT_REQUIRES_STREAM',\n undefined,\n { collectionName: this.collectionName },\n );\n }\n throw err;\n }\n\n if (initial.realtimeMode !== 'stream') {\n // Defensive secondary gate: if a future server returns ack OK + a\n // non-stream realtimeMode (instead of error: 'not-stream-collection'),\n // we still surface the typed error rather than silently subscribing\n // to a pubsub collection. Today's gateway throws via the ack-error\n // path above; this branch is belt-and-suspenders.\n isActive = false;\n initial.unsubscribe();\n throw new SpacelrError(\n `subscribeWithSnapshot requires a stream-mode collection. ` +\n `Use subscribe() for pubsub collections.`,\n 'SUBSCRIBE_WITH_SNAPSHOT_REQUIRES_STREAM',\n undefined,\n { collectionName: this.collectionName },\n );\n }\n\n currentUnsubscribe = initial.unsubscribe;\n\n // Step 2: snapshot. The reader is already buffering events; anything\n // that arrives during this find() is queued and will be delivered via\n // onChange after onSnapshot returns. The unsubscribe closure aborts the\n // request via `snapshotAbort` so a torn-down helper doesn't push a stale\n // onSnapshot.\n let snapshotDocs: (T & { _id: string })[];\n try {\n const builder = this.find(opts.where);\n if (opts.sort) builder.sort(opts.sort);\n if (opts.limit !== undefined) builder.limit(opts.limit);\n if (opts.select) builder.select(opts.select);\n const result = await builder.execute(snapshotAbort.signal);\n snapshotDocs = result.documents;\n } catch (err) {\n currentUnsubscribe?.();\n isActive = false;\n\n // Aborted reads are silent — see spec §\"Cancellation rules\". The abort\n // is triggered by the user's unsubscribe() (or by the wrapped onError\n // path which already surfaced the cause), so the user already knows the\n // helper is being torn down. Don't fire onSnapshotError or onError —\n // they'd be redundant noise. Re-throw so the awaited promise rejects.\n const isAbort =\n err instanceof DOMException && err.name === 'AbortError';\n if (isAbort) throw err;\n\n const error = err instanceof Error ? err : new Error(String(err));\n if (opts.onSnapshotError) opts.onSnapshotError(error);\n else opts.onError?.(error);\n throw error;\n }\n\n // Reachable when the wrapped onError flipped `isActive` during the\n // in-flight find() above. Don't fire onSnapshot against a torn-down\n // stream — onError already surfaced the cause.\n if (!isActive) {\n currentUnsubscribe?.();\n return () => undefined;\n }\n\n try {\n await opts.onSnapshot(snapshotDocs, initial.latestStreamId);\n } catch (err) {\n // User's onSnapshot threw — tear down the live subscription so the\n // server-side reader slot doesn't leak. Surface via opts.onError\n // (the snapshot itself was delivered to find() OK; the user\n // callback failed).\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n isActive = false;\n const error = err instanceof Error ? err : new Error(String(err));\n opts.onError?.(error);\n throw error;\n }\n\n // Drain the snapshot-ordering buffer (Codex review P1 #2). Events\n // queued during the find()/onSnapshot window are delivered in\n // arrival order BEFORE the helper returns its unsubscribe handle.\n // Loop checks length-then-flip in the same synchronous step so a\n // wrapped onEvent that fires between the check and the flip cannot\n // get orphaned in the queue.\n while (true) {\n if (!isActive) return () => undefined;\n if (pendingInitialEvents.length === 0) {\n initialSnapshotDelivered = true;\n break;\n }\n const ev = pendingInitialEvents.shift();\n if (ev !== undefined) {\n try {\n await opts.onChange(ev);\n } catch (err) {\n // User's onChange threw on a buffered event — same teardown\n // shape as the onSnapshot-throw path. Subscription is dead.\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n isActive = false;\n const error = err instanceof Error ? err : new Error(String(err));\n opts.onError?.(error);\n throw error;\n }\n }\n }\n\n return () => {\n if (!isActive) return;\n isActive = false;\n // Aborts an in-flight snapshot find() if unsubscribe() races the\n // initial read or any recovery read; no-op if the read already\n // resolved.\n snapshotAbort.abort();\n currentUnsubscribe?.();\n };\n }\n}\n\nexport class DatabaseModule {\n private http: HttpClient;\n private realtime: RealtimeClient | null;\n private projectId: string;\n\n constructor(http: HttpClient, projectId: string, realtime?: RealtimeClient) {\n this.http = http;\n this.projectId = projectId;\n this.realtime = realtime ?? null;\n }\n\n collection<T = Record<string, unknown>>(name: string): CollectionRef<T> {\n return new CollectionRef<T>(this.http, this.realtime, this.projectId, name);\n }\n}\n","import { HttpClient } from '../core';\n\nexport interface PushSubscriptionInfo {\n id: string;\n platform: 'web' | 'android' | 'ios';\n endpoint?: string;\n deviceToken?: string;\n deviceId?: string;\n deviceName?: string;\n userAgent?: string;\n isActive: boolean;\n lastUsedAt?: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface VapidKeyResponse {\n publicKey: string;\n}\n\nconst DEVICE_ID_KEY = 'spacelr_device_id';\n\nexport class NotificationsModule {\n private customDeviceId: string | null = null;\n private customDeviceName: string | null = null;\n\n constructor(private http: HttpClient) {}\n\n /** Set a custom device ID (e.g. from Capacitor Preferences for persistence beyond localStorage) */\n setDeviceId(id: string): void {\n this.customDeviceId = id;\n }\n\n /** Set a custom device name (e.g. \"iOS App\", \"macOS - Chrome\") */\n setDeviceName(name: string): void {\n this.customDeviceName = name;\n }\n\n /** Get or generate a stable device identifier. Custom ID takes priority over localStorage. */\n private getDeviceId(): string | undefined {\n if (this.customDeviceId) return this.customDeviceId;\n if (typeof localStorage === 'undefined') return undefined;\n let id = localStorage.getItem(DEVICE_ID_KEY);\n if (!id) {\n id = crypto.randomUUID();\n localStorage.setItem(DEVICE_ID_KEY, id);\n }\n return id;\n }\n\n /** Get device name: custom name > auto-detected from user agent > undefined */\n private getDeviceName(): string | undefined {\n if (this.customDeviceName) return this.customDeviceName;\n return this.detectDeviceName();\n }\n\n /** Auto-detect a short device label from navigator.userAgent (e.g. \"macOS - Chrome\") */\n private detectDeviceName(): string | undefined {\n if (typeof navigator === 'undefined' || !navigator.userAgent) return undefined;\n const ua = navigator.userAgent;\n\n let os = 'Unknown';\n if (/iPad|iPhone|iPod/.test(ua)) os = 'iOS';\n else if (/Android/.test(ua)) os = 'Android';\n else if (/Mac OS X/.test(ua)) os = 'macOS';\n else if (/Windows/.test(ua)) os = 'Windows';\n else if (/Linux/.test(ua)) os = 'Linux';\n\n let browser = 'Unknown';\n if (/Edg\\//.test(ua)) browser = 'Edge';\n else if (/OPR\\/|Opera/.test(ua)) browser = 'Opera';\n else if (/Chrome\\//.test(ua)) browser = 'Chrome';\n else if (/Safari\\//.test(ua) && !/Chrome/.test(ua)) browser = 'Safari';\n else if (/Firefox\\//.test(ua)) browser = 'Firefox';\n\n return `${os} - ${browser}`;\n }\n\n /** Get the VAPID public key for Web Push setup */\n async getVapidPublicKey(): Promise<VapidKeyResponse> {\n return this.http.request<VapidKeyResponse>({\n method: 'GET',\n path: '/notifications/vapid-key',\n authenticated: true,\n });\n }\n\n /** Register a push subscription (web, android, or ios) */\n async subscribe(\n subscription: {\n platform: 'web' | 'android' | 'ios';\n endpoint?: string;\n keys?: { p256dh: string; auth: string };\n deviceToken?: string;\n },\n deviceName?: string,\n ): Promise<PushSubscriptionInfo> {\n return this.http.request<PushSubscriptionInfo>({\n method: 'POST',\n path: '/notifications/subscribe',\n body: {\n ...subscription,\n deviceName: deviceName ?? this.getDeviceName(),\n deviceId: this.getDeviceId(),\n },\n authenticated: true,\n });\n }\n\n /** Unregister a push subscription */\n async unsubscribe(\n platform: 'web' | 'android' | 'ios',\n identifier: string,\n ): Promise<{ deleted: boolean }> {\n const body: Record<string, string> = { platform };\n if (platform === 'web') {\n body['endpoint'] = identifier;\n } else {\n body['deviceToken'] = identifier;\n }\n\n return this.http.request<{ deleted: boolean }>({\n method: 'DELETE',\n path: '/notifications/subscribe',\n body,\n authenticated: true,\n });\n }\n\n /** Get all subscriptions for the current user */\n async getSubscriptions(): Promise<PushSubscriptionInfo[]> {\n return this.http.request<PushSubscriptionInfo[]>({\n method: 'GET',\n path: '/notifications/subscriptions',\n authenticated: true,\n });\n }\n\n /**\n * Helper: Register browser Web Push subscription.\n * Requests notification permission, subscribes via Push API,\n * and registers the subscription with the server.\n */\n async registerBrowserPush(\n serviceWorkerRegistration: ServiceWorkerRegistration,\n deviceName?: string,\n ): Promise<PushSubscriptionInfo> {\n // Get VAPID key\n const { publicKey } = await this.getVapidPublicKey();\n\n if (!publicKey) {\n throw new Error(\n 'VAPID public key not configured on the server',\n );\n }\n\n // Request notification permission\n const permission = await Notification.requestPermission();\n if (permission !== 'granted') {\n throw new Error(\n `Notification permission denied: ${permission}`,\n );\n }\n\n // Convert VAPID key to Uint8Array\n const applicationServerKey = this.urlBase64ToUint8Array(publicKey);\n\n // Subscribe via Push API\n const pushSubscription =\n await serviceWorkerRegistration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey,\n });\n\n const subJson = pushSubscription.toJSON();\n const p256dh = subJson.keys?.['p256dh'];\n const auth = subJson.keys?.['auth'];\n\n if (!subJson.endpoint || !p256dh || !auth) {\n throw new Error('Invalid push subscription: missing endpoint or keys');\n }\n\n // Register with server\n return this.subscribe(\n {\n platform: 'web',\n endpoint: subJson.endpoint,\n keys: { p256dh, auth },\n },\n deviceName,\n );\n }\n\n /**\n * Helper: Register a mobile device push token (FCM or APNs).\n */\n async registerDevicePush(\n deviceToken: string,\n platform: 'android' | 'ios',\n deviceName?: string,\n ): Promise<PushSubscriptionInfo> {\n return this.subscribe(\n {\n platform,\n deviceToken,\n },\n deviceName,\n );\n }\n\n private urlBase64ToUint8Array(base64String: string): ArrayBuffer {\n const padding = '='.repeat((4 - (base64String.length % 4)) % 4);\n const base64 = (base64String + padding)\n .replace(/-/g, '+')\n .replace(/_/g, '/');\n\n const rawData = atob(base64);\n const outputArray = new Uint8Array(rawData.length);\n\n for (let i = 0; i < rawData.length; ++i) {\n outputArray[i] = rawData.charCodeAt(i);\n }\n return outputArray.buffer as ArrayBuffer;\n }\n}\n","import { HttpClient } from '../core';\n\nexport interface FunctionInvokeOptions {\n /** Webhook secret — used for `webhook` and `hybrid` invokeMode. */\n secret?: string;\n /**\n * Whether to attach the signed-in user's bearer token via `Authorization`\n * (managed by the SDK's TokenManager).\n *\n * Default: `true` when no `secret` is provided, `false` when `secret` is\n * set. Override explicitly for the hybrid case (provide both) or to\n * suppress the header even when logged in.\n *\n * If `true` but the user is not signed in, the header is simply omitted —\n * safe for `public` invokeMode.\n */\n authenticated?: boolean;\n payload?: Record<string, unknown>;\n}\n\nexport interface FunctionInvokeResult {\n success: boolean;\n executionId?: string;\n error?: string;\n}\n\nexport class FunctionsModule {\n private http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n /**\n * Invoke a function.\n * Calls POST `/functions/:projectId/:functionId/invoke`, resolved against\n * `config.apiUrl` (which already carries the `/api/v1` prefix).\n *\n * Auth defaults, based on `invokeMode` semantics:\n * - webhook: pass `secret` → Authorization is NOT attached\n * - authenticated: pass nothing → Authorization IS attached (from token manager)\n * - public: pass nothing → Authorization is attached if logged in, else omitted\n * - hybrid: pass both `secret` and `authenticated: true`\n *\n * To force a specific behaviour, set `authenticated` explicitly — it wins\n * over the `secret`-based default.\n */\n async invoke(\n projectId: string,\n functionId: string,\n options: FunctionInvokeOptions = {},\n ): Promise<FunctionInvokeResult> {\n // Empty-string secret is treated as absent — both the header guard and\n // the authenticated-default check use `?.length`, so the two stay in\n // lock-step (no \"header omitted but still treated as webhook mode\").\n const hasSecret = (options.secret?.length ?? 0) > 0;\n\n const headers: Record<string, string> = {};\n if (hasSecret) {\n headers['X-Webhook-Secret'] = options.secret as string;\n }\n\n // Default: authenticate unless a webhook secret is provided.\n // Explicit `authenticated` overrides this.\n const authenticated = options.authenticated ?? !hasSecret;\n\n return this.http.request<FunctionInvokeResult>({\n method: 'POST',\n path: `/functions/${encodeURIComponent(projectId)}/${encodeURIComponent(functionId)}/invoke`,\n headers,\n body: options.payload ?? {},\n authenticated,\n });\n }\n}\n","import { SpacelrClientConfig, StoredTokens } from './types';\nimport { HttpClient, TokenManager, MemoryTokenStorage, BrowserTokenStorage, RealtimeClient, ConnectionState } from './core';\nimport { AuthLostReason } from './core/token-manager';\nimport { AuthModule, StorageModule, DatabaseModule, NotificationsModule, FunctionsModule } from './modules';\n\nexport interface SpacelrClient {\n readonly auth: AuthModule;\n readonly storage: StorageModule;\n readonly db: DatabaseModule;\n readonly notifications: NotificationsModule;\n readonly functions: FunctionsModule;\n /**\n * Store auth tokens externally (e.g. obtained via the auth-components\n * library that bypasses the SDK). Makes the tokens available to HTTP +\n * realtime clients. Refresh is on-demand: the SDK will refresh the\n * access token when it next makes a request and the token is within the\n * expiry buffer (or on a 401 retry).\n */\n setTokens(tokens: StoredTokens): Promise<void>;\n /** Clear stored tokens and reset auth-loss state. */\n clearTokens(): Promise<void>;\n /** Disconnect realtime WebSocket (if connected) */\n disconnect(): void;\n /**\n * Subscribe to auth loss. Fires in three situations:\n * - `'refresh-failed'` — a refresh call was rejected by the server\n * (refresh token revoked or expired). Note: this can also fire from a\n * *background* refresh triggered inside the expiry buffer window, at\n * which point the current access token may still be valid for up to\n * `refreshBufferSeconds` (default 60). Treat the session as over anyway;\n * the refresh token is dead.\n * - `'unauthorized'` — an authenticated request came back 401 and no\n * fresh token could be produced (no refresh token, or the retry after\n * a successful refresh was itself 401).\n *\n * Typical consumer reaction: clear UI state and show the login screen.\n * The event is deduped until `setTokens`/`clearTokens` is called again.\n */\n onAuthLost(listener: (reason: AuthLostReason) => void): () => void;\n /** Subscribe to successful token refreshes. */\n onTokenRefreshed(listener: (tokens: StoredTokens) => void): () => void;\n /**\n * Subscribe to realtime socket connection-state changes. Fires on every\n * transition (dedup: identical consecutive states are skipped). Returns\n * an unsubscribe function.\n *\n * States:\n * - `'disconnected'` — initial state before the first subscribe, or after\n * the SDK has torn the realtime socket down (e.g. on logout via the\n * internal `realtime.disconnect()`). This is a terminal state for the\n * current realtime client; reconnect happens on the next `subscribe()`.\n * - `'reconnecting'` — connecting or rebuilding the socket (initial\n * connect, socket drop, `reconnect_failed` rebuild, or a wake-up event\n * after suspend).\n * - `'connected'` — authenticated and actively listening for events.\n */\n onConnectionStateChanged(listener: (state: ConnectionState) => void): () => void;\n /** Read the current realtime socket state without waiting for an event. */\n getConnectionState(): ConnectionState;\n}\n\nexport function createClient(config: SpacelrClientConfig): SpacelrClient {\n const tokenStorage = config.tokenStorage\n ?? (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined'\n ? new BrowserTokenStorage()\n : new MemoryTokenStorage());\n const tokenManager = new TokenManager(\n tokenStorage,\n config.refreshBufferSeconds ?? 60\n );\n const httpClient = new HttpClient(config, tokenManager);\n\n const realtime = new RealtimeClient({\n baseUrl: config.apiUrl,\n getToken: () => tokenManager.getAccessToken(),\n onTokenRefreshed: (listener) =>\n tokenManager.onTokenRefreshed((tokens) => listener(tokens.accessToken)),\n });\n\n const auth = new AuthModule(httpClient, tokenManager, config);\n const storage = new StorageModule(httpClient, tokenManager, config);\n const db = new DatabaseModule(httpClient, config.projectId, realtime);\n const notifications = new NotificationsModule(httpClient);\n const functions = new FunctionsModule(httpClient);\n\n return {\n auth,\n storage,\n db,\n notifications,\n functions,\n setTokens(tokens) {\n return tokenManager.setTokens(tokens);\n },\n clearTokens() {\n return tokenManager.clearTokens();\n },\n disconnect() {\n realtime.disconnect();\n },\n onAuthLost(listener) {\n return tokenManager.onAuthLost(listener);\n },\n onTokenRefreshed(listener) {\n return tokenManager.onTokenRefreshed(listener);\n },\n onConnectionStateChanged(listener) {\n return realtime.onConnectionStateChanged(listener);\n },\n getConnectionState() {\n return realtime.getConnectionState();\n },\n };\n}\n"],"mappings":"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","names":["FileVisibility","GrantType","CodeChallengeMethod","SharePermission","hash","callbacks","document"]}
|
|
1
|
+
{"version":3,"sources":["../../../../libs/sdk/src/core/errors.ts","../../../../libs/sdk/src/core/http-client.ts","../../../../libs/sdk/src/core/token-storage.ts","../../../../libs/sdk/src/core/token-manager.ts","../../../../libs/sdk/src/types/common.ts","../../../../libs/sdk/src/core/pkce.ts","../../../../libs/sdk/src/core/realtime.ts","../../../../libs/sdk/src/core/cursor-storage.ts","../../../../libs/sdk/src/modules/auth.module.ts","../../../../libs/sdk/src/modules/storage.module.ts","../../../../libs/sdk/src/modules/timeline.errors.ts","../../../../libs/sdk/src/modules/timeline.module.ts","../../../../libs/sdk/src/modules/database.module.ts","../../../../libs/sdk/src/modules/notifications.module.ts","../../../../libs/sdk/src/modules/functions.module.ts","../../../../libs/sdk/src/modules/schedule.module.ts","../../../../libs/sdk/src/client.ts"],"sourcesContent":["export class SpacelrError extends Error {\n readonly code: string;\n readonly statusCode?: number;\n readonly details?: Record<string, unknown>;\n\n constructor(\n message: string,\n code: string,\n statusCode?: number,\n details?: Record<string, unknown>\n ) {\n super(message);\n this.name = 'SpacelrError';\n this.code = code;\n this.statusCode = statusCode;\n this.details = details;\n }\n}\n\nexport class SpacelrAuthError extends SpacelrError {\n constructor(\n message: string,\n statusCode = 401,\n details?: Record<string, unknown>\n ) {\n super(message, 'AUTH_ERROR', statusCode, details);\n this.name = 'SpacelrAuthError';\n }\n}\n\nexport class SpacelrNetworkError extends SpacelrError {\n constructor(message: string, details?: Record<string, unknown>) {\n super(message, 'NETWORK_ERROR', undefined, details);\n this.name = 'SpacelrNetworkError';\n }\n}\n\nexport class SpacelrTimeoutError extends SpacelrError {\n constructor(timeoutMs: number) {\n super(\n `Request timed out after ${timeoutMs}ms`,\n 'TIMEOUT_ERROR',\n undefined,\n { timeoutMs }\n );\n this.name = 'SpacelrTimeoutError';\n }\n}\n\nexport class SpacelrTwoFactorRequiredError extends SpacelrError {\n readonly twoFactorToken: string;\n\n constructor(twoFactorToken: string, details?: Record<string, unknown>) {\n super(\n 'Two-factor authentication required',\n 'TWO_FACTOR_REQUIRED',\n 200,\n details\n );\n this.name = 'SpacelrTwoFactorRequiredError';\n this.twoFactorToken = twoFactorToken;\n }\n}\n\n/**\n * Thrown when `collection.search()` is called without the pre-filter the\n * collection requires (issue #165). `details.missingFields` lists the\n * top-level filter keys the server expected.\n *\n * Required filters protect against unindexable full-collection regex\n * scans on large collections — see the collection's `searchConfig` for\n * which fields are required.\n */\nexport class SpacelrSearchFilterRequiredError extends SpacelrError {\n readonly missingFields: string[];\n\n constructor(message: string, details?: Record<string, unknown>) {\n super(message, 'SEARCH_FILTER_REQUIRED', 400, details);\n this.name = 'SpacelrSearchFilterRequiredError';\n const fields = details?.['missingFields'];\n this.missingFields = Array.isArray(fields)\n ? fields.filter((f): f is string => typeof f === 'string')\n : [];\n }\n}\n\nexport class SpacelrEmailVerificationRequiredError extends SpacelrError {\n readonly emailSent: boolean;\n\n constructor(emailSent: boolean, details?: Record<string, unknown>) {\n super(\n emailSent\n ? 'Email verification required. A new verification email has been sent.'\n : 'Email verification required. Please check your inbox for the verification email.',\n 'EMAIL_VERIFICATION_REQUIRED',\n 200,\n details\n );\n this.name = 'SpacelrEmailVerificationRequiredError';\n this.emailSent = emailSent;\n }\n}\n","import { SpacelrClientConfig, ApiResponse } from '../types';\nimport {\n SpacelrError,\n SpacelrAuthError,\n SpacelrNetworkError,\n SpacelrTimeoutError,\n SpacelrTwoFactorRequiredError,\n SpacelrEmailVerificationRequiredError,\n SpacelrSearchFilterRequiredError,\n} from './errors';\nimport { TokenManager } from './token-manager';\n\nexport interface HttpRequestOptions {\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n path: string;\n body?: unknown;\n authenticated?: boolean;\n headers?: Record<string, string>;\n query?: Record<string, string | number | undefined>;\n /** Send cookies cross-origin (credentials: 'include'). Defaults to true for /auth/ paths. */\n withCredentials?: boolean;\n /**\n * External AbortSignal that lets callers cancel an in-flight request before\n * the configured timeout elapses (e.g. `subscribeWithSnapshot` aborting its\n * snapshot find() when the user calls unsubscribe()). Forwarded into the\n * internal AbortController so cancellation surfaces as a DOMException with\n * name 'AbortError' — the caller is responsible for distinguishing it from\n * the timeout-driven abort if it matters.\n */\n signal?: AbortSignal;\n}\n\nexport class HttpClient {\n private config: SpacelrClientConfig;\n private tokenManager: TokenManager;\n\n constructor(config: SpacelrClientConfig, tokenManager: TokenManager) {\n this.config = config;\n this.tokenManager = tokenManager;\n }\n\n async request<T>(options: HttpRequestOptions): Promise<T> {\n return this.requestWithRetry<T>(options, false);\n }\n\n private async requestWithRetry<T>(\n options: HttpRequestOptions,\n isRetry: boolean,\n ): Promise<T> {\n const url = this.buildUrl(options.path, options.query);\n const headers = await this.buildHeaders(options);\n const timeout = this.config.timeout ?? 30000;\n const includeCredentials = options.withCredentials ?? options.path.startsWith('/auth/');\n\n const controller = new AbortController();\n // Track timeout-vs-external precedence so the catch block can't be\n // fooled by a late-arriving external abort listener firing AFTER the\n // timeout already aborted (race window: timeout fires synchronously,\n // catch runs, then the external listener also fires from microtask\n // queue). Without `timeoutFired`, an external abort on top of a\n // timeout-driven abort would misclassify as caller-cancellation.\n let timeoutFired = false;\n const timeoutId = setTimeout(() => {\n timeoutFired = true;\n controller.abort();\n }, timeout);\n\n // Forward an external AbortSignal so callers can cancel the in-flight\n // request before the timeout. We track whether the abort was caller-driven\n // so the catch block can preserve the AbortError instead of masking it as\n // SpacelrTimeoutError.\n let externalAbort = false;\n let onExternalAbort: (() => void) | undefined;\n if (options.signal) {\n if (options.signal.aborted) {\n externalAbort = true;\n controller.abort();\n } else {\n onExternalAbort = () => {\n // Set the flag BEFORE controller.abort() so the catch block\n // reads the correct branch deterministically — abort() can\n // synchronously reject the in-flight fetch and run the catch,\n // and we want `externalAbort === true` to be observable there.\n externalAbort = true;\n controller.abort();\n };\n options.signal.addEventListener('abort', onExternalAbort);\n }\n }\n\n try {\n const response = await fetch(url, {\n method: options.method,\n headers,\n body: options.body ? JSON.stringify(options.body) : undefined,\n signal: controller.signal,\n ...(includeCredentials && { credentials: 'include' as RequestCredentials }),\n });\n\n const responseBody = await this.parseResponse(response);\n\n if (!response.ok) {\n if (options.authenticated && response.status === 401) {\n if (await this.recoverFromAuthFailure(isRetry)) {\n return this.requestWithRetry<T>(options, true);\n }\n }\n this.throwHttpError(response.status, responseBody);\n }\n\n return this.extractData<T>(responseBody);\n } catch (error) {\n if (error instanceof SpacelrError) throw error;\n\n if (error instanceof DOMException && error.name === 'AbortError') {\n // Preserve external aborts as AbortError so callers (e.g.\n // subscribeWithSnapshot) can silence them. Only timeout-driven aborts\n // get rewrapped as SpacelrTimeoutError. `!timeoutFired` is the\n // precedence guard: if the timeout already aborted, classify as\n // timeout regardless of whether the external listener also fired\n // afterwards.\n if (externalAbort && !timeoutFired) throw error;\n throw new SpacelrTimeoutError(timeout);\n }\n\n throw new SpacelrNetworkError(\n error instanceof Error ? error.message : 'Network request failed'\n );\n } finally {\n clearTimeout(timeoutId);\n if (options.signal && onExternalAbort) {\n options.signal.removeEventListener('abort', onExternalAbort);\n }\n }\n }\n\n async uploadForm<T>(\n path: string,\n formData: FormData,\n onProgress?: (event: { loaded: number; total: number }) => void,\n ): Promise<T> {\n return this.uploadFormWithRetry<T>(path, formData, onProgress, false);\n }\n\n private async uploadFormWithRetry<T>(\n path: string,\n formData: FormData,\n onProgress: ((event: { loaded: number; total: number }) => void) | undefined,\n isRetry: boolean,\n ): Promise<T> {\n const url = this.buildUrl(path);\n const headers = await this.buildFormHeaders();\n const timeoutMs = this.config.timeout ?? 30000;\n\n try {\n if (onProgress) {\n return await this.uploadFormWithProgress<T>(url, headers, formData, onProgress, timeoutMs);\n }\n return await this.uploadFormOnce<T>(url, headers, formData, timeoutMs);\n } catch (error) {\n if (error instanceof SpacelrAuthError && error.statusCode === 401) {\n if (await this.recoverFromAuthFailure(isRetry)) {\n return this.uploadFormWithRetry<T>(path, formData, onProgress, true);\n }\n }\n throw error;\n }\n }\n\n /**\n * Shared handler for a 401 on an authenticated request. Returns true if the\n * caller should retry with a refreshed token. 403 is never routed here —\n * it means \"forbidden for this action\" and must not trigger logout.\n *\n * `emitAuthLost('unauthorized')` is deduped inside TokenManager, so it's a\n * no-op if `executeRefresh` already emitted 'refresh-failed'.\n */\n private async recoverFromAuthFailure(isRetry: boolean): Promise<boolean> {\n if (isRetry) {\n this.tokenManager.emitAuthLost('unauthorized');\n return false;\n }\n const refreshed = await this.tokenManager.forceRefresh();\n if (refreshed) return true;\n this.tokenManager.emitAuthLost('unauthorized');\n return false;\n }\n\n private async uploadFormOnce<T>(\n url: string,\n headers: Record<string, string>,\n formData: FormData,\n timeoutMs: number,\n ): Promise<T> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: formData,\n signal: controller.signal,\n });\n\n const responseBody = await this.parseResponse(response);\n\n if (!response.ok) {\n this.throwHttpError(response.status, responseBody);\n }\n\n return this.extractData<T>(responseBody);\n } catch (error) {\n if (error instanceof SpacelrError) throw error;\n\n if (error instanceof DOMException && error.name === 'AbortError') {\n throw new SpacelrTimeoutError(timeoutMs);\n }\n\n throw new SpacelrNetworkError(\n error instanceof Error ? error.message : 'Network request failed'\n );\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n private uploadFormWithProgress<T>(\n url: string,\n headers: Record<string, string>,\n formData: FormData,\n onProgress: (event: { loaded: number; total: number }) => void,\n timeoutMs: number,\n ): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open('POST', url);\n xhr.timeout = timeoutMs;\n\n for (const [key, value] of Object.entries(headers)) {\n xhr.setRequestHeader(key, value);\n }\n\n xhr.upload.addEventListener('progress', (e) => {\n if (e.lengthComputable) {\n onProgress({ loaded: e.loaded, total: e.total });\n }\n });\n\n xhr.addEventListener('load', () => {\n try {\n const contentType = xhr.getResponseHeader('content-type') ?? '';\n if (!contentType.includes('application/json')) {\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve({ success: true, data: xhr.responseText } as unknown as T);\n } else if (xhr.status === 401 || xhr.status === 403) {\n // Surface as SpacelrAuthError for consistent typing with the\n // JSON path below. Only 401 actually triggers the upload retry\n // wrapper (uploadFormWithRetry filters on statusCode === 401);\n // 403 propagates unchanged as a permission denial.\n reject(new SpacelrAuthError(`HTTP ${xhr.status}`, xhr.status));\n } else {\n reject(new SpacelrNetworkError(`HTTP ${xhr.status}: ${xhr.responseText.slice(0, 200)}`));\n }\n return;\n }\n const body = JSON.parse(xhr.responseText);\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve(this.extractData<T>(body));\n } else {\n this.throwHttpError(xhr.status, body);\n }\n } catch (error) {\n if (error instanceof SpacelrError) {\n reject(error);\n } else {\n reject(new SpacelrNetworkError('Failed to parse response'));\n }\n }\n });\n\n xhr.addEventListener('error', () => {\n reject(new SpacelrNetworkError('Network request failed'));\n });\n\n xhr.addEventListener('timeout', () => {\n xhr.abort();\n reject(new SpacelrTimeoutError(timeoutMs));\n });\n\n xhr.send(formData);\n });\n }\n\n private buildUrl(\n path: string,\n query?: Record<string, string | number | undefined>\n ): string {\n const cleanPath = path.startsWith('/') ? path : `/${path}`;\n // .well-known endpoints are served at the origin without the API prefix\n const base = cleanPath.startsWith('/.well-known')\n ? new URL(this.config.apiUrl).origin\n : this.config.apiUrl.replace(/\\/+$/, '');\n const url = new URL(`${base}${cleanPath}`);\n\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined) {\n url.searchParams.set(key, String(value));\n }\n }\n }\n\n return url.toString();\n }\n\n private async buildHeaders(\n options: HttpRequestOptions\n ): Promise<Record<string, string>> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'x-client-id': this.config.clientId,\n 'x-project-id': this.config.projectId,\n ...options.headers,\n };\n\n if (options.authenticated) {\n const token = await this.tokenManager.getAccessToken();\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n }\n\n return headers;\n }\n\n private async buildFormHeaders(): Promise<Record<string, string>> {\n const headers: Record<string, string> = {\n 'x-client-id': this.config.clientId,\n 'x-project-id': this.config.projectId,\n };\n\n const token = await this.tokenManager.getAccessToken();\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n\n return headers;\n }\n\n private async parseResponse(\n response: Response\n ): Promise<ApiResponse | Record<string, unknown>> {\n const contentType = response.headers.get('content-type') ?? '';\n if (contentType.includes('application/json')) {\n return response.json();\n }\n const text = await response.text();\n return { success: response.ok, data: text };\n }\n\n private throwHttpError(\n statusCode: number,\n body: ApiResponse | Record<string, unknown>\n ): never {\n const apiBody = body as ApiResponse;\n const message =\n apiBody.error?.message ?? `HTTP ${statusCode}`;\n const code = apiBody.error?.code ?? `HTTP_${statusCode}`;\n const details = apiBody.error?.details;\n\n // #165: Nest BadRequestException with object body produces a flat\n // envelope `{ statusCode, message, errorCode, ... }`. Surface\n // SEARCH_FILTER_REQUIRED as a typed error so callers can discriminate\n // via `instanceof` instead of message-string matching.\n //\n // The `statusCode === 400` guard prevents an adversarial proxy or\n // misrouted response from masking a 401/403 by smuggling\n // `errorCode: 'SEARCH_FILTER_REQUIRED'` into the body — keep the\n // 401/403 → SpacelrAuthError path authoritative.\n const flatBody = body as Record<string, unknown>;\n if (statusCode === 400 && flatBody['errorCode'] === 'SEARCH_FILTER_REQUIRED') {\n const flatMessage =\n typeof flatBody['message'] === 'string'\n ? (flatBody['message'] as string)\n : message;\n const missingFields = Array.isArray(flatBody['missingFields'])\n ? (flatBody['missingFields'] as unknown[])\n : [];\n throw new SpacelrSearchFilterRequiredError(flatMessage, { missingFields });\n }\n\n if (statusCode === 401 || statusCode === 403) {\n throw new SpacelrAuthError(message, statusCode, details);\n }\n\n throw new SpacelrError(message, code, statusCode, details);\n }\n\n private extractData<T>(body: ApiResponse | Record<string, unknown>): T {\n const apiBody = body as ApiResponse;\n // If the response wraps data in a standard envelope, extract it\n if ('success' in apiBody && apiBody.data !== undefined) {\n const data = apiBody.data as Record<string, unknown>;\n // Check if the response indicates email verification is required\n if (data['emailVerificationRequired'] === true) {\n throw new SpacelrEmailVerificationRequiredError(data['emailSent'] === true);\n }\n // Check if the response indicates a 2FA challenge\n if (data['twoFactorRequired'] === true && typeof data['twoFactorToken'] === 'string') {\n throw new SpacelrTwoFactorRequiredError(data['twoFactorToken']);\n }\n return apiBody.data as T;\n }\n // Check unwrapped responses\n const rawBody = body as Record<string, unknown>;\n if (rawBody['emailVerificationRequired'] === true) {\n throw new SpacelrEmailVerificationRequiredError(rawBody['emailSent'] === true);\n }\n if (rawBody['twoFactorRequired'] === true && typeof rawBody['twoFactorToken'] === 'string') {\n throw new SpacelrTwoFactorRequiredError(rawBody['twoFactorToken']);\n }\n // Otherwise return the whole body (e.g. OIDC endpoints return raw objects)\n return body as T;\n }\n}\n","import { StoredTokens, TokenStorage } from '../types';\n\nexport type { TokenStorage };\n\nexport class MemoryTokenStorage implements TokenStorage {\n private tokens: StoredTokens | null = null;\n\n async getTokens(): Promise<StoredTokens | null> {\n return this.tokens;\n }\n\n async setTokens(tokens: StoredTokens): Promise<void> {\n this.tokens = tokens;\n }\n\n async clearTokens(): Promise<void> {\n this.tokens = null;\n }\n}\n\nexport class BrowserTokenStorage implements TokenStorage {\n private readonly storageKey: string;\n\n constructor(storageKey = 'spacelr_tokens') {\n this.storageKey = storageKey;\n }\n\n async getTokens(): Promise<StoredTokens | null> {\n try {\n const raw = localStorage.getItem(this.storageKey);\n if (!raw) return null;\n return JSON.parse(raw) as StoredTokens;\n } catch {\n return null;\n }\n }\n\n async setTokens(tokens: StoredTokens): Promise<void> {\n try {\n localStorage.setItem(this.storageKey, JSON.stringify(tokens));\n } catch {\n // Quota exceeded or private browsing — silently ignore\n }\n }\n\n async clearTokens(): Promise<void> {\n try {\n localStorage.removeItem(this.storageKey);\n } catch {\n // localStorage unavailable — silently ignore\n }\n }\n}\n","import { StoredTokens, TokenStorage } from '../types';\nimport { MemoryTokenStorage } from './token-storage';\n\nexport type RefreshCallback = (refreshToken: string) => Promise<StoredTokens>;\nexport type AuthLostReason = 'refresh-failed' | 'unauthorized';\nexport type TokenRefreshedListener = (tokens: StoredTokens) => void;\nexport type AuthLostListener = (reason: AuthLostReason) => void;\n\nexport class TokenManager {\n private storage: TokenStorage;\n private refreshBufferSeconds: number;\n private refreshCallback: RefreshCallback | null = null;\n private refreshPromise: Promise<StoredTokens> | null = null;\n private tokenRefreshedListeners = new Set<TokenRefreshedListener>();\n private authLostListeners = new Set<AuthLostListener>();\n // Guard so callbacks that run during auth-loss handling (e.g. a logout()\n // that hits `/auth/logout` with a dead token) don't re-emit and loop.\n // Cleared by setTokens() / clearTokens() to re-arm for the next session.\n private authLostEmitted = false;\n\n constructor(storage?: TokenStorage, refreshBufferSeconds = 60) {\n this.storage = storage ?? new MemoryTokenStorage();\n this.refreshBufferSeconds = refreshBufferSeconds;\n }\n\n setRefreshCallback(callback: RefreshCallback): void {\n this.refreshCallback = callback;\n }\n\n async getAccessToken(): Promise<string | null> {\n const tokens = await this.storage.getTokens();\n if (!tokens) return null;\n\n if (this.isTokenExpired(tokens)) {\n const refreshed = await this.tryRefresh(tokens);\n return refreshed?.accessToken ?? null;\n }\n\n if (this.shouldRefresh(tokens)) {\n // Trigger background refresh but return current token\n this.tryRefresh(tokens).catch(() => {\n // Ignore background refresh failures (onAuthLost will have fired)\n });\n }\n\n return tokens.accessToken;\n }\n\n async setTokens(tokens: StoredTokens): Promise<void> {\n // Defensive default: when a refreshable session comes in without an\n // explicit expiry (typical of bridge code that forgot to thread\n // `expires_in`), treat the access-token as already due. The next\n // `getAccessToken()` then takes the BLOCKING-refresh branch and the\n // very next authenticated request hits the gateway with a fresh\n // token. Worst case: one extra refresh round-trip — vs. silently\n // letting an unrefreshable session run forever client-side until a\n // 401 surfaces the expiry.\n //\n // Skip the default when no refreshToken is present: there's nothing\n // to refresh with, so forcing an \"expired\" state would just produce\n // useless `null` returns from getAccessToken. This also keeps\n // ephemeral test fixtures working.\n //\n // Use the same falsy-guard as tryRefresh (`!tokens.refreshToken`) so a\n // `refreshToken: ''` (some servers return an empty string when refresh\n // is unavailable for this session) doesn't trip the default and brick\n // an otherwise-valid ephemeral session by routing it into a refresh\n // path that immediately bails on the empty-string check.\n const needsDefault =\n tokens.expiresAt === undefined && !!tokens.refreshToken;\n const normalised: StoredTokens = needsDefault\n ? { ...tokens, expiresAt: Math.floor(Date.now() / 1000) }\n : tokens;\n await this.storage.setTokens(normalised);\n this.authLostEmitted = false;\n }\n\n async clearTokens(): Promise<void> {\n await this.storage.clearTokens();\n this.refreshPromise = null;\n this.authLostEmitted = false;\n }\n\n async getStoredTokens(): Promise<StoredTokens | null> {\n return this.storage.getTokens();\n }\n\n /**\n * Force a refresh using the current stored refresh token.\n * Returns the new access token, or null if refresh is not possible or fails.\n * A rejecting custom `TokenStorage.getTokens()` is treated as \"no tokens\"\n * so callers (notably `HttpClient`'s 401 retry path) see a clean `null`\n * rather than an exception that would get wrapped as a network error.\n */\n async forceRefresh(): Promise<string | null> {\n // Once auth is lost, further refresh attempts are pointless and would\n // spam the refresh endpoint while the consumer is handling the logout.\n if (this.authLostEmitted) return null;\n let tokens: StoredTokens | null;\n try {\n tokens = await this.storage.getTokens();\n } catch {\n return null;\n }\n if (!tokens) return null;\n const refreshed = await this.tryRefresh(tokens);\n return refreshed?.accessToken ?? null;\n }\n\n onTokenRefreshed(listener: TokenRefreshedListener): () => void {\n this.tokenRefreshedListeners.add(listener);\n return () => this.tokenRefreshedListeners.delete(listener);\n }\n\n onAuthLost(listener: AuthLostListener): () => void {\n this.authLostListeners.add(listener);\n return () => this.authLostListeners.delete(listener);\n }\n\n emitAuthLost(reason: AuthLostReason): void {\n if (this.authLostEmitted) return;\n this.authLostEmitted = true;\n for (const listener of this.authLostListeners) {\n try {\n listener(reason);\n } catch {\n // Ignore listener errors\n }\n }\n }\n\n private isTokenExpired(tokens: StoredTokens): boolean {\n if (!tokens.expiresAt) return false;\n return Date.now() >= tokens.expiresAt * 1000;\n }\n\n private shouldRefresh(tokens: StoredTokens): boolean {\n if (!tokens.expiresAt || !tokens.refreshToken) return false;\n const bufferMs = this.refreshBufferSeconds * 1000;\n return Date.now() >= tokens.expiresAt * 1000 - bufferMs;\n }\n\n private async tryRefresh(\n tokens: StoredTokens\n ): Promise<StoredTokens | null> {\n if (!tokens.refreshToken || !this.refreshCallback) return null;\n\n // Deduplicate concurrent refresh calls — both the leader and any followers\n // must get `null` on failure, not a rejected promise. Otherwise parallel\n // callers of getAccessToken/forceRefresh will throw unexpectedly while the\n // leader gracefully returns null.\n if (this.refreshPromise) {\n try {\n return await this.refreshPromise;\n } catch {\n return null;\n }\n }\n\n this.refreshPromise = this.executeRefresh(tokens.refreshToken);\n\n try {\n return await this.refreshPromise;\n } catch {\n return null;\n } finally {\n this.refreshPromise = null;\n }\n }\n\n private async executeRefresh(refreshToken: string): Promise<StoredTokens> {\n // `tryRefresh` guards against a missing callback before reaching here.\n const callback = this.refreshCallback as RefreshCallback;\n try {\n const newTokens = await callback(refreshToken);\n await this.storage.setTokens(newTokens);\n this.emitTokenRefreshed(newTokens);\n return newTokens;\n } catch (error) {\n this.emitAuthLost('refresh-failed');\n throw error;\n }\n }\n\n private emitTokenRefreshed(tokens: StoredTokens): void {\n for (const listener of this.tokenRefreshedListeners) {\n try {\n listener(tokens);\n } catch {\n // Ignore listener errors\n }\n }\n }\n}\n","export enum FileVisibility {\n PRIVATE = 'private',\n SHARED = 'shared',\n PUBLIC = 'public',\n}\n\nexport enum GrantType {\n AUTHORIZATION_CODE = 'authorization_code',\n CLIENT_CREDENTIALS = 'client_credentials',\n REFRESH_TOKEN = 'refresh_token',\n}\n\nexport enum CodeChallengeMethod {\n PLAIN = 'plain',\n S256 = 'S256',\n}\n\nexport enum SharePermission {\n READ = 'read',\n WRITE = 'write',\n}\n\nexport interface ApiResponse<T = unknown> {\n success: boolean;\n data?: T;\n error?: {\n message: string;\n code: string;\n details?: Record<string, unknown>;\n };\n}\n","import { PKCEChallenge, CodeChallengeMethod } from '../types';\n\nfunction generateRandomBytes(length: number): Uint8Array {\n if (\n typeof globalThis.crypto !== 'undefined' &&\n globalThis.crypto.getRandomValues\n ) {\n const bytes = new Uint8Array(length);\n globalThis.crypto.getRandomValues(bytes);\n return bytes;\n }\n\n // Node.js fallback\n const nodeCrypto = require('crypto') as typeof import('crypto');\n return new Uint8Array(nodeCrypto.randomBytes(length));\n}\n\nfunction base64UrlEncode(buffer: Uint8Array): string {\n let binary = '';\n for (let i = 0; i < buffer.length; i++) {\n binary += String.fromCharCode(buffer[i]);\n }\n\n const base64 = btoa(binary);\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nasync function sha256(input: string): Promise<Uint8Array> {\n if (\n typeof globalThis.crypto !== 'undefined' &&\n globalThis.crypto.subtle\n ) {\n const encoder = new TextEncoder();\n const data = encoder.encode(input);\n const hash = await globalThis.crypto.subtle.digest('SHA-256', data);\n return new Uint8Array(hash);\n }\n\n // Node.js fallback\n const nodeCrypto = require('crypto') as typeof import('crypto');\n const hash = nodeCrypto.createHash('sha256').update(input).digest();\n return new Uint8Array(hash);\n}\n\nexport async function generatePKCEChallenge(): Promise<PKCEChallenge> {\n const randomBytes = generateRandomBytes(32);\n const codeVerifier = base64UrlEncode(randomBytes);\n\n const hashBytes = await sha256(codeVerifier);\n const codeChallenge = base64UrlEncode(hashBytes);\n\n return {\n codeVerifier,\n codeChallenge,\n codeChallengeMethod: CodeChallengeMethod.S256,\n };\n}\n","import { io, Socket } from 'socket.io-client';\nimport { SpacelrError } from './errors';\n\n// Wait this long before retrying a failed socket rebuild. Short enough that\n// a transient network stumble heals without waiting for a user-visible wake\n// event, long enough not to hammer an unreachable server.\nconst REBUILD_RETRY_DELAY_MS = 5000;\n\n/**\n * Live (post-construction) handle to socket.io-client's Manager. The `reconnection`,\n * `reconnectionDelay`, `reconnectionDelayMax`, and `reconnectionAttempts` setters\n * mutate the Manager's private `_reconnection` / backoff state — `socket.io.opts.*`\n * mutations are inert post-construction (manager.js:52,72-79). `skipReconnect` is\n * a private flag the constructor sets to `true` when `reconnection: false` is\n * passed; the `reconnection(true)` setter does NOT clear it, so we have to.\n */\ninterface ManagerReconnectionAPI {\n reconnection: (v: boolean) => void;\n reconnectionDelay: (v: number) => void;\n reconnectionDelayMax: (v: number) => void;\n reconnectionAttempts: (v: number) => void;\n skipReconnect: boolean;\n}\n\n/** Ack error codes that indicate a permanent server-side rejection — evict\n * the subscription on reconnect rather than retrying forever. Transient\n * codes (redis-unavailable, replay-error, disconnected) stay in the map so\n * the next reconnect tries again. */\nconst PERMANENT_STREAM_ACK_ERRORS = new Set<string>([\n 'not-stream-collection',\n 'Collection not found',\n 'Invalid sinceId format',\n 'Not a member of this project',\n 'Subscribe denied',\n]);\n\nexport type ConnectionState = 'connected' | 'reconnecting' | 'disconnected';\n\n// NOTE: This is the public, client-facing event shape. It intentionally\n// diverges from `DatabaseChangeEvent` in\n// @spacelr-workspace/shared-types/database-events.ts, which still carries\n// the `_source`, `_sourceFunctionId`, and `_chainDepth` orchestration\n// fields used by function-service for loop prevention. The api-gateway\n// strips those fields before forwarding to SDK clients (see\n// `toPublicEvent` in apps/api-gateway-public/.../database-events.gateway.ts),\n// so the SDK type accurately reflects what subscribers actually receive.\nexport interface DatabaseChangeEvent {\n type: 'insert' | 'update' | 'delete';\n projectId: string;\n collectionName: string;\n documentId: string;\n document?: Record<string, unknown>;\n timestamp: number;\n /**\n * Redis Stream entry ID (e.g. \"1745234123-0\") for stream-mode events, or a\n * synthesised `${projectId}:${collectionName}:${timestamp}-${documentId}`\n * for pubsub-mode events. Used by function triggers as an idempotency key.\n */\n eventId?: string;\n}\n\nexport interface RealtimeConfig {\n baseUrl: string;\n getToken: () => Promise<string | null>;\n /**\n * Optional subscription to token-refresh events. When wired, the realtime\n * client will push the fresh access token to the server via a\n * `reauthenticate` message so the open socket stays authenticated without\n * dropping the connection.\n */\n onTokenRefreshed?: (listener: (accessToken: string) => void) => () => void;\n}\n\nexport type GapReason =\n | 'outside-retention-window'\n | 'redis-unavailable'\n | 'replay-error'\n | 'replay-truncated';\n\nexport interface StreamGapInfo {\n projectId: string;\n collectionName: string;\n /** Configured retention as a human-readable string (e.g. \"maxLen: 10000\", \"24h\", or \"unknown\"). */\n retentionWindow: string;\n reason: GapReason;\n}\n\nexport interface StreamSubscriptionOptions {\n projectId: string;\n collectionName: string;\n sinceId?: string;\n where?: Record<string, string | number | boolean>;\n /**\n * Called for each delivered event. Awaited — the cursor does NOT advance\n * until this resolves. If the callback throws, the cursor is NOT advanced\n * and `onError` is called; the event is not retried (at-most-once on\n * handler failure, consistent with the server-side StreamReaderService).\n */\n onEvent: (entry: { eventId: string; event: DatabaseChangeEvent }) => void | Promise<void>;\n /** Called when the server emits an event-gap. See GapReason for the four codes. */\n onGap?: (info: StreamGapInfo) => void;\n /** Called on handshake rejection (not-stream-collection, invalid sinceId, etc.) and handler failures. */\n onError?: (error: Error) => void;\n}\n\ninterface StreamSubscriptionState {\n options: StreamSubscriptionOptions;\n /** Last eventId successfully delivered to onEvent. Advances after each resolved handler. */\n lastDeliveredId: string | null;\n /** `stream:<projectId>:<collectionName>` — used as the Map key. */\n streamKey: string;\n /** Serializes onEvent calls for THIS subscriber so cursor advances in order\n * even when the socket delivers multiple events before the previous handler resolves. */\n dispatchQueue: Promise<void>;\n}\n\ninterface StreamSubscribeAck {\n error?: string;\n message?: string;\n subscribed?: string;\n lastDeliveredId?: string | null;\n // NEW for #99: server returns these on every subscribe-events ack\n // so the SDK can fail-fast on pubsub collections and anchor the\n // snapshot baseline atomically with the reader-attach. Both fields\n // are optional so older servers that don't yet send them don't\n // break the SDK build — the metadata-aware wrapper defaults to\n // pubsub mode so the fail-fast still fires.\n realtimeMode?: 'pubsub' | 'stream';\n latestStreamId?: string | null;\n}\n\nexport class RealtimeClient {\n private socket: Socket | null = null;\n private config: RealtimeConfig;\n private subscriptions = new Map<string, Set<(event: DatabaseChangeEvent) => void>>();\n private connecting: Promise<void> | null = null;\n // Store original where objects per room for reconnect resubscription\n private roomWhereMap = new Map<string, Record<string, string | number | boolean>>();\n private unsubscribeFromTokenRefreshed: (() => void) | null = null;\n // Wake-up listeners (browser only) — recover from long OS suspends where\n // socket.io's internal reconnect loop may have already given up.\n private onVisibilityChange: (() => void) | null = null;\n private onOnline: (() => void) | null = null;\n // Set by `disconnect()`; checked by async paths (rebuildSocket, deferred\n // retries) to avoid reviving a client the consumer has torn down.\n private disposed = false;\n // Scheduled retry after a failed `rebuildSocket()`; cleared when it fires\n // or when `disconnect()` tears down.\n private rebuildRetryTimer: ReturnType<typeof setTimeout> | null = null;\n private connectionState: ConnectionState = 'disconnected';\n private connectionStateListeners = new Set<(state: ConnectionState) => void>();\n private streamSubscriptions = new Map<string, Set<StreamSubscriptionState>>();\n\n constructor(config: RealtimeConfig) {\n this.config = config;\n }\n\n async subscribe(\n projectId: string,\n collectionName: string,\n callback: (event: DatabaseChangeEvent) => void,\n onError?: (error: Error) => void,\n where?: Record<string, string | number | boolean>,\n ): Promise<() => void> {\n this.ensureWakeListeners();\n if (this.connectionState === 'disconnected') {\n this.setConnectionState('reconnecting');\n }\n await this.ensureConnected();\n\n const room = this.buildRoomKey(projectId, collectionName, where);\n\n // Track the callback\n if (!this.subscriptions.has(room)) {\n this.subscriptions.set(room, new Set());\n }\n const callbacks = this.subscriptions.get(room);\n callbacks?.add(callback);\n\n // If this is the first subscription for this room, tell the server\n if (callbacks?.size === 1) {\n if (where && Object.keys(where).length > 0) {\n this.roomWhereMap.set(room, where);\n }\n const payload: Record<string, unknown> = { projectId, collectionName };\n if (where && Object.keys(where).length > 0) {\n payload['where'] = where;\n }\n this.socket?.emit(\n 'subscribe',\n payload,\n (response: { subscribed?: string; error?: string }) => {\n if (response.error && onError) {\n onError(new Error(response.error));\n }\n },\n );\n }\n\n // Return unsubscribe function\n return () => {\n const callbacks = this.subscriptions.get(room);\n if (callbacks) {\n callbacks.delete(callback);\n if (callbacks.size === 0) {\n this.subscriptions.delete(room);\n this.roomWhereMap.delete(room);\n const payload: Record<string, unknown> = { projectId, collectionName };\n if (where && Object.keys(where).length > 0) {\n payload['where'] = where;\n }\n this.socket?.emit('unsubscribe', payload);\n }\n }\n };\n }\n\n /**\n * Subscribe to a stream-mode collection using Redis Streams replay +\n * cursor-based delivery. Parallel to `subscribe()` (which targets\n * pubsub-mode collections). The server validates that the collection is\n * configured in stream mode and rejects the handshake otherwise.\n *\n * The per-subscription cursor (`lastDeliveredId`) advances after each\n * `onEvent` promise resolves, so reconnects resume from the last delivered\n * event rather than the originally-subscribed `sinceId`.\n */\n async subscribeWithCursor(\n options: StreamSubscriptionOptions,\n ): Promise<() => void> {\n const { unsubscribe } = await this._subscribeInternal(options);\n return unsubscribe;\n }\n\n /**\n * Metadata-aware variant of `subscribeWithCursor()` — see #99.\n *\n * Identical to `subscribeWithCursor` except it also returns the\n * `realtimeMode` and `latestStreamId` from the server's\n * subscribe-events ack so callers (notably `subscribeWithSnapshot`)\n * can fail-fast on pubsub-mode collections and anchor the snapshot\n * baseline atomically with the reader-attach.\n *\n * Defaults: if the server didn't send `realtimeMode` (older\n * gateway), the SDK treats the collection as pubsub-mode so\n * downstream fail-fasts still fire correctly. `latestStreamId`\n * defaults to `null` when absent.\n *\n * Dedup-path note: when a sibling local subscription already holds\n * the server-side reader slot for this streamKey, no new handshake\n * is emitted — the SDK piggy-backs on the existing slot. In that\n * case we synthesise `{ realtimeMode: 'stream', latestStreamId: null }`\n * because the existing slot's prior ack is no longer available.\n * Callers that need a fresh `latestStreamId` (e.g. snapshot anchor)\n * MUST handle the `null` case.\n */\n async subscribeWithCursorWithMeta(\n options: StreamSubscriptionOptions,\n ): Promise<{\n unsubscribe: () => void;\n realtimeMode: 'pubsub' | 'stream';\n latestStreamId: string | null;\n }> {\n const { unsubscribe, ack } = await this._subscribeInternal(options);\n return {\n unsubscribe,\n // Default to pubsub if the server didn't send the field — older\n // gateways that haven't been updated yet are treated as pubsub-mode\n // so the SDK helper's fail-fast still fires correctly.\n realtimeMode: ack.realtimeMode ?? 'pubsub',\n latestStreamId: ack.latestStreamId ?? null,\n };\n }\n\n /**\n * Shared body for `subscribeWithCursor` and\n * `subscribeWithCursorWithMeta`. Owns the handshake, dedup path,\n * pre-ack registration, and error eviction. Returns the unsubscribe\n * function plus the subscribe-events ack so metadata-aware callers\n * can read `realtimeMode` / `latestStreamId`.\n *\n * Behaviour-preserving contract — DO NOT change without re-running\n * the singleflight tests in `realtime.spec.ts`:\n *\n * 1. Dedup branch BEFORE registration: if a sibling subscriber for\n * this streamKey is already registered, the new caller piggy-backs\n * on its server-side slot. Re-emitting the handshake here would\n * tear the prior subscriber's reader down.\n * 2. Registration BEFORE handshake: `streamSubscriptions.set` must\n * fire before `emitSubscribeEvents` so replay/event-gap frames\n * delivered DURING the handshake fanout find this subscriber.\n * 3. evictStreamKey on ack-error MUST sweep dedup-path siblings:\n * they have no independent server-side state and would silently\n * miss every event until the next full reconnect.\n * 4. The catch block is a safety net for the re-thrown ack-error\n * only — `emitSubscribeEvents` itself always resolves, so this\n * catch should never fire on the success path.\n */\n private async _subscribeInternal(\n options: StreamSubscriptionOptions,\n ): Promise<{ unsubscribe: () => void; ack: StreamSubscribeAck }> {\n this.ensureWakeListeners();\n if (this.connectionState === 'disconnected') {\n this.setConnectionState('reconnecting');\n }\n await this.ensureConnected();\n\n const streamKey = `stream:${options.projectId}:${options.collectionName}`;\n const state: StreamSubscriptionState = {\n options,\n lastDeliveredId: options.sinceId ?? null,\n streamKey,\n dispatchQueue: Promise.resolve(),\n };\n\n // Dedup path: if another local subscriber already holds the server-side\n // slot for this streamKey, just piggy-back on the existing fan-out. The\n // gateway collapses duplicate `subscribe-events` per socket per streamKey\n // (replacing the prior reader), so re-emitting would tear down the other\n // subscriber's server-side state.\n const existing = this.streamSubscriptions.get(streamKey);\n if (existing && existing.size > 0) {\n existing.add(state);\n // Synthesised ack for the dedup path — there is no fresh server\n // ack to read. The existing slot was successfully established as\n // a stream-mode subscription (otherwise the prior subscribe would\n // have failed and torn the entry down), so `realtimeMode: 'stream'`\n // is safe. `latestStreamId` is null because we don't have a fresh\n // server-side anchor for this caller.\n const syntheticAck: StreamSubscribeAck = {\n subscribed: streamKey,\n realtimeMode: 'stream',\n latestStreamId: null,\n };\n return {\n unsubscribe: () => this.unsubscribeStream(state),\n ack: syntheticAck,\n };\n }\n\n // Register BEFORE the handshake so replay `event` / `event-gap` frames\n // emitted during subscribe fanout find this subscriber. If the ack comes\n // back with an error, we tear down the registration in the catch block.\n const set = new Set<StreamSubscriptionState>();\n set.add(state);\n this.streamSubscriptions.set(streamKey, set);\n\n try {\n const ack = await this.emitSubscribeEvents(state);\n if (ack.error) {\n // The primary handshake failed. Evict this subscriber AND any other\n // subscribers that joined via the dedup path for the same streamKey —\n // they have no active server subscription, so they would silently miss\n // events until the next full reconnect.\n //\n // Throw a SpacelrError that preserves `ack.error` as the `code`\n // property so higher layers can discriminate by ack-error code\n // (e.g. `subscribeWithSnapshot` maps `'not-stream-collection'` to\n // `SUBSCRIBE_WITH_SNAPSHOT_REQUIRES_STREAM`) without string-matching\n // on the human message.\n const message = ack.message ?? ack.error;\n const err = new SpacelrError(message, ack.error);\n this.evictStreamKey(streamKey, err, state);\n options.onError?.(err);\n throw err;\n }\n return {\n unsubscribe: () => this.unsubscribeStream(state),\n ack,\n };\n } catch (err) {\n // Safety net: emitSubscribeEvents always resolves, so this catch only\n // fires when `throw err` from the ack-error branch re-runs here. evictStreamKey\n // is idempotent — the second call is a no-op if the streamKey entry was already\n // deleted. Kept in case a future edit introduces a throw before the error branch.\n const error = err instanceof Error ? err : new Error(String(err));\n this.evictStreamKey(streamKey, error, state);\n throw error;\n }\n }\n\n /**\n * Tear down the realtime client permanently. After this call the instance\n * is disposed — subsequent `subscribe()` calls will not re-establish a\n * connection. Create a new `RealtimeClient` (via `createClient`) if you\n * need to reconnect after a logout.\n */\n disconnect(): void {\n this.setConnectionState('disconnected');\n this.disposed = true;\n if (this.rebuildRetryTimer) {\n clearTimeout(this.rebuildRetryTimer);\n this.rebuildRetryTimer = null;\n }\n if (this.unsubscribeFromTokenRefreshed) {\n this.unsubscribeFromTokenRefreshed();\n this.unsubscribeFromTokenRefreshed = null;\n }\n this.detachWakeListeners();\n if (this.socket) {\n this.socket.disconnect();\n this.socket = null;\n }\n this.subscriptions.clear();\n this.roomWhereMap.clear();\n this.streamSubscriptions.clear();\n this.connecting = null;\n }\n\n getConnectionState(): ConnectionState {\n return this.connectionState;\n }\n\n onConnectionStateChanged(listener: (state: ConnectionState) => void): () => void {\n this.connectionStateListeners.add(listener);\n return () => this.connectionStateListeners.delete(listener);\n }\n\n private setConnectionState(next: ConnectionState): void {\n if (this.connectionState === next) return;\n this.connectionState = next;\n for (const listener of this.connectionStateListeners) {\n try {\n listener(next);\n } catch {\n // Ignore listener errors, matching onTokenRefreshed / onAuthLost.\n }\n }\n }\n\n private buildRoomKey(\n projectId: string,\n collectionName: string,\n where?: Record<string, string | number | boolean>,\n ): string {\n const base = `db:${projectId}:${collectionName}`;\n if (!where || Object.keys(where).length === 0) {\n return base;\n }\n // Type-preserving encoding so `{id: 1}` and `{id: '1'}` produce distinct\n // room keys. MUST mirror the gateway's `buildFilteredRoom`\n // (apps/api-gateway-public/src/app/database/database-events.gateway.ts)\n // — any change here requires a matching server update. Refs #227.\n const filterStr = Object.keys(where)\n .sort()\n .map((k) => `${k}=${JSON.stringify(where[k])}`)\n .join('&');\n return `${base}?${filterStr}`;\n }\n\n private async ensureConnected(): Promise<void> {\n if (this.socket?.connected) return;\n\n if (this.connecting) {\n return this.connecting;\n }\n\n this.connecting = this.connect();\n try {\n await this.connecting;\n } finally {\n this.connecting = null;\n }\n }\n\n private async connect(): Promise<void> {\n const token = await this.config.getToken();\n if (!token) {\n throw new Error('No authentication token available');\n }\n\n // Derive WebSocket URL from the API URL\n const wsUrl = this.config.baseUrl.replace(/\\/api\\/v\\d+\\/?$/, '');\n\n return new Promise<void>((resolve, reject) => {\n let initialConnect = true;\n // Forward-declared so the `authenticated` handler can deregister it\n // explicitly after first auth — closure-retained `reject` references\n // shouldn't outlive the handshake window. socket.io's `once` would\n // self-remove on first fire, but we'd still pay one no-op fire on the\n // first post-auth disconnect; matching the explicit-`off()` pattern\n // used by `emitSubscribeEvents` keeps the handshake state machine\n // tidy.\n let onInitialDisconnect: ((reason: Socket.DisconnectReason) => void) | null = null;\n\n this.socket = io(`${wsUrl}/database`, {\n auth: async (cb: (data: Record<string, unknown>) => void) => {\n try {\n const freshToken = await this.config.getToken();\n cb({ token: freshToken });\n } catch {\n cb({ token: null });\n }\n },\n transports: ['websocket'],\n // Disable auto-reconnect for the INITIAL connect — caller's promise\n // rejects on connect_error and they decide whether to retry. Once the\n // first authentication succeeds we re-enable it via the Manager's\n // setters (see authenticated handler).\n reconnection: false,\n });\n\n this.socket.on('authenticated', () => {\n // If the consumer called disconnect() while an in-flight authenticate\n // was in progress, don't emit a spurious `connected` on a disposed\n // client. socket.io can deliver a queued event after the socket was\n // detached — the `this.socket = null` and disposed flag are set but\n // the handler is still bound to the old socket instance.\n if (this.disposed) return;\n this.setConnectionState('connected');\n if (initialConnect) {\n initialConnect = false;\n // Enable reconnection AFTER the first successful auth.\n //\n // socket.io-client v4 reads `opts.reconnection` ONLY in the Manager\n // constructor (manager.js:52). Mutating `socket.io.opts.reconnection`\n // post-construction is a no-op — the live setting lives in the\n // private `_reconnection` field, mutable only via the `reconnection(v)`\n // setter (manager.js:72-79). And because `reconnection: false` was\n // passed at construction, the constructor also set `skipReconnect = true`\n // (manager.js:76-78), which the `reconnection(true)` setter does NOT\n // reset — we have to clear it ourselves. Without this, every WS drop\n // (Wi-Fi blip, server restart, NAT timeout) leaves the SDK\n // permanently disconnected.\n if (this.socket) {\n const manager = this.socket.io as unknown as ManagerReconnectionAPI;\n // Defensive: if socket.io-client ever bumps its internal API,\n // any of these setters could be missing (or `skipReconnect`\n // renamed). Wrap the whole sequence in try/catch and `reject()`\n // on failure — a bare throw here would propagate as an unhandled\n // exception in socket.io's dispatch loop (we're inside an event\n // handler, NOT the Promise executor body), leaving connect()\n // hanging forever. Same failure mode the once('disconnect')\n // guard prevents, and that guard wouldn't catch this either —\n // `initialConnect` is already cleared by this point.\n try {\n manager.reconnection(true);\n manager.skipReconnect = false;\n manager.reconnectionDelay(1000);\n manager.reconnectionDelayMax(5000);\n manager.reconnectionAttempts(50);\n } catch (err) {\n // Drop the handshake-disconnect handler before rejecting so the\n // closure retaining `reject` doesn't linger on the socket\n // emitter. The `if (initialConnect)` guard inside the handler\n // already makes it a no-op (initialConnect is false here), but\n // explicit cleanup keeps the symmetry with the success path.\n if (onInitialDisconnect && this.socket) {\n this.socket.off('disconnect', onInitialDisconnect);\n onInitialDisconnect = null;\n }\n // Tear the socket down. We already flipped state to 'connected'\n // before entering the setter block, so listeners saw a brief\n // 'connected' event — undo that with an explicit\n // 'disconnected' transition. Without this, a subsequent\n // `subscribe()` would see `this.socket?.connected === true`,\n // skip `ensureConnected()`, and keep using a socket that can\n // never reconnect on drop. (Exceptional path: only fires if\n // socket.io-client drops its internal Manager API.)\n this.socket?.disconnect();\n this.socket = null;\n this.setConnectionState('disconnected');\n reject(\n new Error(\n 'socket.io-client Manager reconnection setters failed — internal API may have changed; ' +\n 'auto-reconnect cannot be enabled. Check socket.io-client version compatibility. ' +\n `Underlying error: ${err instanceof Error ? err.message : String(err)}`,\n ),\n );\n return;\n }\n }\n // Initial handshake is past — drop the `disconnect` rejection\n // handler so its closure stops retaining `reject` (and so we don't\n // pay one no-op fire on the next live-session drop).\n if (onInitialDisconnect && this.socket) {\n this.socket.off('disconnect', onInitialDisconnect);\n onInitialDisconnect = null;\n }\n // Wire up token-refresh → reauthenticate so the open socket picks up\n // new JWTs without disconnecting.\n if (this.config.onTokenRefreshed && !this.unsubscribeFromTokenRefreshed) {\n this.unsubscribeFromTokenRefreshed = this.config.onTokenRefreshed(\n (accessToken) => {\n this.socket?.emit('reauthenticate', { token: accessToken });\n },\n );\n }\n resolve();\n } else {\n // Reconnect path — server has now set socket.data.userId. It is\n // safe to re-subscribe. (Doing this on 'connect' races the async\n // handleConnection and produces silent \"Not authenticated\" errors.)\n this.resubscribeAll();\n void this.resubscribeAllStreams();\n }\n });\n\n this.socket.on('connect_error', (err) => {\n if (initialConnect) {\n reject(new Error(`WebSocket connection failed: ${err.message}`));\n }\n });\n\n // Mid-handshake disconnect: the transport connected (TCP up) but the\n // socket dropped before `authenticated` fired (Wi-Fi blip during the\n // server-side auth handshake, server-side socket.disconnect(), etc.).\n // Without this guard, neither `authenticated` nor `connect_error` ever\n // runs and the caller's promise hangs forever. With `reconnection: false`\n // for the initial connect there's also no auto-retry to recover, so we\n // have to surface the failure ourselves. The handler is deregistered\n // by the `authenticated` branch above once the handshake completes;\n // live-session drops flow through the second `disconnect` listener\n // below as the start of an auto-reconnect.\n onInitialDisconnect = (reason: Socket.DisconnectReason): void => {\n if (!initialConnect) return;\n // Mirror the setter-failure cleanup: tear down the dead socket and\n // flip state to 'disconnected' so getConnectionState() reflects\n // reality. `subscribe()` set state to 'reconnecting' when this\n // attempt began; without this transition, listeners would see\n // 'reconnecting' indefinitely after the connect() promise rejects.\n //\n // We deliberately do NOT clear `initialConnect` here. The permanent\n // `disconnect` handler below skips the 'reconnecting' transition\n // while `initialConnect` is true; flipping it false would defeat\n // that guard and cause a spurious 'reconnecting' event after our\n // 'disconnected' transition. The success path's\n // `initialConnect = false` runs only on `authenticated`, never\n // here — there's no second initial-handshake to gate.\n this.socket?.disconnect();\n this.socket = null;\n this.setConnectionState('disconnected');\n reject(new Error(`WebSocket disconnected during handshake: ${reason}`));\n };\n this.socket.on('disconnect', onInitialDisconnect);\n\n // socket.io has exhausted its reconnection attempts (e.g. after a long\n // laptop suspend where all retries burned in a few seconds on resume).\n // Build a fresh socket so the session can recover.\n this.socket.io.on('reconnect_failed', () => {\n void this.rebuildSocket();\n });\n\n this.socket.on('db:event', (event: DatabaseChangeEvent) => {\n const base = `db:${event.projectId}:${event.collectionName}`;\n\n // Dispatch to whole-collection subscribers via direct Map lookup\n const baseCallbacks = this.subscriptions.get(base);\n if (baseCallbacks) {\n for (const cb of baseCallbacks) {\n try { cb(event); } catch { /* ignore callback errors */ }\n }\n }\n\n // Dispatch to matching filtered subscriptions\n for (const [room, callbacks] of this.subscriptions) {\n if (room.startsWith(`${base}?`) && this.eventMatchesRoom(event, room)) {\n for (const cb of callbacks) {\n try { cb(event); } catch { /* ignore callback errors */ }\n }\n }\n }\n });\n\n this.socket.on('event', (payload: DatabaseChangeEvent & { eventId?: string }) => {\n this.dispatchStreamEvent(payload).catch(() => undefined);\n });\n\n this.socket.on('event-gap', (info: StreamGapInfo) => {\n const streamKey = `stream:${info.projectId}:${info.collectionName}`;\n const set = this.streamSubscriptions.get(streamKey);\n if (!set) return;\n for (const state of set) {\n state.options.onGap?.(info);\n }\n });\n\n this.socket.on('disconnect', () => {\n if (this.disposed) return;\n // Suppress the 'reconnecting' transition during the initial-handshake\n // window. If the socket dropped before `authenticated`, the\n // onInitialDisconnect handler above has already rejected the\n // connect() promise — flipping state to 'reconnecting' here would\n // mislead listeners about a reconnect that never gets scheduled.\n // Live-session drops (post-auth) reach this branch with\n // initialConnect=false and DO transition to 'reconnecting' to\n // signal the auto-reconnect cycle.\n if (initialConnect) return;\n // Skip if the socket was already torn down programmatically by the\n // setter-failure catch (`this.socket = null` before `reject()`).\n // socket.io-client may emit `disconnect` asynchronously after our\n // explicit `socket.disconnect()` call returns, by which point the\n // catch block has already flipped state to 'disconnected' — we\n // mustn't undo that with a spurious 'reconnecting'. Same guard\n // applies if a future code path nulls `this.socket` while leaving\n // listeners attached.\n if (!this.socket) return;\n this.setConnectionState('reconnecting');\n });\n });\n }\n\n private eventMatchesRoom(event: DatabaseChangeEvent, room: string): boolean {\n const base = `db:${event.projectId}:${event.collectionName}`;\n if (room === base) return true;\n if (!room.startsWith(`${base}?`)) return false;\n\n // For filtered rooms, check the document against the where filter\n const where = this.roomWhereMap.get(room);\n // Drop on desync: if the room is in `subscriptions` but the\n // corresponding `roomWhereMap` entry is gone, we cannot evaluate the\n // filter. The previous `return true` default would have delivered an\n // unfiltered event to a callback that expected a filter. Refs #229.\n if (!where) return false;\n if (!event.document) return false; // Delete events without document can't match a filter\n\n // Strict equality + Array.includes mirrors the server's\n // `documentMatchesFilter` (database-events.gateway.ts) so the SDK does not\n // accept events the server already strict-filtered out. Refs #227.\n for (const [key, value] of Object.entries(where)) {\n const docValue = event.document[key];\n if (Array.isArray(docValue)) {\n if (!docValue.includes(value)) {\n return false;\n }\n } else if (docValue !== value) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * Tear down the current socket and create a fresh one. Used by both\n * `reconnect_failed` (socket.io gave up) and the visibility/online wake-up\n * listeners. Preserves the `subscriptions` map so existing rooms can be\n * replayed to the server on the new connection. If the rebuild itself\n * fails it schedules one retry after 5 s — beyond that we rely on the\n * next wake-up event (visibility/online) to try again.\n */\n private async rebuildSocket(): Promise<void> {\n if (this.disposed || this.connecting) return;\n\n const previous = this.socket;\n this.socket = null;\n if (previous) previous.disconnect();\n\n this.setConnectionState('reconnecting');\n\n try {\n await this.ensureConnected();\n this.handlePostRebuild();\n } catch {\n this.scheduleRebuildRetry();\n }\n }\n\n private handlePostRebuild(): void {\n // Capture into a local first — TS narrows `this.socket` to `null` after\n // the earlier assignment and doesn't re-widen through the await above.\n const newSocket: Socket | null = this.socket;\n if (this.disposed) {\n this.socket = null;\n newSocket?.disconnect();\n return;\n }\n // The server has no record of the subscriptions we held on the old\n // (dead) socket, so replay them here. The new socket's `authenticated`\n // handler took the `initialConnect = true` branch and doesn't resubscribe.\n if (this.subscriptions.size > 0) {\n this.resubscribeAll();\n }\n if (this.streamSubscriptions.size > 0) {\n void this.resubscribeAllStreams();\n }\n }\n\n private scheduleRebuildRetry(): void {\n // A transient rebuild failure (token fetch rejected, connect_error) —\n // retry once so recovery doesn't stall waiting for a wake event.\n if (this.disposed || this.rebuildRetryTimer) return;\n this.rebuildRetryTimer = setTimeout(() => {\n this.rebuildRetryTimer = null;\n void this.rebuildSocket();\n }, REBUILD_RETRY_DELAY_MS);\n (this.rebuildRetryTimer as unknown as { unref?: () => void }).unref?.();\n }\n\n private ensureWakeListeners(): void {\n if (typeof document === 'undefined' || typeof window === 'undefined') return;\n if (this.onVisibilityChange !== null) return;\n\n const wakeIfUnhealthy = () => {\n if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;\n if (this.socket?.connected) return;\n if (this.connecting) return;\n void this.rebuildSocket();\n };\n\n this.onVisibilityChange = wakeIfUnhealthy;\n this.onOnline = wakeIfUnhealthy;\n document.addEventListener('visibilitychange', this.onVisibilityChange);\n window.addEventListener('online', this.onOnline);\n }\n\n private detachWakeListeners(): void {\n if (typeof document !== 'undefined' && this.onVisibilityChange) {\n document.removeEventListener('visibilitychange', this.onVisibilityChange);\n }\n if (typeof window !== 'undefined' && this.onOnline) {\n window.removeEventListener('online', this.onOnline);\n }\n this.onVisibilityChange = null;\n this.onOnline = null;\n }\n\n private resubscribeAll(): void {\n for (const [room] of this.subscriptions) {\n // Parse room format: db:{projectId}:{collectionName} or db:{projectId}:{collectionName}?filter\n const queryIdx = room.indexOf('?');\n const basePart = queryIdx >= 0 ? room.substring(0, queryIdx) : room;\n const parts = basePart.split(':');\n if (parts.length >= 3) {\n const projectId = parts[1];\n const collectionName = parts.slice(2).join(':');\n const payload: Record<string, unknown> = { projectId, collectionName };\n\n // Use stored where object to preserve original types (number, boolean)\n const where = this.roomWhereMap.get(room);\n if (where) {\n payload['where'] = where;\n }\n\n this.socket?.emit(\n 'subscribe',\n payload,\n (response: { subscribed?: string; error?: string }) => {\n if (response?.error) {\n // Server rejected resubscription — clean up the stale subscription\n this.subscriptions.delete(room);\n this.roomWhereMap.delete(room);\n }\n },\n );\n }\n }\n }\n\n private emitSubscribeEvents(\n state: StreamSubscriptionState,\n ): Promise<StreamSubscribeAck> {\n return new Promise((resolve) => {\n if (!this.socket) {\n resolve({ error: 'disconnected' });\n return;\n }\n const socket = this.socket;\n\n // socket.io does not call ack callbacks when the socket disconnects mid-handshake.\n // Resolve on disconnect so the Promise can't hang.\n const onDisconnect = (): void => {\n socket.off('disconnect', onDisconnect);\n resolve({ error: 'disconnected' });\n };\n socket.once('disconnect', onDisconnect);\n\n socket.emit(\n 'subscribe-events',\n this.buildStreamPayload(state),\n (ack: StreamSubscribeAck) => {\n socket.off('disconnect', onDisconnect);\n resolve(ack);\n },\n );\n });\n }\n\n private buildStreamPayload(state: StreamSubscriptionState): Record<string, unknown> {\n const { projectId, collectionName, where } = state.options;\n const payload: Record<string, unknown> = { projectId, collectionName };\n // Use the current cursor (advanced by onEvent) so a reconnect picks up\n // from the last delivered event, not the originally-subscribed sinceId.\n if (state.lastDeliveredId) payload['sinceId'] = state.lastDeliveredId;\n if (where && Object.keys(where).length > 0) payload['where'] = where;\n return payload;\n }\n\n private unsubscribeStream(state: StreamSubscriptionState): void {\n const set = this.streamSubscriptions.get(state.streamKey);\n if (!set) return;\n set.delete(state);\n if (set.size === 0) {\n this.streamSubscriptions.delete(state.streamKey);\n // Only release the server-side reader slot when no local subscriber\n // remains for this streamKey. Avoids tearing down a shared slot when\n // one of several local consumers unsubscribes.\n this.socket?.emit('unsubscribe-events', {\n projectId: state.options.projectId,\n collectionName: state.options.collectionName,\n });\n }\n }\n\n private dispatchStreamEvent(\n payload: DatabaseChangeEvent & { eventId?: string },\n ): Promise<void> {\n if (!payload.eventId) return Promise.resolve();\n const streamKey = `stream:${payload.projectId}:${payload.collectionName}`;\n const set = this.streamSubscriptions.get(streamKey);\n if (!set) return Promise.resolve();\n\n const entryId = payload.eventId;\n // Per-subscriber queue — serializes onEvent calls within a subscriber while\n // allowing different subscribers to run in parallel. Without this, two\n // events arriving while the first handler is still awaiting could advance\n // the cursor out of order.\n for (const state of set) {\n state.dispatchQueue = state.dispatchQueue.then(async () => {\n // Guard: subscription may have been cancelled while this was queued.\n const liveSet = this.streamSubscriptions.get(state.streamKey);\n if (!liveSet?.has(state)) return;\n\n try {\n await state.options.onEvent({ eventId: entryId, event: payload });\n state.lastDeliveredId = entryId;\n } catch (err) {\n // At-most-once on handler failure — don't advance the cursor.\n // Surface via onError if provided.\n const error = err instanceof Error ? err : new Error(String(err));\n try {\n state.options.onError?.(error);\n } catch {\n // onError must not break the dispatch chain — swallow any throw.\n }\n }\n });\n }\n return Promise.resolve();\n }\n\n private async resubscribeAllStreams(): Promise<void> {\n // Re-subscribe any active stream subscriptions with the CURRENT cursor so\n // reconnects pick up exactly where we left off. Emit ONE handshake per\n // streamKey — the gateway collapses duplicate `subscribe-events` per\n // socket per streamKey, so re-emitting for every local subscriber would\n // tear down prior state. Replay events delivered over the single\n // handshake fan out to all local subscribers via dispatchStreamEvent.\n //\n // For shared-slot subscriptions, use the EARLIEST `lastDeliveredId` across\n // subscribers so none miss events they had not yet observed.\n const work: Promise<void>[] = [];\n for (const set of this.streamSubscriptions.values()) {\n const states = Array.from(set);\n if (states.length === 0) continue;\n const primary = this.pickEarliestCursorState(states);\n work.push(this.resubscribeOne(primary, set));\n }\n try {\n await Promise.all(work);\n } catch {\n // Defensive — each resubscribeOne has its own try/catch,\n // but add a belt-and-suspenders here for the void call site.\n }\n }\n\n private async resubscribeOne(\n primary: StreamSubscriptionState,\n set: Set<StreamSubscriptionState>,\n ): Promise<void> {\n try {\n const ack = await this.emitSubscribeEvents(primary);\n if (ack.error) {\n // Match the SpacelrError shape used by `_subscribeInternal` so\n // callers that discriminate by `.code` see the ack-error code on\n // reconnect rejections too (e.g. `not-stream-collection`).\n const err = new SpacelrError(ack.message ?? ack.error, ack.error);\n for (const state of [...set]) {\n state.options.onError?.(err);\n }\n if (PERMANENT_STREAM_ACK_ERRORS.has(ack.error)) {\n // Permanent rejection — don't retry on the next reconnect.\n this.streamSubscriptions.delete(primary.streamKey);\n }\n return;\n }\n\n // Happy path — ack succeeded. If the local set was fully torn down during\n // the await (last unsubscribe raced with the reconnect), release the freshly\n // acquired server-side reader slot so we don't leak it.\n if (!this.streamSubscriptions.has(primary.streamKey)) {\n this.socket?.emit('unsubscribe-events', {\n projectId: primary.options.projectId,\n collectionName: primary.options.collectionName,\n });\n }\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n for (const state of [...set]) {\n state.options.onError?.(error);\n }\n // Thrown path is exceptional (emitSubscribeEvents always resolves), treat\n // as transient and let the next reconnect retry.\n }\n }\n\n private pickEarliestCursorState(\n states: StreamSubscriptionState[],\n ): StreamSubscriptionState {\n // Stream entry IDs look like \"<ms>-<seq>\"; compare numerically so lexical\n // ordering doesn't produce wrong results once the ms digit count changes.\n const parse = (id: string | null): [number, number] => {\n // null = \"no cursor preference\" — never overrides a real cursor.\n if (!id) return [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];\n const [ms, seq = '0'] = id.split('-');\n return [Number(ms), Number(seq)];\n };\n let earliest = states[0];\n let [eMs, eSeq] = parse(earliest.lastDeliveredId);\n for (let i = 1; i < states.length; i++) {\n const [ms, seq] = parse(states[i].lastDeliveredId);\n if (ms < eMs || (ms === eMs && seq < eSeq)) {\n earliest = states[i];\n eMs = ms;\n eSeq = seq;\n }\n }\n return earliest;\n }\n\n /**\n * Evict the primary subscriber AND every sibling that joined the same\n * streamKey via the dedup path. Fires `onError` on all siblings, then\n * removes the entire streamKey entry from `streamSubscriptions`.\n */\n private evictStreamKey(\n streamKey: string,\n err: Error,\n primary: StreamSubscriptionState,\n ): void {\n const set = this.streamSubscriptions.get(streamKey);\n if (!set) return;\n for (const sibling of [...set]) {\n if (sibling !== primary) {\n sibling.options.onError?.(err);\n }\n set.delete(sibling);\n }\n this.streamSubscriptions.delete(streamKey);\n }\n}\n","/**\n * Persist the stream-mode resume cursor across app restarts and reconnects.\n *\n * Pass an implementation to `subscribeEvents({ cursorStorage })` and the SDK\n * will load the previous cursor before subscribing and save after each event\n * is handled. All methods may be sync or async — the SDK awaits the returned\n * value so React Native's AsyncStorage and other async backends work without\n * extra adapter code.\n */\nexport interface CursorStorage {\n /**\n * Return the saved cursor for this key, or `null` if none.\n *\n * An empty string is also treated as \"no cursor\" — Redis stream entry ids\n * are never empty, so an empty value is always the result of a misconfigured\n * adapter or a zero-length write. Returning `null` is preferred for clarity.\n */\n load(key: string): Promise<string | null> | string | null;\n /** Persist the cursor for this key. Called after each delivered event. */\n save(key: string, cursor: string): Promise<void> | void;\n}\n\n/**\n * In-memory cursor store. Useful for tests and short-lived scripts; state is\n * lost on process exit. Pass this to `subscribeEvents({ cursorStorage })`\n * when you want non-persistent in-memory resume behaviour within a single\n * session — omitting `cursorStorage` entirely disables persistence instead.\n */\nexport function memoryCursorStorage(): CursorStorage {\n const store = new Map<string, string>();\n return {\n load(key) {\n return store.get(key) ?? null;\n },\n save(key, cursor) {\n store.set(key, cursor);\n },\n };\n}\n\n/**\n * Browser `localStorage`-backed cursor store. Gracefully no-ops in\n * non-browser environments (Node without a `localStorage` polyfill), so\n * isomorphic code can use this factory unconditionally.\n *\n * @param prefix Key prefix applied to every stored entry. Defaults to\n * `spacelr:cursor:` so multiple SDK consumers on the same origin don't\n * collide.\n */\nexport function localStorageCursorStorage(\n prefix = 'spacelr:cursor:',\n): CursorStorage {\n // Sandboxed cross-origin iframes and browsers with storage disabled throw\n // `SecurityError` on the property *access* itself — not just on method\n // calls — so the guard has to wrap the read, not just typeof-check.\n const storage = ((): Storage | null => {\n try {\n const candidate = (globalThis as { localStorage?: Storage }).localStorage;\n return typeof candidate === 'undefined' ? null : candidate;\n } catch {\n return null;\n }\n })();\n\n return {\n load(key) {\n if (!storage) return null;\n try {\n return storage.getItem(prefix + key);\n } catch {\n return null;\n }\n },\n save(key, cursor) {\n if (!storage) return;\n try {\n storage.setItem(prefix + key, cursor);\n } catch {\n // Quota exceeded or storage disabled — silently drop; cursor\n // persistence is a best-effort resume hint, not a correctness\n // guarantee.\n }\n },\n };\n}\n","import { HttpClient, TokenManager, generatePKCEChallenge } from '../core';\nimport {\n SpacelrClientConfig,\n GrantType,\n LoginParams,\n LoginResponse,\n RegisterParams,\n RegisterResponse,\n TokenResponse,\n UserInfo,\n UserProfile,\n AuthorizationUrlParams,\n ExchangeCodeParams,\n PKCEChallenge,\n OpenIDConfiguration,\n JWKSResponse,\n TwoFactorVerifyParams,\n} from '../types';\n\nexport type AuthState = 'authenticated' | 'unauthenticated';\nexport type AuthStateListener = (state: AuthState) => void | Promise<void>;\n\nexport class AuthModule {\n private http: HttpClient;\n private tokenManager: TokenManager;\n private config: SpacelrClientConfig;\n private stateListeners = new Set<AuthStateListener>();\n private unsubscribeAuthLost!: () => void;\n private lastEmittedState: AuthState | null = null;\n\n constructor(\n http: HttpClient,\n tokenManager: TokenManager,\n config: SpacelrClientConfig\n ) {\n this.http = http;\n this.tokenManager = tokenManager;\n this.config = config;\n\n // Wire up refresh callback to avoid circular deps\n this.tokenManager.setRefreshCallback(async (refreshToken: string) => {\n const result = await this.refresh(refreshToken);\n const expiresAt = result.expires_in\n ? Math.floor(Date.now() / 1000) + result.expires_in\n : undefined;\n return {\n accessToken: result.access_token,\n refreshToken: result.refresh_token,\n expiresAt,\n };\n });\n\n // A failed refresh (triggered by 401 / refresh endpoint error) drops the\n // session — mirror that into our auth-state subscribers so UIs can react.\n // Keep the unsubscribe so `dispose()` can detach if this AuthModule is\n // ever discarded while the TokenManager lives on (shared client, HMR).\n this.unsubscribeAuthLost = this.tokenManager.onAuthLost(() =>\n this.emitState('unauthenticated'),\n );\n }\n\n /**\n * Returns true if a non-expired access token is currently in storage.\n * Does NOT make a network request — safe for route guards and other\n * hot paths that run on every navigation.\n *\n * A token within the refresh buffer (about to expire) still counts as\n * authenticated because the next protected request will auto-refresh it.\n *\n * Any error from the underlying TokenStorage (corrupt JSON, quota, etc.)\n * is treated as \"not authenticated\" rather than propagated, so route\n * guards can't be crashed by a misbehaving storage backend.\n */\n async isAuthenticated(): Promise<boolean> {\n try {\n const tokens = await this.tokenManager.getStoredTokens();\n if (!tokens?.accessToken) return false;\n if (tokens.expiresAt && tokens.expiresAt * 1000 <= Date.now()) return false;\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Subscribe to auth-state transitions. The callback fires:\n * - 'authenticated' after a successful login/register/exchange/2FA-verify\n * - 'unauthenticated' after logout or when a token refresh fails\n *\n * Only fires for transitions that happen after the subscription. If the\n * user is already logged in at subscribe time (e.g. tokens restored from\n * storage on app boot), no 'authenticated' event is emitted — call\n * `isAuthenticated()` once up-front for the initial state.\n *\n * Silent token refreshes do NOT produce an event (auth state is\n * unchanged). Subscribe to `spacelrClient.onTokenRefreshed(...)` if you\n * need to observe successful refreshes.\n *\n * Listener may return `void` or `Promise<void>`. Rejections are swallowed\n * so one broken subscriber can't poison others or the auth flow. The\n * dispatch is fire-and-forget: `logout()` / `login()` resolve as soon as\n * the dispatch loop returns, without awaiting async listeners.\n *\n * Returns an unsubscribe function.\n */\n onAuthStateChange(listener: AuthStateListener): () => void {\n this.stateListeners.add(listener);\n return () => {\n this.stateListeners.delete(listener);\n };\n }\n\n /**\n * Detach this AuthModule from the TokenManager. Call when discarding the\n * client (tests, HMR, multi-client setups) to avoid leaking the internal\n * onAuthLost subscription. Idempotent — safe to call more than once.\n */\n dispose(): void {\n this.unsubscribeAuthLost();\n this.unsubscribeAuthLost = () => {\n // Second and later dispose() calls no-op.\n };\n this.stateListeners.clear();\n // Defensive reset: if this instance is ever reused (HMR, test reuse),\n // the dedup guard shouldn't swallow the first post-dispose emission\n // just because it happens to match the state we last emitted.\n this.lastEmittedState = null;\n }\n\n private emitState(state: AuthState): void {\n // Dedupe consecutive identical emits: a failed logout call can both\n // trigger onAuthLost (via 401 in the request) and then fall through to\n // our explicit emit — without dedup, subscribers would see two\n // unauthenticated events for a single user action.\n if (state === this.lastEmittedState) return;\n this.lastEmittedState = state;\n\n for (const listener of this.stateListeners) {\n try {\n const result = listener(state);\n // Duck-type via `.then` (spec definition of a thenable) so any\n // Promise-like return, including non-native thenables from JS\n // callers that bypass the TS types, is handled uniformly.\n if (result && typeof (result as Promise<void>).then === 'function') {\n (result as Promise<void>).then(undefined, () => {\n // Async listener rejections must not break siblings or callers.\n });\n }\n } catch {\n // Synchronous listener throws must not break siblings or callers.\n }\n }\n }\n\n async login(params: LoginParams): Promise<LoginResponse> {\n const response = await this.http.request<LoginResponse>({\n method: 'POST',\n path: '/auth/login',\n body: params,\n });\n\n await this.storeTokensFromLogin(response);\n return response;\n }\n\n async register(params: RegisterParams): Promise<RegisterResponse> {\n const response = await this.http.request<RegisterResponse>({\n method: 'POST',\n path: '/auth/register',\n body: params,\n });\n\n // Only store tokens if they were returned (not when email verification is required)\n if (response.access_token) {\n await this.storeTokensFromRegister(response);\n }\n return response;\n }\n\n async refresh(refreshToken: string): Promise<TokenResponse> {\n return this.http.request<TokenResponse>({\n method: 'POST',\n path: '/auth/refresh',\n body: { refreshToken },\n });\n }\n\n async getProfile(): Promise<UserProfile> {\n return this.http.request<UserProfile>({\n method: 'GET',\n path: '/auth/me',\n authenticated: true,\n });\n }\n\n async logout(): Promise<void> {\n try {\n await this.http.request<void>({\n method: 'POST',\n path: '/auth/logout',\n authenticated: true,\n });\n } catch {\n // Server-side revocation is best-effort. If the token is already invalid\n // the server has effectively revoked the session anyway — always clear\n // the local state so the UI doesn't get stuck.\n }\n await this.tokenManager.clearTokens();\n this.emitState('unauthenticated');\n }\n\n async verifyEmail(token: string): Promise<{ message: string }> {\n return this.http.request<{ message: string }>({\n method: 'GET',\n path: '/auth/verify-email',\n query: { token },\n });\n }\n\n async resendVerification(email: string): Promise<{ message: string }> {\n return this.http.request<{ message: string }>({\n method: 'POST',\n path: '/auth/resend-verification',\n body: { email },\n });\n }\n\n async getUserInfo(): Promise<UserInfo> {\n return this.http.request<UserInfo>({\n method: 'GET',\n path: this.config.userInfoEndpoint ?? '/auth/userinfo',\n authenticated: true,\n });\n }\n\n getAuthorizationUrl(params: AuthorizationUrlParams): string {\n const baseUrl = this.config.apiUrl.replace(/\\/+$/, '');\n const endpoint =\n this.config.authorizationEndpoint ?? '/auth/authorize';\n const url = new URL(`${baseUrl}${endpoint}`);\n\n url.searchParams.set('client_id', this.config.clientId);\n url.searchParams.set('redirect_uri', params.redirectUri);\n url.searchParams.set('response_type', params.responseType ?? 'code');\n\n const scope =\n params.scope ?? this.config.scopes?.join(' ') ?? 'openid';\n url.searchParams.set('scope', scope);\n\n if (params.state) {\n url.searchParams.set('state', params.state);\n }\n if (params.codeChallenge) {\n url.searchParams.set('code_challenge', params.codeChallenge);\n }\n if (params.codeChallengeMethod) {\n url.searchParams.set(\n 'code_challenge_method',\n params.codeChallengeMethod\n );\n }\n\n return url.toString();\n }\n\n async exchangeCode(params: ExchangeCodeParams): Promise<TokenResponse> {\n const body: Record<string, string> = {\n grant_type: params.grantType ?? GrantType.AUTHORIZATION_CODE,\n code: params.code,\n redirect_uri: params.redirectUri,\n client_id: this.config.clientId,\n };\n\n if (params.clientSecret) {\n body['client_secret'] = params.clientSecret;\n }\n if (params.codeVerifier) {\n body['code_verifier'] = params.codeVerifier;\n }\n\n const tokenEndpoint =\n this.config.tokenEndpoint ?? '/auth/token';\n\n const response = await this.http.request<TokenResponse>({\n method: 'POST',\n path: tokenEndpoint,\n body,\n });\n\n const expiresAt = response.expires_in\n ? Math.floor(Date.now() / 1000) + response.expires_in\n : undefined;\n\n await this.tokenManager.setTokens({\n accessToken: response.access_token,\n refreshToken: response.refresh_token,\n expiresAt,\n });\n this.emitState('authenticated');\n\n return response;\n }\n\n async generatePKCE(): Promise<PKCEChallenge> {\n return generatePKCEChallenge();\n }\n\n async getOpenIDConfiguration(): Promise<OpenIDConfiguration> {\n return this.http.request<OpenIDConfiguration>({\n method: 'GET',\n path: '/.well-known/openid-configuration',\n });\n }\n\n async getJWKS(): Promise<JWKSResponse> {\n return this.http.request<JWKSResponse>({\n method: 'GET',\n path: '/.well-known/jwks.json',\n });\n }\n\n /**\n * Request a password reset email.\n * Always returns a generic message regardless of whether the email exists.\n */\n async requestPasswordReset(email: string): Promise<{ message: string }> {\n return this.http.request<{ message: string }>({\n method: 'POST',\n path: '/auth/request-password-reset',\n body: { email },\n });\n }\n\n /**\n * Reset password using a token received via email.\n */\n async resetPassword(\n token: string,\n password: string,\n ): Promise<{ message: string }> {\n return this.http.request<{ message: string }>({\n method: 'POST',\n path: '/auth/reset-password',\n body: { token, password },\n });\n }\n\n /**\n * Exchange a one-time verification code for tokens.\n * Use this after email verification redirects the user with a ?loginCode= parameter.\n */\n async exchangeVerificationCode(code: string): Promise<LoginResponse> {\n const response = await this.http.request<LoginResponse>({\n method: 'POST',\n path: '/auth/exchange-code',\n body: { code },\n });\n\n await this.storeTokensFromLogin(response);\n return response;\n }\n\n /**\n * Resend a two-factor authentication code email.\n * Call this when the user hasn't received the code or it expired.\n */\n async resendTwoFactorCode(token: string): Promise<{ message: string }> {\n return this.http.request<{ message: string }>({\n method: 'POST',\n path: '/auth/resend-two-factor-code',\n body: { token },\n });\n }\n\n /**\n * Verify a two-factor authentication code.\n * Call this after catching SpacelrTwoFactorRequiredError from login().\n */\n async verifyTwoFactor(params: TwoFactorVerifyParams): Promise<LoginResponse> {\n const response = await this.http.request<LoginResponse>({\n method: 'POST',\n path: '/auth/verify-two-factor',\n body: { token: params.token, code: params.code },\n });\n\n await this.storeTokensFromLogin(response);\n return response;\n }\n\n private async storeTokensFromLogin(response: LoginResponse): Promise<void> {\n const expiresAt = response.expires_in\n ? Math.floor(Date.now() / 1000) + response.expires_in\n : undefined;\n\n await this.tokenManager.setTokens({\n accessToken: response.access_token,\n refreshToken: response.refresh_token,\n expiresAt,\n });\n this.emitState('authenticated');\n }\n\n private async storeTokensFromRegister(\n response: RegisterResponse\n ): Promise<void> {\n // Guard mirrors the optional `access_token?: string` in RegisterResponse:\n // the server omits tokens when email verification is required. The\n // caller already pre-checks, but keeping the guard here means the helper\n // stays correct against its declared input type rather than relying on\n // the single current call site.\n if (!response.access_token) return;\n await this.tokenManager.setTokens({\n accessToken: response.access_token,\n refreshToken: response.refresh_token,\n });\n this.emitState('authenticated');\n }\n}\n","import { HttpClient, TokenManager } from '../core';\nimport {\n SpacelrClientConfig,\n FileVisibility,\n FileInfo,\n FileListResponse,\n ListFilesParams,\n InitMultipartUploadParams,\n InitMultipartUploadResponse,\n PartEtag,\n ShareFileParams,\n UnshareFileParams,\n QuotaInfo,\n UploadFileParams,\n UploadLargeFileParams,\n UploadProgress,\n DownloadUrlResponse,\n} from '../types';\n\nexport class StorageModule {\n private http: HttpClient;\n private tokenManager: TokenManager;\n private config: SpacelrClientConfig;\n\n constructor(\n http: HttpClient,\n tokenManager: TokenManager,\n config: SpacelrClientConfig\n ) {\n this.http = http;\n this.tokenManager = tokenManager;\n this.config = config;\n }\n\n /**\n * Upload a file through the gateway (no direct S3 access).\n * Accepts a Blob/File (browser) or ArrayBuffer/Uint8Array (Node).\n */\n async uploadFile(\n file: Blob | ArrayBuffer | Uint8Array,\n params: UploadFileParams,\n onProgress?: (progress: UploadProgress) => void,\n ): Promise<FileInfo> {\n const formData = new FormData();\n const blob =\n file instanceof Blob\n ? file\n : new Blob([file as BlobPart], { type: params.mimeType });\n formData.append('file', blob, params.filename);\n if (params.visibility) formData.append('visibility', params.visibility);\n if (params.description) formData.append('description', params.description);\n\n const progressHandler = onProgress\n ? (e: { loaded: number; total: number }) => {\n onProgress({\n loaded: e.loaded,\n total: e.total,\n percentage: e.total > 0 ? Math.round((e.loaded / e.total) * 100) : 0,\n });\n }\n : undefined;\n\n return this.http.uploadForm<FileInfo>('/storage/files', formData, progressHandler);\n }\n\n /**\n * Upload a large file using multipart upload through the gateway.\n * Splits into parts, uploads concurrently, and completes.\n */\n async uploadLargeFile(\n file: Blob | ArrayBuffer | Uint8Array,\n params: UploadLargeFileParams,\n onProgress?: (progress: UploadProgress) => void\n ): Promise<FileInfo> {\n const fileSize = file instanceof Blob ? file.size : file.byteLength;\n const concurrency = params.concurrency ?? 3;\n\n const init = await this.initMultipartUpload({\n filename: params.filename,\n mimeType: params.mimeType,\n totalSizeBytes: fileSize,\n visibility: params.visibility,\n description: params.description,\n });\n\n const { partSize, totalParts, fileId } = init;\n const completedParts: PartEtag[] = [];\n let completedBytes = 0;\n const partProgressMap = new Map<number, number>();\n\n try {\n const allPartNumbers = Array.from(\n { length: totalParts },\n (_, i) => i + 1\n );\n\n for (let i = 0; i < allPartNumbers.length; i += concurrency) {\n const batch = allPartNumbers.slice(i, i + concurrency);\n\n const uploads = batch.map(async (partNumber) => {\n const start = (partNumber - 1) * partSize;\n const end = Math.min(start + partSize, fileSize);\n const chunkSize = end - start;\n const chunk =\n file instanceof Blob\n ? file.slice(start, end)\n : new Blob([file.slice(start, end)]);\n\n const formData = new FormData();\n formData.append('file', chunk, `part-${partNumber}`);\n formData.append('partNumber', String(partNumber));\n\n const partProgress = onProgress && typeof XMLHttpRequest !== 'undefined'\n ? (e: { loaded: number; total: number }) => {\n const ratio = e.total > 0 ? e.loaded / e.total : 0;\n partProgressMap.set(partNumber, Math.min(ratio * chunkSize, chunkSize));\n const inFlightBytes = Array.from(partProgressMap.values())\n .reduce((sum, v) => sum + v, 0);\n const totalLoaded = Math.min(completedBytes + inFlightBytes, fileSize);\n onProgress({\n loaded: totalLoaded,\n total: fileSize,\n percentage: fileSize > 0\n ? Math.min(100, Math.round((totalLoaded / fileSize) * 100))\n : 0,\n });\n }\n : undefined;\n\n const result = await this.http.uploadForm<{ etag: string }>(\n `/storage/files/${fileId}/multipart/upload-part`,\n formData,\n partProgress,\n );\n\n partProgressMap.delete(partNumber);\n completedBytes += chunkSize;\n\n if (onProgress) {\n onProgress({\n loaded: Math.min(completedBytes, fileSize),\n total: fileSize,\n percentage: fileSize > 0\n ? Math.min(100, Math.round((completedBytes / fileSize) * 100))\n : 0,\n });\n }\n\n completedParts.push({\n partNumber,\n etag: result.etag,\n });\n });\n\n await Promise.all(uploads);\n partProgressMap.clear();\n }\n\n // Sort parts by number before completing\n completedParts.sort((a, b) => a.partNumber - b.partNumber);\n\n await this.completeMultipartUpload(fileId, completedParts);\n return this.getFileInfo(fileId);\n } catch (error) {\n // Clean up the incomplete multipart upload on failure\n try {\n await this.abortMultipartUpload(fileId);\n } catch {\n // Abort is best-effort; ignore cleanup failures\n }\n throw error;\n }\n }\n\n async initMultipartUpload(\n params: InitMultipartUploadParams\n ): Promise<InitMultipartUploadResponse> {\n return this.http.request<InitMultipartUploadResponse>({\n method: 'POST',\n path: '/storage/files/multipart/init',\n body: params,\n authenticated: true,\n });\n }\n\n async completeMultipartUpload(\n fileId: string,\n parts: PartEtag[]\n ): Promise<FileInfo> {\n return this.http.request<FileInfo>({\n method: 'POST',\n path: `/storage/files/${fileId}/multipart/complete`,\n body: { parts },\n authenticated: true,\n });\n }\n\n async abortMultipartUpload(fileId: string): Promise<void> {\n await this.http.request<void>({\n method: 'POST',\n path: `/storage/files/${fileId}/multipart/abort`,\n authenticated: true,\n });\n }\n\n async listFiles(params?: ListFilesParams): Promise<FileListResponse> {\n return this.http.request<FileListResponse>({\n method: 'GET',\n path: '/storage/files',\n query: params as Record<string, string | number | undefined>,\n authenticated: true,\n });\n }\n\n async listSharedFiles(params?: ListFilesParams): Promise<FileListResponse> {\n return this.http.request<FileListResponse>({\n method: 'GET',\n path: '/storage/shared',\n query: params as Record<string, string | number | undefined>,\n authenticated: true,\n });\n }\n\n async getFileInfo(fileId: string): Promise<FileInfo> {\n return this.http.request<FileInfo>({\n method: 'GET',\n path: `/storage/files/${fileId}`,\n authenticated: true,\n });\n }\n\n async downloadFile(fileId: string): Promise<Blob> {\n const baseUrl = this.config.apiUrl.replace(/\\/+$/, '');\n const url = `${baseUrl}/storage/files/${fileId}/download`;\n\n const headers: Record<string, string> = {\n 'x-client-id': this.config.clientId,\n 'x-project-id': this.config.projectId,\n };\n\n const token = await this.tokenManager.getAccessToken();\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n\n const response = await fetch(url, { headers });\n\n if (!response.ok) {\n throw new Error(`Download failed: ${response.status}`);\n }\n\n return response.blob();\n }\n\n async getDownloadUrl(fileId: string): Promise<DownloadUrlResponse> {\n return this.http.request<DownloadUrlResponse>({\n method: 'GET',\n path: `/storage/files/${fileId}/download-url`,\n authenticated: true,\n });\n }\n\n async deleteFile(fileId: string): Promise<void> {\n await this.http.request<void>({\n method: 'DELETE',\n path: `/storage/files/${fileId}`,\n authenticated: true,\n });\n }\n\n async shareFile(\n fileId: string,\n params: ShareFileParams\n ): Promise<void> {\n await this.http.request<void>({\n method: 'POST',\n path: `/storage/files/${fileId}/share`,\n body: params,\n authenticated: true,\n });\n }\n\n async unshareFile(\n fileId: string,\n params: UnshareFileParams\n ): Promise<void> {\n await this.http.request<void>({\n method: 'POST',\n path: `/storage/files/${fileId}/unshare`,\n body: params,\n authenticated: true,\n });\n }\n\n async updateVisibility(\n fileId: string,\n visibility: FileVisibility\n ): Promise<FileInfo> {\n return this.http.request<FileInfo>({\n method: 'PATCH',\n path: `/storage/files/${fileId}/visibility`,\n body: { visibility },\n authenticated: true,\n });\n }\n\n async getQuota(): Promise<QuotaInfo> {\n return this.http.request<QuotaInfo>({\n method: 'GET',\n path: '/storage/quota',\n authenticated: true,\n });\n }\n\n async getPublicFileUrl(fileId: string, projectId?: string): Promise<string> {\n const baseUrl = this.config.apiUrl.replace(/\\/+$/, '');\n const resolvedProjectId = projectId ?? this.config.projectId;\n return `${baseUrl}/public/files/${resolvedProjectId}/${fileId}`;\n }\n}\n","import { SpacelrError } from '../core/errors';\n\n/**\n * Base class for all timeline-related errors thrown by the SDK. Extends\n * SpacelrError so it fits the SDK's existing `catch (e instanceof\n * SpacelrError)` pattern. Subclasses give callers fine-grained control\n * (e.g. catch only `CursorInvalidError` and restart pagination).\n */\nexport class TimelineError extends SpacelrError {\n constructor(message: string, opts: { statusCode?: number; code?: string } = {}) {\n super(message, opts.code ?? 'TIMELINE_ERROR', opts.statusCode);\n this.name = 'TimelineError';\n }\n}\n\nexport class CursorInvalidError extends TimelineError {\n constructor(message: string, opts: { statusCode?: number; code?: string } = {}) {\n super(message, { statusCode: opts.statusCode ?? 400, code: opts.code ?? 'CURSOR_INVALID' });\n this.name = 'CursorInvalidError';\n }\n}\n\nexport class ValidationError extends TimelineError {\n constructor(message: string, opts: { statusCode?: number; code?: string } = {}) {\n super(message, { statusCode: opts.statusCode ?? 400, code: opts.code ?? 'BAD_REQUEST' });\n this.name = 'ValidationError';\n }\n}\n\nexport class ForbiddenError extends TimelineError {\n constructor(message: string, opts: { statusCode?: number; code?: string } = {}) {\n super(message, { statusCode: opts.statusCode ?? 403, code: opts.code ?? 'FORBIDDEN' });\n this.name = 'ForbiddenError';\n }\n}\n\nexport class NotFoundError extends TimelineError {\n constructor(message: string, opts: { statusCode?: number; code?: string } = {}) {\n super(message, { statusCode: opts.statusCode ?? 404, code: opts.code ?? 'NOT_FOUND' });\n this.name = 'NotFoundError';\n }\n}\n\nexport class ServerConfigError extends TimelineError {\n constructor(message: string, opts: { statusCode?: number; code?: string } = {}) {\n super(message, { statusCode: opts.statusCode ?? 500, code: opts.code ?? 'CONFIG_INVALID' });\n this.name = 'ServerConfigError';\n }\n}\n\nexport class TimeoutError extends TimelineError {\n constructor(message: string, opts: { statusCode?: number; code?: string } = {}) {\n super(message, { statusCode: opts.statusCode ?? 504, code: opts.code ?? 'TIMEOUT' });\n this.name = 'TimeoutError';\n }\n}\n\ninterface ErrorBody {\n code?: string;\n message?: string;\n}\n\n/**\n * Maps a gateway error response (HTTP status + parsed body) to the typed\n * SDK error class. Status-primary dispatch with `code` as a tiebreaker at\n * 400 only.\n *\n * Why status-primary: ProjectMemberGuard 403 and ValidationPipe 400\n * bypass the timeline controller's catch block and keep NestJS's flat\n * body shape (no `error.code` field). The SDK's HttpClient sees\n * `code='HTTP_400'` / `'HTTP_403'` for those paths. Status-primary\n * dispatch handles both the Task-0-styled controller responses (with\n * the typed code on the wire) and the NestJS guard/pipe responses\n * (without it) — no separate exception filter needed.\n */\nexport function mapTimelineError(\n statusCode: number,\n body: ErrorBody | undefined,\n): TimelineError {\n const code = body?.code;\n const message = body?.message ?? `Timeline request failed with HTTP ${statusCode}`;\n\n if (statusCode === 400) {\n if (code === 'CURSOR_INVALID') {\n return new CursorInvalidError(message, { statusCode, code });\n }\n return new ValidationError(message, { statusCode, code });\n }\n if (statusCode === 403) {\n return new ForbiddenError(message, { statusCode, code });\n }\n if (statusCode === 404) {\n return new NotFoundError(message, { statusCode, code });\n }\n if (statusCode === 500) {\n return new ServerConfigError(message, { statusCode, code });\n }\n if (statusCode === 504) {\n return new TimeoutError(message, { statusCode, code });\n }\n return new TimelineError(message, { statusCode, code });\n}\n","// libs/sdk/src/modules/timeline.module.ts\nimport { HttpClient } from '../core/http-client';\nimport { SpacelrAuthError, SpacelrError, SpacelrNetworkError, SpacelrTimeoutError } from '../core/errors';\nimport { mapTimelineError, TimelineError, TimeoutError, ForbiddenError } from './timeline.errors';\nimport type {\n TimelineQueryOptions,\n TimelineQueryResponse,\n TimelineSourceStats,\n} from './timeline.types';\n\n/**\n * Low-level timeline SDK module. One method: query(opts) → page.\n *\n * The cursor is opaque from the SDK's perspective — it's stored as a\n * string, never decoded, never inspected. Callers pass the value of\n * `nextCursor` from the previous response verbatim into the next request.\n *\n * Errors thrown from query() extend SpacelrError so they integrate with\n * the SDK's existing error hierarchy. Use one of the typed subclasses\n * (CursorInvalidError, ForbiddenError, etc.) to catch specific cases or\n * `catch (e instanceof TimelineError)` for any timeline-related failure.\n */\nexport class TimelineModule {\n constructor(private readonly http: HttpClient) {}\n\n async query(opts: TimelineQueryOptions): Promise<TimelineQueryResponse> {\n // Build the body deliberately so we don't smuggle undefined keys.\n // Some HTTP clients serialise `undefined` as `null`, which would\n // re-fail server-side DTO validation for fields that are @IsOptional\n // but not nullable.\n const body: Record<string, unknown> = {\n collection: opts.collection,\n partitionValue: opts.partitionValue,\n };\n if (opts.where !== undefined) body['where'] = opts.where;\n if (opts.orderBy !== undefined) body['orderBy'] = opts.orderBy;\n if (opts.limit !== undefined) body['limit'] = opts.limit;\n if (opts.cursor !== undefined) body['cursor'] = opts.cursor;\n\n let raw: unknown;\n try {\n raw = await this.http.request<unknown>({\n method: 'POST',\n path: '/db/timeline',\n body,\n authenticated: true,\n });\n } catch (err) {\n throw this.translateError(err);\n }\n\n if (!isTimelineQueryResponse(raw)) {\n throw new TimelineError(\n 'Timeline gateway returned a response shape the SDK does not recognise',\n { statusCode: 500, code: 'INVALID_RESPONSE_SHAPE' },\n );\n }\n return raw;\n }\n\n private translateError(err: unknown): unknown {\n // SpacelrTimeoutError from HttpClient's AbortController → fold into\n // TimelineError taxonomy so callers don't need to know about the SDK's\n // transport-layer errors when working with timeline.\n if (err instanceof SpacelrTimeoutError) {\n return new TimeoutError(err.message, { statusCode: 504, code: 'TIMEOUT' });\n }\n // 401 SpacelrAuthError stays as-is — auth-flow concern, not timeline.\n // 403 SpacelrAuthError comes from partition-membership denial; translate\n // to the timeline-flavoured ForbiddenError so callers don't reach for\n // SpacelrAuthError when working with timeline-specific responses.\n if (err instanceof SpacelrAuthError) {\n if (err.statusCode === 403) {\n return new ForbiddenError(err.message, { statusCode: 403, code: err.code ?? 'FORBIDDEN' });\n }\n return err; // 401 → propagate unchanged so the SDK's auth-recovery path runs\n }\n // SpacelrNetworkError is a SpacelrError subclass but represents a\n // transport-layer failure unrelated to the timeline domain — re-raise\n // unchanged so callers can distinguish network failures from timeline\n // application errors.\n if (err instanceof SpacelrNetworkError) {\n return err;\n }\n if (err instanceof SpacelrError) {\n return mapTimelineError(err.statusCode ?? 0, { code: err.code, message: err.message });\n }\n // Unexpected exceptions — re-raise unchanged.\n return err;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Response-shape guard. Defensive against proxy/middleware/misconfigured\n// gateway responses; not a substitute for server-side validation.\n// ---------------------------------------------------------------------------\n\nfunction isTimelineQueryResponse(value: unknown): value is TimelineQueryResponse {\n if (typeof value !== 'object' || value === null) return false;\n const v = value as Record<string, unknown>;\n if (!Array.isArray(v['items'])) return false;\n if (v['nextCursor'] !== null && typeof v['nextCursor'] !== 'string') return false;\n if (!isTimelineSourceStats(v['sourceStats'])) return false;\n return true;\n}\n\nfunction isTimelineSourceStats(value: unknown): value is TimelineSourceStats {\n if (typeof value !== 'object' || value === null) return false;\n const v = value as Record<string, unknown>;\n if (typeof v['hot'] !== 'number') return false;\n if (typeof v['cold'] !== 'number') return false;\n if (v['segmentsScanned'] !== undefined && typeof v['segmentsScanned'] !== 'number') {\n return false;\n }\n return true;\n}\n","import { HttpClient } from '../core';\nimport { RealtimeClient, DatabaseChangeEvent, SpacelrError } from '../core';\nimport { TimelineModule } from './timeline.module';\nimport type { GapReason, StreamGapInfo } from '../core/realtime';\nimport type { CursorStorage } from '../core/cursor-storage';\n\n// Re-export for downstream consumers so they don't need to reach into core.\nexport type { GapReason, StreamGapInfo, CursorStorage };\n\nexport interface PopulateOption {\n field: string;\n collection: string;\n foreignField?: string;\n}\n\nexport interface DocumentResult {\n _id: string;\n [key: string]: unknown;\n}\n\nexport interface OffsetResult<T = Record<string, unknown>> {\n mode: 'offset';\n documents: (T & { _id: string })[];\n total: number;\n limit: number;\n offset: number;\n}\n\nexport interface CursorResult<T = Record<string, unknown>> {\n mode: 'cursor';\n documents: (T & { _id: string })[];\n limit: number;\n /**\n * `_id` of the last document in `documents`, or `null` if `documents` is empty.\n * Independent of `hasMore` — the cursor reflects the page contents, not\n * whether more pages exist.\n */\n nextCursor: string | null;\n /**\n * True when more documents exist past the returned page. Computed via the\n * `limit + 1` server-side fetch trick.\n */\n hasMore: boolean;\n}\n\nexport type FindResult<T = Record<string, unknown>> = OffsetResult<T> | CursorResult<T>;\n\n// SearchResult retains the pre-discriminator shape; search() does not yet\n// support cursor pagination.\nexport interface SearchResult<T = Record<string, unknown>> {\n documents: (T & { _id: string })[];\n total: number;\n limit: number;\n offset: number;\n}\n\nexport interface InsertResult {\n insertedCount: number;\n insertedIds: string[];\n}\n\nexport interface UpdateResult {\n matchedCount: number;\n modifiedCount: number;\n}\n\nexport interface DeleteResult {\n deletedCount: number;\n}\n\nexport interface CountResult {\n count: number;\n}\n\nexport interface FindByIdOptions {\n populate?: PopulateOption[];\n}\n\n/**\n * Options for a server-side substring search.\n *\n * Limits: `query` 1–200 chars, `fields` 1–10 entries (each matching\n * `/^[a-zA-Z0-9_.]+$/`, max 64 chars), `limit` max 100.\n */\nexport interface SearchOptions {\n query: string;\n fields: string[];\n filter?: Record<string, unknown>;\n sort?: Record<string, 1 | -1>;\n limit?: number;\n offset?: number;\n select?: string[];\n}\n\n/**\n * Options for `CollectionRef.subscribeWithSnapshot()` — see #99.\n *\n * The helper combines an initial query, a live stream subscription,\n * and outside-retention-window gap recovery into one idempotent\n * contract. Apps must implement `onChange` as state mutation keyed\n * by `_id` (Map/Set semantics): events MAY arrive twice (snapshot\n * vs stream overlap, gap recovery replay window, reconnect replay\n * from persisted cursor).\n *\n * Performance: `onChange` is awaited; a slow handler stalls the\n * stream for THIS subscriber. Keep it fast (synchronous state\n * updates only).\n *\n * Requires a stream-mode collection. Calling on a pubsub collection\n * throws SpacelrError with code SUBSCRIBE_WITH_SNAPSHOT_REQUIRES_STREAM\n * BEFORE any find() runs.\n */\nexport interface SubscribeWithSnapshotOptions<T> {\n /**\n * Filter for both the initial snapshot AND the live `onChange` stream.\n *\n * **Snapshot semantics** (full MongoDB query): supports operators like\n * `{ status: { $in: [...] } }`, `{ ts: { $gt: ... } }`, etc.\n *\n * **Live-stream semantics** (gateway-side equality only): the stream\n * filter only honours top-level primitive equality\n * (`string | number | boolean` values). Operator-shaped values are\n * silently dropped from the stream filter at handshake time — the\n * remaining primitive entries still apply, so a typical chat-style\n * `{ chatId: 'c1' }` filter works identically on both sides. If you\n * pass a mixed `{ chatId: 'c1', status: { $in: [...] } }`, the\n * snapshot is fully filtered but the live stream filters by `chatId`\n * only; you may receive `onChange` events for documents whose `status`\n * is outside your `$in` set. Re-check in the handler if needed.\n */\n where?: Record<string, unknown>;\n sort?: Record<string, 1 | -1>;\n limit?: number;\n select?: string[];\n\n /**\n * Called with the full snapshot:\n * - once after the initial find()\n * - again after every gap-exceeded recovery\n * `cursor` is the stream position at the moment the snapshot was\n * taken — opaque, may be persisted, may be passed to a later\n * subscribeWithCursor as sinceId. Never compare cursor strings.\n *\n * Note on shared readers: when another subscriber on the SAME\n * client+collection is already active, this subscriber piggy-backs\n * on that reader. The returned `cursor` reflects the stream tip at\n * THIS handshake; the shared reader may already have advanced past\n * it, so the duplicate-delivery window between this `cursor` and\n * the next `onChange` event can be wider than a fresh single-\n * subscriber attach. The at-least-once contract still holds —\n * idempotent `onChange` handlers absorb the extra duplicates.\n */\n onSnapshot: (\n docs: (T & { _id: string })[],\n cursor: string | null,\n ) => void | Promise<void>;\n\n /**\n * Per-event callback. Awaited; cursor advances after this resolves.\n * State mutations MUST be idempotent by `_id`.\n */\n onChange: (event: DatabaseChangeEvent) => void | Promise<void>;\n\n /**\n * Fatal stream/subscription errors AFTER the initial handshake has\n * resolved. Subscription is torn down. Pre-handshake failures (e.g.\n * pubsub-mode collection, ack-error from the gateway) DO NOT fire\n * this callback — they reject the helper's promise instead, with a\n * typed `SpacelrError` carrying a discriminator code.\n *\n * If this callback itself throws, the gap-recovery state machine\n * still resets to `'idle'`, but `onError` may be invoked a second\n * time (with the same error) by the dispatch IIFE's safety-net\n * catch. Keep the callback total: log, set state, return.\n */\n onError?: (err: Error) => void;\n\n /**\n * Snapshot read (`find()`) failed. Default: forwards to `onError`.\n * Aborted reads (caused by `unsubscribe()`) NEVER call this — they\n * silently reject the helper's promise with `AbortError`.\n *\n * **Dual-notification on non-abort failures**: a snapshot failure\n * fires this callback AND rejects the helper's outer promise with\n * the same error. Callers that handle both `await ...catch()` AND\n * register `onSnapshotError` will receive the error twice — pick\n * one path. The dual surface is intentional so apps using fire-and-\n * forget callback handlers don't need to also wrap the await.\n */\n onSnapshotError?: (err: Error) => void;\n\n // Note: gap-recovery only fires for `event-gap` frames whose\n // `reason === 'outside-retention-window'`. Other GapReason values\n // (`'redis-unavailable'`, `'replay-error'`, `'replay-truncated'`)\n // are transient or replay-time conditions that the realtime layer\n // either retries on reconnect or surfaces via the underlying\n // subscribe's `onError`. The helper does NOT re-snapshot for them,\n // so a long-running `'replay-truncated'` could mean some events\n // were skipped during initial replay — observable only as a\n // `onError` invocation, not as a fresh snapshot.\n //\n // Note: if a recovery's `subscribe-events` handshake itself fails\n // (transient network blip, expired token), the helper surfaces the\n // failure via `onError` and returns the state machine to `idle`.\n // It does NOT auto-retry — the next `event-gap` will trigger a\n // fresh recovery, but if no further gap arrives the subscription is\n // effectively abandoned. Pair with reconnect-aware token refresh\n // upstream of the SDK if you need stronger recovery guarantees.\n //\n // Note: a SECOND simultaneous `subscribeWithSnapshot()` call for\n // the same collection on the same client takes the realtime layer's\n // dedup path and synthesises `latestStreamId: null` — the second\n // caller's `onSnapshot` will receive `cursor: null` (no resume\n // anchor available) even if the stream is non-empty. The first\n // subscriber gets the real anchor. Avoid simultaneous calls to the\n // same collection.\n}\n\nexport interface SubscribeEventsHandlers<T = Record<string, unknown>> {\n /** Cursor to resume from. Undefined = fresh subscription, deliver only new events. */\n sinceId?: string;\n where?: Record<string, string | number | boolean>;\n /**\n * Optional resume-cursor store. When set, the SDK loads the previous cursor\n * before subscribing and persists the new cursor after each delivered\n * event. An explicit `sinceId` takes precedence over a loaded cursor —\n * `load()` is skipped entirely and saves resume from `sinceId` forward,\n * overwriting any previously persisted cursor.\n *\n * Pair `where`-filtered subscriptions with a unique `cursorKey`; subscribers\n * with different filters must not share a cursor since the stream position\n * represents \"events seen\" — filtered-out entries still advance it.\n */\n cursorStorage?: CursorStorage;\n /**\n * Override the default cursor key. Defaults to `{projectId}:{collectionName}`.\n * Useful when multiple subscriptions on the same collection (e.g. different\n * `where` filters or per-user feeds) must persist independent cursors.\n */\n cursorKey?: string;\n /** Called on inserts. Receives the document merged with `_eventId`. */\n onInsert?: (doc: T & { _id: string; _eventId: string }) => void | Promise<void>;\n onUpdate?: (doc: T & { _id: string; _eventId: string }) => void | Promise<void>;\n /** Called on deletes. `document` may be absent on tombstones — only documentId + eventId are guaranteed. */\n onDelete?: (documentId: string, eventId: string) => void | Promise<void>;\n /** Called when the server emits an event-gap. See GapReason for the four codes. */\n onGap?: (info: StreamGapInfo) => void;\n /** Called on handshake rejection (not-stream-collection, invalid sinceId, etc.) and handler failures. */\n onError?: (error: Error) => void;\n}\n\nexport interface StreamSubscription {\n /**\n * Stop receiving events and release the server-side reader slot. Idempotent.\n *\n * When `cursorStorage` is set, an in-flight async `save()` may still deliver\n * `onError` asynchronously after `unsubscribe()` returns — the fire-and-forget\n * save chain is not cancelled. Callers whose `onError` is tied to a component\n * lifecycle should guard for post-unmount delivery.\n */\n unsubscribe(): void;\n /**\n * Most recently processed eventId — advances after the subscription's\n * onEvent pipeline completes for each event, whether or not a user\n * handler was registered for that event type. `undefined` before any\n * event arrives.\n *\n * Persist this across app restarts and pass it back as `sinceId` on\n * the next `subscribeEvents()` call to catch up on missed events.\n */\n getCursor(): string | undefined;\n}\n\nexport interface SubscribeHandlers<T = Record<string, unknown>> {\n where?: Record<string, string | number | boolean>;\n onInsert?: (doc: T & { _id: string }) => void;\n onUpdate?: (doc: T & { _id: string }) => void;\n onDelete?: (documentId: string) => void;\n onError?: (error: Error) => void;\n}\n\nexport interface PaginateOptions {\n where?: Record<string, unknown>;\n /** Sort direction by `_id`. Defaults to -1 (newest first — chat scroll-back). */\n sort?: { _id: 1 | -1 };\n /** Page size. Default 50. Server typically caps at 100. */\n limit?: number;\n /**\n * Optional initial cursor — paginate from this `_id` exclusively. Useful\n * when the caller already holds a boundary document (e.g. the oldest\n * message currently rendered in a chat view paired with `subscribeEvents`).\n */\n cursor?: string;\n}\n\nexport interface PaginatorPage<T = Record<string, unknown>> {\n documents: (T & { _id: string })[];\n /**\n * True when more documents exist past this page. The paginator also flips\n * `exhausted` to true when this returns false, so callers can poll either\n * `page.hasMore` after each call or `paginator.exhausted` between calls.\n */\n hasMore: boolean;\n}\n\n/**\n * Cursor-based scroll helper around `find()`. Tracks the last-seen `_id`\n * across calls and applies it as `.before()` (descending sort) or\n * `.after()` (ascending sort) on the next page so the keyset stays stable\n * under concurrent inserts. Use `subscribeEvents` for new docs and\n * `paginate` for older — the two compose into a chat-style timeline.\n */\nexport class Paginator<T = Record<string, unknown>> {\n private cursor?: string;\n private _exhausted = false;\n private readonly direction: 1 | -1;\n private readonly pageSize: number;\n private readonly where?: Record<string, unknown>;\n /**\n * Tail of the serialized `next()` chain. Each call appends to this so\n * concurrent invocations from a UI scroll handler (e.g. user spam-tapping\n * \"load more\") don't issue parallel requests with the same cursor — which\n * would return duplicate pages and clobber the cursor based on whichever\n * response settles last. Calls execute strictly in invocation order.\n */\n private chain: Promise<PaginatorPage<T>> = Promise.resolve({\n documents: [],\n hasMore: false,\n });\n\n constructor(\n private readonly http: HttpClient,\n private readonly basePath: string,\n opts: PaginateOptions,\n ) {\n this.cursor = opts.cursor;\n this.direction = opts.sort?._id ?? -1;\n this.pageSize = opts.limit ?? 50;\n this.where = opts.where;\n }\n\n /**\n * True once a `next()` call has returned an empty page. Subsequent\n * `next()` calls return an empty page without hitting the server.\n *\n * Note: only the empty-page signal flips this flag; a server response\n * that returns documents but `hasMore: false` does NOT exhaust. This\n * lets ascending paginators (`sort: { _id: 1 }`) keep polling for\n * documents inserted after the caller caught up to the current tail —\n * the next `.after(lastSeen)` call will simply return zero documents\n * until something new lands.\n */\n get exhausted(): boolean {\n return this._exhausted;\n }\n\n next(): Promise<PaginatorPage<T>> {\n // Serialize: each call resolves only after the previous one settles.\n // We swallow rejections from the predecessor so a single failed page\n // doesn't poison the whole chain — the caller of the failed call\n // already saw the rejection.\n const run = this.chain.then(\n () => this.fetchNextPage(),\n () => this.fetchNextPage(),\n );\n this.chain = run;\n return run;\n }\n\n private async fetchNextPage(): Promise<PaginatorPage<T>> {\n if (this._exhausted) return { documents: [], hasMore: false };\n\n const builder = new QueryBuilder<T>(this.http, this.basePath, this.where)\n .sort({ _id: this.direction })\n .limit(this.pageSize);\n\n let result: FindResult<T>;\n if (this.cursor !== undefined) {\n // Cursor mode: keyset pagination, immune to concurrent inserts.\n const cursorBuilder =\n this.direction === -1\n ? builder.before(this.cursor)\n : builder.after(this.cursor);\n result = await cursorBuilder.execute();\n } else {\n // First page without an initial cursor — offset mode on the server.\n // Sort + limit are honoured the same way; we capture the last `_id`\n // here so all subsequent pages can switch to cursor mode.\n result = await builder.execute();\n }\n\n if (result.documents.length === 0) {\n // Empty page is the only signal that exhausts the paginator. A\n // populated page with `hasMore: false` leaves us re-callable so\n // ascending mode can pick up future inserts past `lastSeen`.\n this._exhausted = true;\n return { documents: [], hasMore: false };\n }\n\n this.cursor = result.documents[result.documents.length - 1]._id;\n\n const hasMore =\n result.mode === 'cursor'\n ? result.hasMore\n : result.documents.length < result.total;\n return { documents: result.documents, hasMore };\n }\n}\n\nexport class QueryBuilder<\n T = Record<string, unknown>,\n M extends 'offset' | 'cursor' = 'offset',\n> {\n private _filter?: Record<string, unknown>;\n private _sort?: Record<string, 1 | -1>;\n private _limit?: number;\n private _offset?: number;\n private _before?: string;\n private _after?: string;\n private _fields?: string[];\n private _populate: PopulateOption[] = [];\n\n constructor(\n private http: HttpClient,\n private basePath: string,\n filter?: Record<string, unknown>,\n ) {\n this._filter = filter;\n }\n\n sort(sort: Record<string, 1 | -1>): this {\n this._sort = sort;\n return this;\n }\n\n limit(limit: number): this {\n this._limit = limit;\n return this;\n }\n\n offset(offset: number): this {\n this._offset = offset;\n return this;\n }\n\n /**\n * **Constraint:** the cursor value must be a 24-hex ObjectId string.\n * Collections using custom non-ObjectId `_id` strings will not work\n * correctly with cursor pagination — the server's `$lt`/`$gt` comparison\n * uses BSON type ordering, mixing string `_id`s with ObjectId comparison\n * produces undefined behaviour. Documented limitation; future work may\n * add opaque cursor tokens that abstract over `_id` types.\n *\n * Switch to cursor-pagination mode: return documents with `_id < id`\n * (in the sort-defined order). The cursor refers to the cursor *value*,\n * not visual UI direction. Requires `.sort()` to be `{ _id: 1 }` or\n * `{ _id: -1 }` (or omitted — server defaults to `{ _id: 1 }`).\n *\n * Narrows the builder's mode parameter so subsequent `.execute()` returns\n * a `CursorResult<T>` instead of `OffsetResult<T>`.\n *\n * **For paginating further:** the `nextCursor` field returned by\n * `execute()` is the `_id` of the last document on the page. To load the\n * next older page, pass it again to `.before()`. (Do NOT pass it to\n * `.after()` — that would request docs newer than this page.)\n *\n * **Cannot be combined with `.offset()`.** Type system allows the chain\n * for ergonomics, but the server rejects it with HTTP 400.\n */\n before(id: string): QueryBuilder<T, 'cursor'> {\n this._before = id;\n return this as QueryBuilder<T, 'cursor'>;\n }\n\n /**\n * Switch to cursor-pagination mode: return documents with `_id > id`.\n * See `.before()` for full semantics — including the ObjectId-only cursor\n * constraint and the `.offset()` incompatibility (server-enforced 400).\n *\n * **For paginating further:** pass the returned `nextCursor` to\n * `.after()` again to load the next newer page.\n */\n after(id: string): QueryBuilder<T, 'cursor'> {\n this._after = id;\n return this as QueryBuilder<T, 'cursor'>;\n }\n\n select(fields: string[]): this {\n this._fields = fields;\n return this;\n }\n\n populate(\n field: string,\n collection: string,\n foreignField?: string,\n ): this {\n this._populate.push({ field, collection, foreignField });\n return this;\n }\n\n /**\n * `signal` is an optional external AbortSignal forwarded into the underlying\n * HTTP request. Used by `subscribeWithSnapshot` so that an unsubscribe()\n * call cancels the in-flight snapshot find(); regular callers can leave it\n * undefined.\n */\n async execute(\n signal?: AbortSignal,\n ): Promise<M extends 'cursor' ? CursorResult<T> : OffsetResult<T>> {\n const query: Record<string, string | number | undefined> = {};\n if (this._filter) query['filter'] = JSON.stringify(this._filter);\n if (this._sort) query['sort'] = JSON.stringify(this._sort);\n if (this._limit !== undefined) query['limit'] = this._limit;\n if (this._offset !== undefined) query['offset'] = this._offset;\n if (this._before !== undefined) query['before'] = this._before;\n if (this._after !== undefined) query['after'] = this._after;\n if (this._fields) query['fields'] = this._fields.join(',');\n if (this._populate.length) {\n query['populate'] = this._populate\n .map((p) =>\n p.foreignField\n ? `${p.field}:${p.collection}:${p.foreignField}`\n : `${p.field}:${p.collection}`,\n )\n .join(',');\n }\n\n return this.http.request<M extends 'cursor' ? CursorResult<T> : OffsetResult<T>>({\n method: 'GET',\n path: this.basePath,\n query,\n authenticated: true,\n signal,\n });\n }\n}\n\nexport class CollectionRef<T = Record<string, unknown>> {\n private basePath: string;\n private collectionName: string;\n\n constructor(\n private http: HttpClient,\n private realtime: RealtimeClient | null,\n private projectId: string,\n collectionName: string,\n ) {\n this.collectionName = collectionName;\n this.basePath = `/db/${collectionName}`;\n }\n\n async insert(\n document: Partial<T> & { _id?: string },\n ): Promise<InsertResult> {\n return this.http.request<InsertResult>({\n method: 'POST',\n path: this.basePath,\n body: { documents: [document] },\n authenticated: true,\n });\n }\n\n async insertMany(\n documents: (Partial<T> & { _id?: string })[],\n ): Promise<InsertResult> {\n return this.http.request<InsertResult>({\n method: 'POST',\n path: this.basePath,\n body: { documents },\n authenticated: true,\n });\n }\n\n /**\n * Query the collection's **hot tier** (live MongoDB).\n *\n * **Cold-tier note:** if the collection has cold-tier archival enabled, aged\n * documents are moved to object storage and purged from the hot tier — they\n * will NOT appear in `find()` results (nor `findById`, `count`, `search`,\n * `paginate`, which are all hot-tier only). To read archived history, use\n * {@link TimelineModule.query} via `db.timeline.query(...)`, which transparently\n * merges hot and cold results for a partition.\n */\n find(filter?: Record<string, unknown>): QueryBuilder<T> {\n return new QueryBuilder<T>(this.http, this.basePath, filter);\n }\n\n /**\n * Cursor-based scroll-back helper. Returns a `Paginator` whose `.next()`\n * yields successive pages and tracks the last-seen `_id` internally.\n * Defaults to descending sort and 50 docs per page (chat scroll-back is\n * the canonical use case). See `PaginateOptions` for tuning.\n *\n * **Cursor constraint:** uses `_id` keyset pagination, which requires\n * 24-hex ObjectId `_id`s. Collections with custom string `_id` schemes\n * fall back to comparing strings as ObjectIds — the server's handler\n * documents this limitation on `before` / `after` directly.\n */\n paginate(opts: PaginateOptions = {}): Paginator<T> {\n return new Paginator<T>(this.http, this.basePath, opts);\n }\n\n /**\n * Server-side substring search across the specified fields.\n *\n * The query is regex-escaped server-side and matched case-insensitively via\n * MongoDB `$regex`. Performance note: unanchored case-insensitive regex\n * cannot use a standard B-tree index — on very large collections consider\n * narrowing with `filter` to scope the scan.\n *\n * Required filters: collections may declare `searchConfig.requireFilter`\n * via the admin API. Calls without those keys at the top level of `filter`\n * (or inside a top-level `$and`) reject with\n * `SpacelrSearchFilterRequiredError` carrying `missingFields`. Allowed\n * shapes: `{ field: value }`, `{ field: { $eq: value } }`,\n * `{ field: { $in: [...] } }` (non-empty).\n *\n * Limits: `query` 1–200 chars, `fields` 1–10 entries (each matching\n * `/^[a-zA-Z0-9_.]+$/`, max 64 chars), `limit` max 100.\n *\n * **Cold-tier note:** searches the hot tier only; archived documents are not\n * included. See {@link CollectionRef.find} and `db.timeline.query(...)`.\n */\n async search(opts: SearchOptions): Promise<SearchResult<T>> {\n return this.http.request<SearchResult<T>>({\n method: 'POST',\n path: `${this.basePath}/search`,\n body: opts,\n authenticated: true,\n });\n }\n\n /**\n * Fetch a single document by `_id` from the **hot tier**.\n *\n * **Cold-tier note:** returns null/throws for documents that have been\n * archived and purged from the hot tier. Archived history is reachable only\n * via `db.timeline.query(...)`. See {@link CollectionRef.find}.\n */\n async findById(\n id: string,\n options?: FindByIdOptions,\n ): Promise<T & { _id: string }> {\n const query: Record<string, string> = {};\n if (options?.populate?.length) {\n query['populate'] = options.populate\n .map((p) =>\n p.foreignField\n ? `${p.field}:${p.collection}:${p.foreignField}`\n : `${p.field}:${p.collection}`,\n )\n .join(',');\n }\n\n return this.http.request<T & { _id: string }>({\n method: 'GET',\n path: `${this.basePath}/${id}`,\n query,\n authenticated: true,\n });\n }\n\n async update(id: string, update: Partial<T>): Promise<UpdateResult> {\n return this.http.request<UpdateResult>({\n method: 'PATCH',\n path: `${this.basePath}/${id}`,\n body: { update },\n authenticated: true,\n });\n }\n\n async delete(id: string): Promise<DeleteResult> {\n return this.http.request<DeleteResult>({\n method: 'DELETE',\n path: `${this.basePath}/${id}`,\n authenticated: true,\n });\n }\n\n /**\n * Count documents in the **hot tier** matching `filter`.\n *\n * **Cold-tier note:** counts hot-tier documents only — archived/purged\n * documents are not included, so this is not a total-history count. See\n * {@link CollectionRef.find} and `db.timeline.query(...)`.\n */\n async count(filter?: Record<string, unknown>): Promise<number> {\n const result = await this.http.request<CountResult>({\n method: 'POST',\n path: `${this.basePath}/count`,\n body: { filter },\n authenticated: true,\n });\n return result.count;\n }\n\n subscribe(handlers: SubscribeHandlers<T>): () => void {\n if (!this.realtime) {\n throw new Error('Realtime not available: no RealtimeClient configured');\n }\n\n let unsubscribeFn: (() => void) | null = null;\n let pendingUnsub = false;\n\n const callback = (event: DatabaseChangeEvent) => {\n switch (event.type) {\n case 'insert':\n if (handlers.onInsert && event.document) {\n handlers.onInsert(event.document as T & { _id: string });\n }\n break;\n case 'update':\n if (handlers.onUpdate && event.document) {\n handlers.onUpdate(event.document as T & { _id: string });\n }\n break;\n case 'delete':\n if (handlers.onDelete) {\n handlers.onDelete(event.documentId);\n }\n break;\n }\n };\n\n this.realtime\n .subscribe(this.projectId, this.collectionName, callback, handlers.onError, handlers.where)\n .then((unsub) => {\n if (pendingUnsub) {\n // Unsubscribe was called before async subscribe resolved\n unsub();\n } else {\n unsubscribeFn = unsub;\n }\n })\n .catch((err) => {\n if (handlers.onError) {\n handlers.onError(err instanceof Error ? err : new Error(String(err)));\n }\n });\n\n // Return synchronous unsubscribe\n return () => {\n if (unsubscribeFn) {\n unsubscribeFn();\n } else {\n pendingUnsub = true;\n }\n };\n }\n\n subscribeEvents(handlers: SubscribeEventsHandlers<T>): StreamSubscription {\n if (!this.realtime) {\n throw new Error('Realtime not available: no RealtimeClient configured');\n }\n\n let lastCursor: string | undefined;\n let unsub: (() => void) | null = null;\n let unsubscribed = false;\n\n const cursorStorage = handlers.cursorStorage;\n const cursorKey =\n handlers.cursorKey ?? `${this.projectId}:${this.collectionName}`;\n\n // Loud warning for the #1 footgun: sharing a persisted cursor across\n // differently-filtered subscriptions silently skips events on reconnect\n // because the stream position represents \"seen\", not \"matched\".\n if (\n cursorStorage &&\n handlers.where &&\n Object.keys(handlers.where).length > 0 &&\n handlers.cursorKey === undefined\n ) {\n console.warn(\n `[spacelr] subscribeEvents on ${this.projectId}:${this.collectionName} uses a 'where' filter with cursorStorage but no explicit cursorKey — ` +\n `filtered subscriptions sharing the default key will silently skip events across reconnects. ` +\n `Pass a unique cursorKey (e.g. include a hash of the filter).`,\n );\n }\n // Capture after the null-guard so the closures below don't need `!`.\n const realtime = this.realtime;\n // Async saves must not complete out of order — that would regress the\n // persisted cursor below an already-emitted event. Chain every save\n // onto the previous so they serialize end-to-end while still being\n // decoupled from event delivery.\n let lastSavePromise: Promise<unknown> = Promise.resolve();\n\n const onEvent = async ({\n eventId,\n event,\n }: {\n eventId: string;\n event: DatabaseChangeEvent;\n }) => {\n try {\n if (event.type === 'insert' && handlers.onInsert && event.document) {\n await handlers.onInsert({\n ...(event.document as T & { _id: string }),\n _eventId: eventId,\n });\n } else if (event.type === 'update' && handlers.onUpdate && event.document) {\n await handlers.onUpdate({\n ...(event.document as T & { _id: string }),\n _eventId: eventId,\n });\n } else if (event.type === 'delete' && handlers.onDelete) {\n await handlers.onDelete(event.documentId, eventId);\n }\n lastCursor = eventId;\n if (cursorStorage) {\n // Chain this save onto the previous one so async backends can't\n // complete out of order and regress the stored cursor. Both\n // fulfilled and rejected continuations still run `save()` —\n // we don't want one failed save to skip the next event's save.\n // Each save's own failure surfaces via onError asynchronously.\n lastSavePromise = lastSavePromise\n .then(\n () => cursorStorage.save(cursorKey, eventId),\n () => cursorStorage.save(cursorKey, eventId),\n )\n .catch((err) => {\n handlers.onError?.(err instanceof Error ? err : new Error(String(err)));\n });\n }\n } catch (err) {\n // Rethrow so RealtimeClient's per-subscriber queue sees the rejection and\n // skips advancing its own `lastDeliveredId`. That same catch calls\n // `handlers.onError` for us — do NOT call it here or the user would see\n // the error twice.\n throw err instanceof Error ? err : new Error(String(err));\n }\n };\n\n const callSubscribe = (sinceId: string | undefined): Promise<() => void> =>\n realtime.subscribeWithCursor({\n projectId: this.projectId,\n collectionName: this.collectionName,\n sinceId,\n where: handlers.where,\n onEvent,\n onGap: handlers.onGap,\n onError: handlers.onError,\n });\n\n // Fast path: no storage (or explicit sinceId) — skip the extra await\n // so consumers that don't opt into cursor persistence keep the same\n // one-microtask handshake timing they had before.\n let promise: Promise<() => void>;\n if (handlers.sinceId !== undefined || !cursorStorage) {\n promise = callSubscribe(handlers.sinceId);\n } else {\n promise = (async () => {\n let resumeFrom: string | undefined;\n try {\n const loaded = await Promise.resolve(cursorStorage.load(cursorKey));\n if (loaded) resumeFrom = loaded;\n } catch (err) {\n // Storage read failure is never a reason to skip the subscription —\n // resume fresh and let the consumer's onError see the warning.\n handlers.onError?.(err instanceof Error ? err : new Error(String(err)));\n }\n return callSubscribe(resumeFrom);\n })();\n }\n\n promise\n .then((u) => {\n if (unsubscribed) {\n // Caller already asked to cancel; release the reader immediately.\n u();\n } else {\n unsub = u;\n }\n })\n .catch(() => undefined); // onError already surfaced inside the realtime layer.\n\n return {\n unsubscribe(): void {\n unsubscribed = true;\n if (unsub) {\n unsub();\n unsub = null;\n }\n },\n getCursor(): string | undefined {\n return lastCursor;\n },\n };\n }\n\n /**\n * Snapshot-aware subscribe — see #99 / SubscribeWithSnapshotOptions.\n *\n * Returns a Promise that resolves with the unsubscribe function only\n * after the first `onSnapshot` has run to completion (initial-load\n * signal). Calling unsubscribe() while the snapshot find() is in flight\n * aborts the request and silences the AbortError. Gap-recovery state\n * machine lands in Task 6.\n */\n async subscribeWithSnapshot(\n opts: SubscribeWithSnapshotOptions<T>,\n ): Promise<() => void> {\n if (!this.realtime) {\n throw new Error('Realtime not available: no RealtimeClient configured');\n }\n\n let isActive = true;\n let currentUnsubscribe: (() => void) | null = null;\n\n // Cancellation contract — see spec §\"Cancellation rules\". The snapshot\n // find() is the only awaited HTTP call inside this helper, and the\n // unsubscribe closure aborts it so a torn-down helper doesn't keep a\n // request in flight (and doesn't fire a stale onSnapshot when it lands).\n // Replaced on each recovery so a fresh find() can be aborted independently\n // of any prior aborted controllers.\n let snapshotAbort = new AbortController();\n\n // Gap-recovery state machine (spec §\"Recovery flow\"). Three states:\n // - idle — no recovery in flight; gap fires runRecovery\n // - running — recovery in flight; further gaps queue ONE re-run\n // - pending-rerun — re-run already queued; further gaps coalesce\n // Net effect: rapid gap bursts collapse into at most one trailing\n // re-run after the in-flight recovery finishes.\n type RecoveryState = 'idle' | 'running' | 'pending-rerun';\n let recoveryState: RecoveryState = 'idle';\n\n // Capture realtime now so the closures don't have to re-narrow the\n // nullable `this.realtime`.\n const realtime = this.realtime;\n\n // Project the helper's `where` (full MongoDB query) into the\n // gateway's stream-side filter shape (top-level primitive equality\n // only). Operator-shaped values (e.g. `{ $in: [...] }`) are\n // silently dropped from the live filter — see the JSDoc on\n // `SubscribeWithSnapshotOptions.where` for the full semantics. The\n // snapshot path keeps the original `where` unchanged.\n const streamWhere = ((): Record<string, string | number | boolean> | undefined => {\n if (!opts.where) return undefined;\n const out: Record<string, string | number | boolean> = {};\n for (const [k, v] of Object.entries(opts.where)) {\n if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {\n out[k] = v;\n }\n }\n return Object.keys(out).length > 0 ? out : undefined;\n })();\n\n // Operator-only `where` (no primitive values to project) means the\n // live stream filter is empty — onChange will receive every event\n // on the collection, not just the filter-matching ones. Snapshot is\n // still correctly filtered. Warn so production runs show why\n // onChange volume is higher than expected.\n if (\n opts.where &&\n streamWhere === undefined &&\n Object.keys(opts.where).length > 0\n ) {\n // eslint-disable-next-line no-console\n console.warn(\n `[spacelr] subscribeWithSnapshot on ${this.projectId}:${this.collectionName}: ` +\n `'where' filter contains only operator-shaped values (e.g. $gt, $in) which ` +\n `are not supported by the live stream filter. onChange will receive ALL ` +\n `collection events; snapshot is still filtered.`,\n );\n }\n\n const runRecovery = async (): Promise<void> => {\n // Tear down the current reader BEFORE starting a new one — frees the\n // server-side reader slot so the dedup branch on the new subscribe\n // doesn't piggy-back on the soon-to-be-replaced state.\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n\n if (!isActive) return;\n\n // Fresh subscribe — server returns a new latestStreamId atomically with\n // the reader-attach. Wire `onGap: handleGap` so a second gap during\n // recovery is also dispatched.\n //\n // Same `subscribeAckResolved` pattern as the initial subscribe: the\n // realtime layer fires `options.onError` synchronously before throwing\n // when the handshake ack carries an error. Without the gate, a\n // transient handshake failure would flip `isActive = false` and\n // permanently abandon the subscription — a subsequent gap would not\n // re-trigger recovery (state machine returns to idle but isActive is\n // already false). The catch below is the right home for handshake\n // failures: it surfaces via opts.onError without flipping isActive.\n let recoveryAckResolved = false;\n\n // Snapshot-ordering buffer for recovery (Codex review P1 #2). Same\n // contract as the initial-path buffer: queue live events arriving\n // between this recovery's subscribe-ack and its onSnapshot, then\n // drain them in order.\n let recoverySnapshotDelivered = false;\n const pendingRecoveryEvents: DatabaseChangeEvent[] = [];\n\n let next: {\n unsubscribe: () => void;\n realtimeMode: 'pubsub' | 'stream';\n latestStreamId: string | null;\n };\n try {\n next = await realtime.subscribeWithCursorWithMeta({\n projectId: this.projectId,\n collectionName: this.collectionName,\n sinceId: undefined,\n where: streamWhere,\n onEvent: async ({ event }) => {\n if (!isActive) return;\n if (!recoverySnapshotDelivered) {\n pendingRecoveryEvents.push(event);\n return;\n }\n await opts.onChange(event);\n },\n onError: (err) => {\n if (!isActive) return;\n // Suppress pre-handshake errors; the catch below surfaces them.\n if (!recoveryAckResolved) return;\n isActive = false;\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n snapshotAbort.abort();\n opts.onError?.(err);\n },\n onGap: handleGap,\n });\n recoveryAckResolved = true;\n } catch (err) {\n if (!isActive) return;\n opts.onError?.(err instanceof Error ? err : new Error(String(err)));\n return;\n }\n\n if (!isActive) {\n next.unsubscribe();\n return;\n }\n\n currentUnsubscribe = next.unsubscribe;\n\n // Fresh AbortController so the unsubscribe path can cancel this find()\n // independently of any earlier (already-resolved) snapshot reads.\n snapshotAbort = new AbortController();\n let recoveryDocs: (T & { _id: string })[];\n try {\n const builder = this.find(opts.where);\n if (opts.sort) builder.sort(opts.sort);\n if (opts.limit !== undefined) builder.limit(opts.limit);\n if (opts.select) builder.select(opts.select);\n const result = await builder.execute(snapshotAbort.signal);\n recoveryDocs = result.documents;\n } catch (err) {\n if (!isActive) return;\n if (err instanceof DOMException && err.name === 'AbortError') return;\n // Codex review P1 #3: a non-abort recovery-snapshot failure means\n // the local state is provably stale (the gap fired BECAUSE\n // retention overflowed) AND the replacement snapshot didn't\n // arrive. Continuing to deliver onChange events on top of the\n // pre-gap state would corrupt the user's local model. Tear down\n // the live subscription so no further `opts.onChange` fires; the\n // user gets onSnapshotError/onError and can decide whether to\n // re-subscribe.\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n isActive = false;\n const error = err instanceof Error ? err : new Error(String(err));\n if (opts.onSnapshotError) opts.onSnapshotError(error);\n else opts.onError?.(error);\n return;\n }\n\n if (!isActive) return;\n try {\n await opts.onSnapshot(recoveryDocs, next.latestStreamId);\n } catch (err) {\n // User's onSnapshot threw during recovery — tear down so the\n // (newly-acquired) server-side reader slot doesn't leak.\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n isActive = false;\n const error = err instanceof Error ? err : new Error(String(err));\n opts.onError?.(error);\n return;\n }\n\n // Drain the recovery's snapshot-ordering buffer (Codex P1 #2).\n // Same shape as the initial path — flip the flag only when the\n // queue is empty so events queued mid-drain are picked up by the\n // same loop.\n while (true) {\n if (!isActive) return;\n if (pendingRecoveryEvents.length === 0) {\n recoverySnapshotDelivered = true;\n break;\n }\n const ev = pendingRecoveryEvents.shift();\n if (ev !== undefined) {\n try {\n await opts.onChange(ev);\n } catch (err) {\n // onChange on a buffered recovery event threw — tear down\n // the live subscription. State is corrupt either way.\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n isActive = false;\n const error = err instanceof Error ? err : new Error(String(err));\n opts.onError?.(error);\n return;\n }\n }\n }\n };\n\n const dispatchRecovery = (): void => {\n if (!isActive) return;\n if (recoveryState === 'running') {\n recoveryState = 'pending-rerun';\n return;\n }\n if (recoveryState === 'pending-rerun') {\n return; // already queued; coalesce\n }\n recoveryState = 'running';\n void (async () => {\n // Yield once so synchronous gap bursts following the dispatch can\n // observe state='running' and queue a pending-rerun before any\n // teardown work begins. Without this, `runRecovery`'s synchronous\n // `currentUnsubscribe?.()` removes the state from\n // `streamSubscriptions` BEFORE later gaps can reach `onGap`,\n // dropping the coalescing signal entirely.\n await Promise.resolve();\n while (true) {\n try {\n await runRecovery();\n } catch (err) {\n // runRecovery routes its own errors via onSnapshotError / onError\n // and returns gracefully. This catch is a final safety net —\n // without it the while-true loop would spin on an unexpected\n // throw and exhaust the event loop. Surface and exit.\n //\n // Reset `recoveryState` FIRST so a throwing user callback (e.g.\n // `opts.onError`) cannot strand the state machine in 'running'\n // — that would silently coalesce every future gap into\n // 'pending-rerun' with no drainer.\n recoveryState = 'idle';\n if (!isActive) return;\n const error = err instanceof Error ? err : new Error(String(err));\n try {\n opts.onError?.(error);\n } catch {\n // User's callback threw — don't mask the original error\n // path. The state machine is already idle so the next gap\n // will recover.\n }\n return;\n }\n if (!isActive) {\n recoveryState = 'idle';\n return;\n }\n // Cast: TS narrows `recoveryState` to 'running' here because the\n // assignment at line 913 is the most recent observed write. TS\n // doesn't track the closure-mutation done by `dispatchRecovery`\n // (which can flip the state to 'pending-rerun' between iterations).\n // The cast widens the type back to RecoveryState so the\n // pending-rerun branch is reachable. Removing this cast yields\n // TS2367 (\"comparison appears unintentional\").\n if ((recoveryState as RecoveryState) === 'pending-rerun') {\n recoveryState = 'running';\n continue;\n }\n recoveryState = 'idle';\n return;\n }\n })();\n };\n\n const handleGap = (info: StreamGapInfo): void => {\n if (!isActive) return;\n // Only \"outside-retention-window\" requires a re-snapshot — the other\n // GapReason values are transient (redis-unavailable, replay-error,\n // replay-truncated) and the realtime layer's resubscribe path handles\n // them without needing a fresh baseline.\n if (info.reason !== 'outside-retention-window') return;\n dispatchRecovery();\n };\n\n // Step 1 of the spec flow: subscribe first so the server returns\n // `latestStreamId` atomically with the reader-attach. The reader is\n // already buffering events when this resolves — anything that arrives\n // during the snapshot find() is queued and replayed via onChange after\n // onSnapshot returns.\n // Tracks whether the initial subscribe-events handshake has resolved\n // successfully. Until then, errors surfaced by the realtime layer's\n // onError closure are HANDSHAKE failures (e.g. ack.error =\n // 'not-stream-collection') — they are also thrown by the helper's\n // try/catch below and remapped to SpacelrError. Forwarding them to\n // opts.onError too would double-notify the caller (review-round-3\n // bug #2). Only forward post-handshake.\n let subscribeAckResolved = false;\n\n // Initial-path snapshot-ordering buffer (Codex review P1 #2).\n // Live events arriving between subscribe-ack and onSnapshot completion\n // would otherwise fire opts.onChange BEFORE opts.onSnapshot, violating\n // the documented \"snapshot first, then live changes\" contract. Queue\n // events while `initialSnapshotDelivered === false`; drain them in\n // arrival order after onSnapshot returns. The drain flips the flag\n // ONLY when the queue is empty, so events that arrive mid-drain are\n // picked up by the same loop (no race window before live delivery).\n let initialSnapshotDelivered = false;\n const pendingInitialEvents: DatabaseChangeEvent[] = [];\n\n let initial: Awaited<ReturnType<typeof realtime.subscribeWithCursorWithMeta>>;\n try {\n initial = await realtime.subscribeWithCursorWithMeta({\n projectId: this.projectId,\n collectionName: this.collectionName,\n sinceId: undefined,\n where: streamWhere,\n onEvent: async ({ event }) => {\n if (!isActive) return;\n // Buffer live events until the initial snapshot has been\n // delivered. Returns immediately so the reader can advance\n // (no blocking the cross-subscriber Promise.all in\n // StreamReaderService).\n if (!initialSnapshotDelivered) {\n pendingInitialEvents.push(event);\n return;\n }\n await opts.onChange(event);\n },\n onError: (err) => {\n if (!isActive) return;\n // Suppress pre-handshake errors; the helper's catch block below\n // surfaces them via the typed SpacelrError throw.\n if (!subscribeAckResolved) return;\n // Post-handshake fatal stream error — flip isActive so an\n // in-flight find() resolution doesn't push a stale onSnapshot,\n // abort the snapshot read so it rejects promptly with\n // AbortError (silenced in the find()-catch), and tear down the\n // reader slot. Same teardown shape as the user-driven\n // unsubscribe() path.\n isActive = false;\n snapshotAbort.abort();\n currentUnsubscribe?.();\n // Null out so the find()-catch doesn't double-tear-down the slot.\n currentUnsubscribe = null;\n opts.onError?.(err);\n },\n onGap: handleGap,\n });\n subscribeAckResolved = true;\n } catch (err) {\n // The handshake itself rejected. The real gateway returns\n // `error: 'not-stream-collection'` for pubsub-mode collections, which\n // `_subscribeInternal` now surfaces as a SpacelrError with that code.\n // Map it to the typed `SUBSCRIBE_WITH_SNAPSHOT_REQUIRES_STREAM` so\n // callers get the SDK-level discriminator regardless of whether the\n // detection happened via the ack-error path or the realtimeMode check\n // below. All other handshake errors propagate unchanged.\n isActive = false;\n // _subscribeInternal throws SpacelrError when the ack carries an\n // error; use instanceof for type-safe code extraction (no\n // unchecked cast).\n const code = err instanceof SpacelrError ? err.code : undefined;\n if (code === 'not-stream-collection') {\n throw new SpacelrError(\n `subscribeWithSnapshot requires a stream-mode collection. ` +\n `Use subscribe() for pubsub collections.`,\n 'SUBSCRIBE_WITH_SNAPSHOT_REQUIRES_STREAM',\n undefined,\n { collectionName: this.collectionName },\n );\n }\n throw err;\n }\n\n if (initial.realtimeMode !== 'stream') {\n // Defensive secondary gate: if a future server returns ack OK + a\n // non-stream realtimeMode (instead of error: 'not-stream-collection'),\n // we still surface the typed error rather than silently subscribing\n // to a pubsub collection. Today's gateway throws via the ack-error\n // path above; this branch is belt-and-suspenders.\n isActive = false;\n initial.unsubscribe();\n throw new SpacelrError(\n `subscribeWithSnapshot requires a stream-mode collection. ` +\n `Use subscribe() for pubsub collections.`,\n 'SUBSCRIBE_WITH_SNAPSHOT_REQUIRES_STREAM',\n undefined,\n { collectionName: this.collectionName },\n );\n }\n\n currentUnsubscribe = initial.unsubscribe;\n\n // Step 2: snapshot. The reader is already buffering events; anything\n // that arrives during this find() is queued and will be delivered via\n // onChange after onSnapshot returns. The unsubscribe closure aborts the\n // request via `snapshotAbort` so a torn-down helper doesn't push a stale\n // onSnapshot.\n let snapshotDocs: (T & { _id: string })[];\n try {\n const builder = this.find(opts.where);\n if (opts.sort) builder.sort(opts.sort);\n if (opts.limit !== undefined) builder.limit(opts.limit);\n if (opts.select) builder.select(opts.select);\n const result = await builder.execute(snapshotAbort.signal);\n snapshotDocs = result.documents;\n } catch (err) {\n currentUnsubscribe?.();\n isActive = false;\n\n // Aborted reads are silent — see spec §\"Cancellation rules\". The abort\n // is triggered by the user's unsubscribe() (or by the wrapped onError\n // path which already surfaced the cause), so the user already knows the\n // helper is being torn down. Don't fire onSnapshotError or onError —\n // they'd be redundant noise. Re-throw so the awaited promise rejects.\n const isAbort =\n err instanceof DOMException && err.name === 'AbortError';\n if (isAbort) throw err;\n\n const error = err instanceof Error ? err : new Error(String(err));\n if (opts.onSnapshotError) opts.onSnapshotError(error);\n else opts.onError?.(error);\n throw error;\n }\n\n // Reachable when the wrapped onError flipped `isActive` during the\n // in-flight find() above. Don't fire onSnapshot against a torn-down\n // stream — onError already surfaced the cause.\n if (!isActive) {\n currentUnsubscribe?.();\n return () => undefined;\n }\n\n try {\n await opts.onSnapshot(snapshotDocs, initial.latestStreamId);\n } catch (err) {\n // User's onSnapshot threw — tear down the live subscription so the\n // server-side reader slot doesn't leak. Surface via opts.onError\n // (the snapshot itself was delivered to find() OK; the user\n // callback failed).\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n isActive = false;\n const error = err instanceof Error ? err : new Error(String(err));\n opts.onError?.(error);\n throw error;\n }\n\n // Drain the snapshot-ordering buffer (Codex review P1 #2). Events\n // queued during the find()/onSnapshot window are delivered in\n // arrival order BEFORE the helper returns its unsubscribe handle.\n // Loop checks length-then-flip in the same synchronous step so a\n // wrapped onEvent that fires between the check and the flip cannot\n // get orphaned in the queue.\n while (true) {\n if (!isActive) return () => undefined;\n if (pendingInitialEvents.length === 0) {\n initialSnapshotDelivered = true;\n break;\n }\n const ev = pendingInitialEvents.shift();\n if (ev !== undefined) {\n try {\n await opts.onChange(ev);\n } catch (err) {\n // User's onChange threw on a buffered event — same teardown\n // shape as the onSnapshot-throw path. Subscription is dead.\n currentUnsubscribe?.();\n currentUnsubscribe = null;\n isActive = false;\n const error = err instanceof Error ? err : new Error(String(err));\n opts.onError?.(error);\n throw error;\n }\n }\n }\n\n return () => {\n if (!isActive) return;\n isActive = false;\n // Aborts an in-flight snapshot find() if unsubscribe() races the\n // initial read or any recovery read; no-op if the read already\n // resolved.\n snapshotAbort.abort();\n currentUnsubscribe?.();\n };\n }\n}\n\nexport class DatabaseModule {\n private http: HttpClient;\n private realtime: RealtimeClient | null;\n private projectId: string;\n public readonly timeline: TimelineModule;\n\n constructor(http: HttpClient, projectId: string, realtime?: RealtimeClient) {\n this.http = http;\n this.projectId = projectId;\n this.realtime = realtime ?? null;\n this.timeline = new TimelineModule(http);\n }\n\n collection<T = Record<string, unknown>>(name: string): CollectionRef<T> {\n return new CollectionRef<T>(this.http, this.realtime, this.projectId, name);\n }\n}\n","import { HttpClient } from '../core';\n\nexport interface PushSubscriptionInfo {\n id: string;\n platform: 'web' | 'android' | 'ios';\n endpoint?: string;\n deviceToken?: string;\n deviceId?: string;\n deviceName?: string;\n userAgent?: string;\n isActive: boolean;\n lastUsedAt?: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface VapidKeyResponse {\n publicKey: string;\n}\n\nconst DEVICE_ID_KEY = 'spacelr_device_id';\n\nexport class NotificationsModule {\n private customDeviceId: string | null = null;\n private customDeviceName: string | null = null;\n\n constructor(private http: HttpClient) {}\n\n /** Set a custom device ID (e.g. from Capacitor Preferences for persistence beyond localStorage) */\n setDeviceId(id: string): void {\n this.customDeviceId = id;\n }\n\n /** Set a custom device name (e.g. \"iOS App\", \"macOS - Chrome\") */\n setDeviceName(name: string): void {\n this.customDeviceName = name;\n }\n\n /** Get or generate a stable device identifier. Custom ID takes priority over localStorage. */\n private getDeviceId(): string | undefined {\n if (this.customDeviceId) return this.customDeviceId;\n if (typeof localStorage === 'undefined') return undefined;\n let id = localStorage.getItem(DEVICE_ID_KEY);\n if (!id) {\n id = crypto.randomUUID();\n localStorage.setItem(DEVICE_ID_KEY, id);\n }\n return id;\n }\n\n /** Get device name: custom name > auto-detected from user agent > undefined */\n private getDeviceName(): string | undefined {\n if (this.customDeviceName) return this.customDeviceName;\n return this.detectDeviceName();\n }\n\n /** Auto-detect a short device label from navigator.userAgent (e.g. \"macOS - Chrome\") */\n private detectDeviceName(): string | undefined {\n if (typeof navigator === 'undefined' || !navigator.userAgent) return undefined;\n const ua = navigator.userAgent;\n\n let os = 'Unknown';\n if (/iPad|iPhone|iPod/.test(ua)) os = 'iOS';\n else if (/Android/.test(ua)) os = 'Android';\n else if (/Mac OS X/.test(ua)) os = 'macOS';\n else if (/Windows/.test(ua)) os = 'Windows';\n else if (/Linux/.test(ua)) os = 'Linux';\n\n let browser = 'Unknown';\n if (/Edg\\//.test(ua)) browser = 'Edge';\n else if (/OPR\\/|Opera/.test(ua)) browser = 'Opera';\n else if (/Chrome\\//.test(ua)) browser = 'Chrome';\n else if (/Safari\\//.test(ua) && !/Chrome/.test(ua)) browser = 'Safari';\n else if (/Firefox\\//.test(ua)) browser = 'Firefox';\n\n return `${os} - ${browser}`;\n }\n\n /** Get the VAPID public key for Web Push setup */\n async getVapidPublicKey(): Promise<VapidKeyResponse> {\n return this.http.request<VapidKeyResponse>({\n method: 'GET',\n path: '/notifications/vapid-key',\n authenticated: true,\n });\n }\n\n /** Register a push subscription (web, android, or ios) */\n async subscribe(\n subscription: {\n platform: 'web' | 'android' | 'ios';\n endpoint?: string;\n keys?: { p256dh: string; auth: string };\n deviceToken?: string;\n },\n deviceName?: string,\n ): Promise<PushSubscriptionInfo> {\n return this.http.request<PushSubscriptionInfo>({\n method: 'POST',\n path: '/notifications/subscribe',\n body: {\n ...subscription,\n deviceName: deviceName ?? this.getDeviceName(),\n deviceId: this.getDeviceId(),\n },\n authenticated: true,\n });\n }\n\n /** Unregister a push subscription */\n async unsubscribe(\n platform: 'web' | 'android' | 'ios',\n identifier: string,\n ): Promise<{ deleted: boolean }> {\n const body: Record<string, string> = { platform };\n if (platform === 'web') {\n body['endpoint'] = identifier;\n } else {\n body['deviceToken'] = identifier;\n }\n\n return this.http.request<{ deleted: boolean }>({\n method: 'DELETE',\n path: '/notifications/subscribe',\n body,\n authenticated: true,\n });\n }\n\n /** Get all subscriptions for the current user */\n async getSubscriptions(): Promise<PushSubscriptionInfo[]> {\n return this.http.request<PushSubscriptionInfo[]>({\n method: 'GET',\n path: '/notifications/subscriptions',\n authenticated: true,\n });\n }\n\n /**\n * Helper: Register browser Web Push subscription.\n * Requests notification permission, subscribes via Push API,\n * and registers the subscription with the server.\n */\n async registerBrowserPush(\n serviceWorkerRegistration: ServiceWorkerRegistration,\n deviceName?: string,\n ): Promise<PushSubscriptionInfo> {\n // Get VAPID key\n const { publicKey } = await this.getVapidPublicKey();\n\n if (!publicKey) {\n throw new Error(\n 'VAPID public key not configured on the server',\n );\n }\n\n // Request notification permission\n const permission = await Notification.requestPermission();\n if (permission !== 'granted') {\n throw new Error(\n `Notification permission denied: ${permission}`,\n );\n }\n\n // Convert VAPID key to Uint8Array\n const applicationServerKey = this.urlBase64ToUint8Array(publicKey);\n\n // Subscribe via Push API\n const pushSubscription =\n await serviceWorkerRegistration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey,\n });\n\n const subJson = pushSubscription.toJSON();\n const p256dh = subJson.keys?.['p256dh'];\n const auth = subJson.keys?.['auth'];\n\n if (!subJson.endpoint || !p256dh || !auth) {\n throw new Error('Invalid push subscription: missing endpoint or keys');\n }\n\n // Register with server\n return this.subscribe(\n {\n platform: 'web',\n endpoint: subJson.endpoint,\n keys: { p256dh, auth },\n },\n deviceName,\n );\n }\n\n /**\n * Helper: Register a mobile device push token (FCM or APNs).\n */\n async registerDevicePush(\n deviceToken: string,\n platform: 'android' | 'ios',\n deviceName?: string,\n ): Promise<PushSubscriptionInfo> {\n return this.subscribe(\n {\n platform,\n deviceToken,\n },\n deviceName,\n );\n }\n\n private urlBase64ToUint8Array(base64String: string): ArrayBuffer {\n const padding = '='.repeat((4 - (base64String.length % 4)) % 4);\n const base64 = (base64String + padding)\n .replace(/-/g, '+')\n .replace(/_/g, '/');\n\n const rawData = atob(base64);\n const outputArray = new Uint8Array(rawData.length);\n\n for (let i = 0; i < rawData.length; ++i) {\n outputArray[i] = rawData.charCodeAt(i);\n }\n return outputArray.buffer as ArrayBuffer;\n }\n}\n","import { HttpClient } from '../core';\n\nexport interface FunctionInvokeOptions {\n /** Webhook secret — used for `webhook` and `hybrid` invokeMode. */\n secret?: string;\n /**\n * Whether to attach the signed-in user's bearer token via `Authorization`\n * (managed by the SDK's TokenManager).\n *\n * Default: `true` when no `secret` is provided, `false` when `secret` is\n * set. Override explicitly for the hybrid case (provide both) or to\n * suppress the header even when logged in.\n *\n * If `true` but the user is not signed in, the header is simply omitted —\n * safe for `public` invokeMode.\n */\n authenticated?: boolean;\n payload?: Record<string, unknown>;\n}\n\nexport interface FunctionInvokeResult {\n success: boolean;\n executionId?: string;\n error?: string;\n}\n\nexport class FunctionsModule {\n private http: HttpClient;\n\n constructor(http: HttpClient) {\n this.http = http;\n }\n\n /**\n * Invoke a function.\n * Calls POST `/functions/:projectId/:functionId/invoke`, resolved against\n * `config.apiUrl` (which already carries the `/api/v1` prefix).\n *\n * Auth defaults, based on `invokeMode` semantics:\n * - webhook: pass `secret` → Authorization is NOT attached\n * - authenticated: pass nothing → Authorization IS attached (from token manager)\n * - public: pass nothing → Authorization is attached if logged in, else omitted\n * - hybrid: pass both `secret` and `authenticated: true`\n *\n * To force a specific behaviour, set `authenticated` explicitly — it wins\n * over the `secret`-based default.\n */\n async invoke(\n projectId: string,\n functionId: string,\n options: FunctionInvokeOptions = {},\n ): Promise<FunctionInvokeResult> {\n // Empty-string secret is treated as absent — both the header guard and\n // the authenticated-default check use `?.length`, so the two stay in\n // lock-step (no \"header omitted but still treated as webhook mode\").\n const hasSecret = (options.secret?.length ?? 0) > 0;\n\n const headers: Record<string, string> = {};\n if (hasSecret) {\n headers['X-Webhook-Secret'] = options.secret as string;\n }\n\n // Default: authenticate unless a webhook secret is provided.\n // Explicit `authenticated` overrides this.\n const authenticated = options.authenticated ?? !hasSecret;\n\n return this.http.request<FunctionInvokeResult>({\n method: 'POST',\n path: `/functions/${encodeURIComponent(projectId)}/${encodeURIComponent(functionId)}/invoke`,\n headers,\n body: options.payload ?? {},\n authenticated,\n });\n }\n}\n","import { HttpClient } from '../core';\n\nexport type ScheduleStatus = 'scheduled' | 'fired' | 'failed' | 'cancelled';\n\nexport interface Schedule {\n id: string;\n projectId: string;\n functionId: string;\n payload: Record<string, unknown>;\n executeAt: string;\n idempotencyKey: string | null;\n status: ScheduleStatus;\n bullJobId: string | null;\n firedAt: string | null;\n executionId: string | null;\n attemptNumber: number;\n maxAttempts: number;\n lastError: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface ScheduleInvokeOptions {\n /** 24-char hex function id to invoke when the schedule fires. */\n functionId: string;\n /** Arbitrary JSON payload surfaced to the function as `event.payload`. */\n payload?: Record<string, unknown>;\n /**\n * When to fire. Accepts a `Date`, an ISO-8601 string, or epoch ms.\n * Server-side applies a 5-second past-skew tolerance and rejects anything\n * beyond `SCHEDULE_MAX_DELAY_DAYS` (default 90 days).\n */\n executeAt: Date | string | number;\n /**\n * Dedup key. Repeating the call with the same key in the same project for\n * the same function returns the existing schedule unchanged.\n */\n idempotencyKey?: string;\n /**\n * Maximum retry attempts after a failed execution (0–10, default 3).\n * Counts only retries — the initial fire is always attempted.\n */\n maxAttempts?: number;\n}\n\nexport interface ScheduleListOptions {\n functionId?: string;\n status?: ScheduleStatus;\n limit?: number;\n offset?: number;\n}\n\nexport class ScheduleModule {\n constructor(\n private readonly http: HttpClient,\n private readonly projectId: string,\n ) {}\n\n /**\n * Schedule a one-shot function invocation. Returns the existing handle\n * unchanged if `idempotencyKey` matches a prior invoke in this project\n * for this function.\n */\n async invoke(options: ScheduleInvokeOptions): Promise<Schedule> {\n return this.http.request<Schedule>({\n method: 'POST',\n path: `/schedules/${encodeURIComponent(this.projectId)}`,\n body: {\n functionId: options.functionId,\n executeAt: this.toIsoString(options.executeAt),\n ...(options.payload !== undefined ? { payload: options.payload } : {}),\n ...(options.idempotencyKey !== undefined\n ? { idempotencyKey: options.idempotencyKey }\n : {}),\n ...(options.maxAttempts !== undefined\n ? { maxAttempts: options.maxAttempts }\n : {}),\n },\n authenticated: true,\n });\n }\n\n async get(scheduleId: string): Promise<Schedule> {\n return this.http.request<Schedule>({\n method: 'GET',\n path: `/schedules/${encodeURIComponent(this.projectId)}/${encodeURIComponent(scheduleId)}`,\n authenticated: true,\n });\n }\n\n async list(\n options: ScheduleListOptions = {},\n ): Promise<{ items: Schedule[]; total: number }> {\n return this.http.request<{ items: Schedule[]; total: number }>({\n method: 'GET',\n path: `/schedules/${encodeURIComponent(this.projectId)}`,\n query: {\n functionId: options.functionId,\n status: options.status,\n limit: options.limit,\n offset: options.offset,\n },\n authenticated: true,\n });\n }\n\n async cancel(scheduleId: string): Promise<Schedule> {\n return this.http.request<Schedule>({\n method: 'DELETE',\n path: `/schedules/${encodeURIComponent(this.projectId)}/${encodeURIComponent(scheduleId)}`,\n authenticated: true,\n });\n }\n\n private toIsoString(input: Date | string | number): string {\n if (input instanceof Date) return input.toISOString();\n if (typeof input === 'number') return new Date(input).toISOString();\n // Trust the caller's ISO string; the server validates with @IsISO8601.\n return input;\n }\n}\n","import { SpacelrClientConfig, StoredTokens } from './types';\nimport { HttpClient, TokenManager, MemoryTokenStorage, BrowserTokenStorage, RealtimeClient, ConnectionState } from './core';\nimport { AuthLostReason } from './core/token-manager';\nimport { AuthModule, StorageModule, DatabaseModule, NotificationsModule, FunctionsModule, ScheduleModule } from './modules';\n\nexport interface SpacelrClient {\n readonly auth: AuthModule;\n readonly storage: StorageModule;\n readonly db: DatabaseModule;\n readonly notifications: NotificationsModule;\n readonly functions: FunctionsModule;\n readonly schedule: ScheduleModule;\n /**\n * Store auth tokens externally (e.g. obtained via the auth-components\n * library that bypasses the SDK). Makes the tokens available to HTTP +\n * realtime clients. Refresh is on-demand: the SDK will refresh the\n * access token when it next makes a request and the token is within the\n * expiry buffer (or on a 401 retry).\n */\n setTokens(tokens: StoredTokens): Promise<void>;\n /** Clear stored tokens and reset auth-loss state. */\n clearTokens(): Promise<void>;\n /** Disconnect realtime WebSocket (if connected) */\n disconnect(): void;\n /**\n * Subscribe to auth loss. Fires in three situations:\n * - `'refresh-failed'` — a refresh call was rejected by the server\n * (refresh token revoked or expired). Note: this can also fire from a\n * *background* refresh triggered inside the expiry buffer window, at\n * which point the current access token may still be valid for up to\n * `refreshBufferSeconds` (default 60). Treat the session as over anyway;\n * the refresh token is dead.\n * - `'unauthorized'` — an authenticated request came back 401 and no\n * fresh token could be produced (no refresh token, or the retry after\n * a successful refresh was itself 401).\n *\n * Typical consumer reaction: clear UI state and show the login screen.\n * The event is deduped until `setTokens`/`clearTokens` is called again.\n */\n onAuthLost(listener: (reason: AuthLostReason) => void): () => void;\n /** Subscribe to successful token refreshes. */\n onTokenRefreshed(listener: (tokens: StoredTokens) => void): () => void;\n /**\n * Subscribe to realtime socket connection-state changes. Fires on every\n * transition (dedup: identical consecutive states are skipped). Returns\n * an unsubscribe function.\n *\n * States:\n * - `'disconnected'` — initial state before the first subscribe, or after\n * the SDK has torn the realtime socket down (e.g. on logout via the\n * internal `realtime.disconnect()`). This is a terminal state for the\n * current realtime client; reconnect happens on the next `subscribe()`.\n * - `'reconnecting'` — connecting or rebuilding the socket (initial\n * connect, socket drop, `reconnect_failed` rebuild, or a wake-up event\n * after suspend).\n * - `'connected'` — authenticated and actively listening for events.\n */\n onConnectionStateChanged(listener: (state: ConnectionState) => void): () => void;\n /** Read the current realtime socket state without waiting for an event. */\n getConnectionState(): ConnectionState;\n}\n\nexport function createClient(config: SpacelrClientConfig): SpacelrClient {\n const tokenStorage = config.tokenStorage\n ?? (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined'\n ? new BrowserTokenStorage()\n : new MemoryTokenStorage());\n const tokenManager = new TokenManager(\n tokenStorage,\n config.refreshBufferSeconds ?? 60\n );\n const httpClient = new HttpClient(config, tokenManager);\n\n const realtime = new RealtimeClient({\n baseUrl: config.apiUrl,\n getToken: () => tokenManager.getAccessToken(),\n onTokenRefreshed: (listener) =>\n tokenManager.onTokenRefreshed((tokens) => listener(tokens.accessToken)),\n });\n\n const auth = new AuthModule(httpClient, tokenManager, config);\n const storage = new StorageModule(httpClient, tokenManager, config);\n const db = new DatabaseModule(httpClient, config.projectId, realtime);\n const notifications = new NotificationsModule(httpClient);\n const functions = new FunctionsModule(httpClient);\n const schedule = new ScheduleModule(httpClient, config.projectId);\n\n return {\n auth,\n storage,\n db,\n notifications,\n functions,\n schedule,\n setTokens(tokens) {\n return tokenManager.setTokens(tokens);\n },\n clearTokens() {\n return tokenManager.clearTokens();\n },\n disconnect() {\n realtime.disconnect();\n },\n onAuthLost(listener) {\n return tokenManager.onAuthLost(listener);\n },\n onTokenRefreshed(listener) {\n return tokenManager.onTokenRefreshed(listener);\n },\n onConnectionStateChanged(listener) {\n return realtime.onConnectionStateChanged(listener);\n },\n getConnectionState() {\n return realtime.getConnectionState();\n },\n };\n}\n"],"mappings":"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","names":["FileVisibility","GrantType","CodeChallengeMethod","SharePermission","hash","callbacks","document"]}
|