@luckystack/core 0.8.1 → 0.8.3
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/CHANGELOG.md +6 -0
- package/dist/{chunk-6K7MAXMT.js → chunk-GZEHPARN.js} +1 -1
- package/dist/chunk-GZEHPARN.js.map +1 -0
- package/dist/client.js +3 -3
- package/dist/client.js.map +1 -1
- package/dist/index.js +1 -1
- package/docs/app-bootstrap.md +11 -3
- package/package.json +1 -1
- package/dist/chunk-6K7MAXMT.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.3] - 2026-07-27
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Routed API method selection is regression-tested to prefer the registered generated method map over route-name inference for explicitly declared methods such as an `organization` GET route.
|
|
15
|
+
|
|
10
16
|
## [0.8.1] - 2026-07-27
|
|
11
17
|
|
|
12
18
|
### Added
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/apiMethodMapRegistry.ts","../src/httpApiUtils.ts","../src/socketState.ts","../src/notifier.ts","../src/tryCatchSync.ts","../src/clientHookBus.ts","../src/offlineQueue.ts","../src/responseNormalizer.ts","../src/serviceRoute.ts","../src/socketEvents.ts","../src/csrfConfig.ts","../src/tryCatch.ts","../src/csrf.ts","../src/sleep.ts","../src/pageRouteValidation.ts"],"sourcesContent":["//? Generated `apiMethodMap` registry. Replaces the prefix-heuristic\n//? in `isGetMethod` (apiRequest.ts) so abort-controller selection uses\n//? the actual HTTP method declared on the handler instead of guessing\n//? from the name prefix.\n//?\n//? Wiring: the project's `src/_sockets/apiRequest.ts` wrapper imports the\n//? generated runtime map and calls `registerApiMethodMap(apiMethodMap)` once at\n//? module load, immediately before re-exporting the core request helper. Until then,\n//? lookups return `undefined` and `isGetMethod` falls back to the\n//? prefix heuristic (preserving previous behavior for transitional code).\n\nexport type HttpMethodLiteral = 'GET' | 'POST' | 'PUT' | 'DELETE';\n\nexport type ApiMethodMap = Record<string, Record<string, Record<string, HttpMethodLiteral>>>;\n\nimport { createRegistry } from './createRegistry';\n\nconst registry = createRegistry<ApiMethodMap | null>(null);\n\nexport const registerApiMethodMap = (map: ApiMethodMap): void => {\n registry.register(map);\n};\n\nexport const getRegisteredApiMethod = (\n pagePath: string,\n apiName: string,\n version: string,\n): HttpMethodLiteral | undefined => {\n const activeMap = registry.get();\n if (!activeMap) return undefined;\n //? The map shape is `Record<page, Record<name, Record<version, method>>>`;\n //? TS treats each indexed access as defined (no `noUncheckedIndexedAccess`),\n //? so the optional chains look redundant. They're not — a missing key returns\n //? undefined at runtime, which is the contract this function is documenting.\n /* eslint-disable @typescript-eslint/no-unnecessary-condition -- runtime-defensive index access */\n return activeMap[pagePath]?.[apiName]?.[version];\n /* eslint-enable @typescript-eslint/no-unnecessary-condition */\n};\n\nexport const isApiMethodMapRegistered = (): boolean => registry.isRegistered();\n","/**\n * HTTP API Utilities\n * \n * Helpers for HTTP API request handling including:\n * - Smart HTTP method detection based on API name\n * - Method validation\n */\n\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';\n\n/**\n * Single source of truth for the GET-prefix naming heuristic\n * (`get` / `fetch` / `list`). Used by both `inferHttpMethod` here and the\n * client transport's `isGetMethod` fallback in `apiRequest.ts`, so the two\n * prefix heuristics can't drift apart. Expects an already-lowercased leaf name.\n */\nexport const isGetMethodName = (methodNameLower: string): boolean =>\n methodNameLower.startsWith('get') ||\n methodNameLower.startsWith('fetch') ||\n methodNameLower.startsWith('list');\n\n/**\n * Infer the HTTP method from an API name based on common naming conventions.\n * \n * - Names starting with \"get\" → GET\n * - Names starting with \"delete\" or \"remove\" → DELETE\n * - Names starting with \"update\" or \"edit\" → PUT\n * - Everything else → POST (create operations, actions)\n * \n * @example\n * ```typescript\n * inferHttpMethod('getUserData') // → 'GET'\n * inferHttpMethod('deleteAccount') // → 'DELETE'\n * inferHttpMethod('updateProfile') // → 'PUT'\n * inferHttpMethod('createUser') // → 'POST'\n * inferHttpMethod('sendEmail') // → 'POST'\n * ```\n */\nexport const inferHttpMethod = (apiName: string): HttpMethod => {\n // Extract the method name from the full path (e.g., \"api/examples/getUserData\" → \"getUserData\")\n const nameParts = apiName.split('/');\n const lastPart = nameParts.at(-1)?.toLowerCase() ?? '';\n const methodName = /^v\\d+$/.test(lastPart)\n ? (nameParts.at(-2) ?? '').toLowerCase()\n : lastPart;\n\n if (isGetMethodName(methodName)) {\n return 'GET';\n }\n\n if (methodName.startsWith('delete') || methodName.startsWith('remove')) {\n return 'DELETE';\n }\n\n if (methodName.startsWith('update') || methodName.startsWith('edit') || methodName.startsWith('patch')) {\n return 'PUT';\n }\n\n // Default: POST for create operations and general actions\n return 'POST';\n};\n\n/**\n * Get the effective HTTP method for an API.\n * Uses explicit export if available, otherwise infers from name.\n */\nexport const getEffectiveHttpMethod = (\n apiConfig: { httpMethod?: HttpMethod },\n apiName: string\n): HttpMethod => {\n return apiConfig.httpMethod ?? inferHttpMethod(apiName);\n};\n\n/**\n * Check if an HTTP method is valid for an API.\n *\n * @param requestMethod - The method from the HTTP request\n * @param allowedMethod - The method the API accepts\n * @returns true only when the method matches the route's allowed method.\n *\n * NOTE: `OPTIONS` is intentionally NOT treated as allowed here. A custom-route\n * author wiring this into their dispatch must answer CORS preflights BEFORE the\n * route check — letting `OPTIONS` through would execute the handler on a request\n * the CSRF middleware treats as non-state-changing (CSRF-exempt), opening a\n * bypass on method-locked routes.\n */\nexport const isMethodAllowed = (\n requestMethod: string,\n allowedMethod: HttpMethod\n): boolean => {\n return requestMethod === allowedMethod;\n};\n","import type { Socket } from 'socket.io-client';\n\n// Shared, mutable socket-client state. Single source of truth for the socket\n// instance, consumed by transport helpers in @luckystack/core (apiRequest)\n// and @luckystack/sync (syncRequest), and written by the project's React\n// hook wiring in `src/_sockets/socketInitializer.ts` via `setSocket(io(...))`.\n//\n// The `export let socket` form gives importers a live binding — they always\n// see the current value after `setSocket` replaces it.\n\n// eslint-disable-next-line import-x/no-mutable-exports\nexport let socket: Socket | null = null;\n\n//? One-shot waiters: promises that resolve the moment a non-null socket is\n//? set. Replacing the original `setInterval` poll (10 ms × 500 iterations)\n//? with a subscribe-and-notify pattern eliminates the busy-loop overhead and\n//? guarantees sub-tick delivery rather than up-to-10 ms latency.\nlet waiters: ((s: Socket) => void)[] = [];\n\n//? Absolute deadline for `waitForSocket` — prevents a permanently-dangling\n//? promise in environments where the socket is never created (SSR, tests).\nconst WAIT_FOR_SOCKET_TIMEOUT_MS = 5000;\n\nexport const setSocket = (next: Socket | null): void => {\n socket = next;\n //? Resolve all pending waiters the moment a real socket arrives.\n if (next) {\n const pending = waiters;\n waiters = [];\n for (const resolve of pending) {\n resolve(next);\n }\n }\n};\n\nlet responseIndex = 0;\n\nexport const incrementResponseIndex = (): number => {\n responseIndex = responseIndex + 1;\n return responseIndex;\n};\n\nexport const waitForSocket = (): Promise<Socket | null> => {\n if (socket) return Promise.resolve(socket);\n return new Promise<Socket | null>((resolve) => {\n const timer = setTimeout(() => {\n //? Time out: remove our waiter slot and resolve with null so the\n //? caller surfaces a \"socket unavailable\" path instead of hanging.\n waiters = waiters.filter((w) => w !== onSocket);\n resolve(null);\n }, WAIT_FOR_SOCKET_TIMEOUT_MS);\n\n const onSocket = (s: Socket): void => {\n clearTimeout(timer);\n resolve(s);\n };\n\n waiters.push(onSocket);\n });\n};\n","//? Client-side notifier DI. Framework packages (apiRequest, syncRequest,\n//? offline queue flush) emit toasts on transport errors. Default is silent\n//? (no-op) so tests and SSR contexts don't need a toaster installed.\n//?\n//? The project's entry point registers a notifier backed by its UI toolkit\n//? (sonner + i18n) via `registerNotifier({ error, success, ... })`.\n\nimport { createRegistry } from './createRegistry';\n\nexport interface NotifyParam {\n key: string;\n value: string | number | boolean;\n}\n\nexport interface NotifyInput {\n key: string;\n params?: NotifyParam[];\n}\n\nexport interface Notifier {\n success: (input: NotifyInput) => void;\n error: (input: NotifyInput) => void;\n info: (input: NotifyInput) => void;\n warning: (input: NotifyInput) => void;\n}\n\nconst noopNotifier: Notifier = {\n success: () => { /* no-op */ },\n error: () => { /* no-op */ },\n info: () => { /* no-op */ },\n warning: () => { /* no-op */ },\n};\n\nconst registry = createRegistry<Notifier>(noopNotifier);\n\nexport const registerNotifier = (notifier: Notifier): void => {\n registry.register(notifier);\n};\n\nexport const getNotifier = (): Notifier => registry.get();\n\n//? Delegating wrapper matching the project's existing `notify` shape so the\n//? switchover inside framework packages is a one-line import change.\nexport const notify: Notifier = {\n success: (input) => { registry.get().success(input); },\n error: (input) => { registry.get().error(input); },\n info: (input) => { registry.get().info(input); },\n warning: (input) => { registry.get().warning(input); },\n};\n","//? Synchronous sibling of `tryCatch`. The async `tryCatch` returns a Promise\n//? and auto-captures to the error tracker — neither of which fits a hot,\n//? genuinely-synchronous path (error-formatter dispatch, cookie/CSRF parse)\n//? that must stay synchronous and decides its own logging.\n//?\n//? Unlike the async `tryCatch`, this helper does NOT auto-capture to the\n//? error tracker: the call sites that need it are synchronous fallbacks that\n//? already do their own contextual logging and deliberately keep the error\n//? path side-effect-light. Capture explicitly on the error tuple when needed.\n\n//? `P = void` mirrors the async `tryCatch<T, P = void>` so a params-less call\n//? can pass just the result type: `tryCatchSync<URL>(() => new URL(raw))`.\n//? Without the default TS demands both type args (TS2558).\nexport default function tryCatchSync<T, P = void>(\n func: (values: P) => T,\n params?: P,\n): [Error | null, T | null] {\n try {\n const response = func(params as P);\n return [null, response];\n } catch (error) {\n return [error instanceof Error ? error : new Error(String(error)), null];\n }\n}\n","//? Client-side hook bus. Counterpart to the server-side `registerHook` /\n//? `dispatchHook` system in `hookRegistry.ts`, but lives here in the\n//? browser-safe surface and listens for client-side lifecycle moments:\n//? session login, session logout (and room for `sessionUpdate` later).\n//?\n//? Fired from `react/sessionContext.ts` when `setLatestSession` observes\n//? a null → non-null (postLogin) or non-null → null (postLogout) transition.\n//? That makes the fire-point framework-controlled — consumers don't have to\n//? add their own useEffect in `SessionProvider` to detect these moments.\n//?\n//? Handlers are best-effort: errors are caught + logged but don't break the\n//? session pipeline. Async handlers fire-and-forget; the caller of\n//? `setLatestSession` does not await.\n//?\n//? Typical use:\n//? import { registerClientHook } from '@luckystack/core/client';\n//?\n//? registerClientHook('postLogin', ({ session }) => {\n//? const letter = session.name?.[0]?.toLowerCase();\n//? if (letter) joinRoom(`letter-${letter}`);\n//? });\n//?\n//? Consumers with extended session shapes get narrow typing via the same\n//? `BaseSessionLayout` module augmentation pattern the rest of the framework\n//? uses (see ARCHITECTURE_EXTENSION_POINTS.md `@luckystack/login` section).\n\nimport type { BaseSessionLayout } from './sessionTypes';\nimport { getLogger } from './loggerRegistry';\n\nexport interface ClientHookPayloadMap {\n //? Fires BEFORE the framework commits a null → session transition.\n //? Handlers may return a `ClientHookStopSignal` to veto the login (e.g.\n //? account suspended since last visit, feature-flag gate, geo block).\n //? Only fires when the consumer commits the transition through the\n //? vetoable entry point — see `proposeLogin` in `react/sessionContext.ts`.\n preLogin: { candidateSession: BaseSessionLayout };\n postLogin: { session: BaseSessionLayout };\n postLogout: { previousSession: BaseSessionLayout };\n //? Fires when the offline queue drops a request (full queue with a\n //? `drop-oldest`/`drop-newest`/`reject` policy, or an item past `maxAgeMs`).\n //? The in-memory queue is not durable across refreshes; this is the seam to\n //? surface a \"your change wasn't sent\" toast or to persist the item\n //? elsewhere. `queue` says which queue, `reason` why it was dropped.\n queueItemDropped: {\n queue: 'api' | 'sync';\n key: string;\n reason: 'queue-full' | 'expired';\n dropPolicy: 'drop-oldest' | 'drop-newest' | 'reject';\n };\n}\n\nexport type ClientHookName = keyof ClientHookPayloadMap;\n\n//? Stop signal returned by a `pre*` client hook handler to veto the\n//? framework's main flow. Mirrors the server-side `HookStopSignal` shape so\n//? `errorCode` flows through the same i18n / responseNormalizer pipeline a\n//? consumer already wires up.\nexport interface ClientHookStopSignal {\n stop: true;\n errorCode: string;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- mirrors server-side HookResult, see hooks/types.ts\nexport type ClientHookResult = void | undefined | ClientHookStopSignal;\n\nexport type ClientDispatchResult =\n | { stopped: false }\n | { stopped: true; signal: ClientHookStopSignal };\n\nexport type ClientHookHandler<N extends ClientHookName> = (\n payload: ClientHookPayloadMap[N],\n) => ClientHookResult | Promise<ClientHookResult>;\n\n//? Storage uses `unknown` handler sets so the generic `N` doesn't have to\n//? narrow at every access — the cast happens once at read/write inside the\n//? typed wrappers below, which is sound because each hook name's Set only\n//? ever sees handlers registered under that same name.\ntype AnyHandler = (payload: unknown) => ClientHookResult | Promise<ClientHookResult>;\nconst handlerSets: Partial<Record<ClientHookName, Set<AnyHandler>>> = {};\n\n/**\n * Subscribe to a client-side lifecycle hook. Returns an unsubscribe function\n * — call it to remove the handler (e.g. inside a React effect cleanup).\n *\n * Multiple handlers can be registered for the same hook; they all fire on\n * each dispatch. A handler that throws (sync) or rejects (async) is logged\n * to `console.error` and does not affect other handlers.\n */\nexport function registerClientHook<N extends ClientHookName>(\n name: N,\n handler: ClientHookHandler<N>,\n): () => void {\n const existing = handlerSets[name];\n const set = existing ?? new Set<AnyHandler>();\n if (!existing) handlerSets[name] = set;\n const wrapped = handler as AnyHandler;\n set.add(wrapped);\n return () => {\n set.delete(wrapped);\n };\n}\n\n/**\n * Fire a client-side hook (fire-and-forget). Framework-internal — consumers\n * should not call this directly. Used for `post*` hooks where any stop-signal\n * returned by a handler is intentionally ignored: a `postLogin` listener that\n * tried to \"stop\" the framework would be too late — the transition already\n * happened. Use `dispatchVetoableClientHook` for `pre*` hooks where the\n * caller actually awaits the veto decision.\n */\nexport function dispatchClientHook<N extends ClientHookName>(\n name: N,\n payload: ClientHookPayloadMap[N],\n): void {\n const set = handlerSets[name];\n if (!set || set.size === 0) return;\n //? Snapshot the set before iterating so a handler that unregisters itself\n //? (or a sibling) mid-dispatch doesn't skip un-visited handlers — Set\n //? iteration honors mid-iter deletes per ECMAScript spec, which we don't\n //? want here.\n const handlers = [...set];\n for (const handler of handlers) {\n try {\n const result = handler(payload);\n if (result && typeof (result as Promise<unknown>).catch === 'function') {\n (result as Promise<unknown>).catch((error: unknown) => {\n getLogger().error(`[clientHook:${name}] async handler rejected`, error);\n });\n }\n } catch (error) {\n getLogger().error(`[clientHook:${name}] handler threw`, error);\n }\n }\n}\n\n/**\n * Awaits every registered handler for a `pre*` client hook. If any handler\n * returns a `ClientHookStopSignal`, dispatch short-circuits and returns\n * `{ stopped: true, signal }`; callers must respect the veto (skip the\n * pending action). Handlers that throw or reject are logged but treated as\n * `undefined` — a buggy handler must not silently block the user flow.\n *\n * Mirrors `dispatchHook` from `@luckystack/core` so consumer/framework code\n * can use the same stop-signal pattern on both sides of the wire.\n */\nexport async function dispatchVetoableClientHook<N extends ClientHookName>(\n name: N,\n payload: ClientHookPayloadMap[N],\n): Promise<ClientDispatchResult> {\n const set = handlerSets[name];\n if (!set || set.size === 0) return { stopped: false };\n //? Snapshot the set (see `dispatchClientHook` for why).\n const handlers = [...set];\n for (const handler of handlers) {\n let result: ClientHookResult;\n try {\n result = await handler(payload);\n } catch (error) {\n getLogger().error(`[clientHook:${name}] vetoable handler threw`, error);\n continue;\n }\n if (result?.stop) {\n return { stopped: true, signal: result };\n }\n }\n return { stopped: false };\n}\n\n/** Test helper — drop every registered handler. Not part of the public API. */\nexport function _resetClientHooksForTests(): void {\n for (const key of Object.keys(handlerSets) as ClientHookName[]) {\n handlerSets[key]?.clear();\n }\n}\n","import { Socket } from \"socket.io-client\";\nimport { getProjectConfig } from \"./projectConfig\";\nimport { getLogger } from \"./loggerRegistry\";\nimport tryCatchSync from \"./tryCatchSync\";\nimport { dispatchClientHook } from \"./clientHookBus\";\n\n/** Why a queued item was dropped (handed to its `onDrop` callback). */\nexport type QueueDropReason = 'expired' | 'queue-full';\n\ninterface ApiQueueItem {\n id: string;\n key: string;\n run: (socketInstance: Socket) => void;\n createdAt: number;\n /**\n * Per-request override of `projectConfig.offlineQueue.dropPolicy`. See\n * `SyncQueueItem.dropPolicy` for details.\n */\n dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';\n /**\n * Optional callback invoked when THIS item is dropped (evicted for age or\n * because the queue was full / rejected) instead of ever running (SYNC-09).\n * The transport wires this to settle the pending `syncRequest`/`apiRequest`\n * promise with an error envelope, so an evicted offline request resolves\n * rather than hanging forever. Invoked at most once; failures are swallowed.\n */\n onDrop?: (reason: QueueDropReason) => void;\n}\n\ninterface SyncQueueItem {\n id: string;\n key: string;\n run: (socketInstance: Socket) => void;\n createdAt: number;\n /**\n * Per-request override of `projectConfig.offlineQueue.dropPolicy`. Lets a\n * specific sync (\"editor cursor move\") pick `'drop-oldest'` while the app\n * default stays `'reject'` for safer sends. When omitted, falls back to\n * the global config.\n */\n dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';\n /** See {@link ApiQueueItem.onDrop} (SYNC-09). */\n onDrop?: (reason: QueueDropReason) => void;\n}\n\nconst apiQueue: ApiQueueItem[] = [];\nconst syncQueue: SyncQueueItem[] = [];\n\n//? Independent flush guards so a long api flush doesn't no-op a concurrent\n//? sync flush (or vice versa).\nlet isFlushingApi = false;\nlet isFlushingSync = false;\n\nexport const isOnline = () => {\n if (typeof navigator === \"undefined\") return true;\n return navigator.onLine;\n};\n\n//? Common drop shape — every queue item the framework drops carries these.\ninterface DroppableItem {\n createdAt: number;\n key: string;\n dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';\n onDrop?: (reason: QueueDropReason) => void;\n}\n\n//? Notify a dropped item's owner exactly once. Swallows callback errors so a\n//? throwing `onDrop` can't wedge the eviction/enqueue loop (SYNC-09).\nconst notifyDropped = (item: DroppableItem, reason: QueueDropReason): void => {\n if (!item.onDrop) return;\n tryCatchSync(() => { item.onDrop?.(reason); });\n};\n\n//? Dispatch the `queueItemDropped` client hook, guarded so a misbehaving\n//? handler set can't interrupt the eviction/enqueue loop (CORE-N8).\nconst safeDispatchDropHook = (\n queue: 'api' | 'sync',\n key: string,\n reason: QueueDropReason,\n dropPolicy: 'drop-oldest' | 'drop-newest' | 'reject',\n): void => {\n tryCatchSync(() => {\n dispatchClientHook('queueItemDropped', { queue, key, reason, dropPolicy });\n });\n};\n\nconst evictExpired = (\n queue: DroppableItem[],\n label: 'api' | 'sync',\n): void => {\n const { maxAgeMs, dropPolicy: globalPolicy } = getProjectConfig().offlineQueue;\n if (maxAgeMs <= 0) return;\n const cutoff = Date.now() - maxAgeMs;\n for (let i = queue.length - 1; i >= 0; i -= 1) {\n const item = queue[i];\n if (item && item.createdAt < cutoff) {\n queue.splice(i, 1);\n notifyDropped(item, 'expired');\n safeDispatchDropHook(label, item.key, 'expired', item.dropPolicy ?? globalPolicy);\n }\n }\n};\n\nconst enqueueWithPolicy = <T extends DroppableItem>(\n queue: T[],\n item: T,\n label: 'api' | 'sync',\n): boolean => {\n evictExpired(queue, label);\n const { maxSize, dropPolicy: globalPolicy } = getProjectConfig().offlineQueue;\n //? Per-request override wins over the global policy. Lets specific\n //? syncs pick 'drop-oldest' for liveness while the app default stays\n //? 'reject' for safer sends.\n const effectivePolicy = item.dropPolicy ?? globalPolicy;\n if (queue.length < maxSize) {\n queue.push(item);\n return true;\n }\n if (effectivePolicy === 'drop-oldest') {\n const dropped = queue.shift();\n queue.push(item);\n getLogger().debug(`offlineQueue:${label} full — dropped oldest item (policy=${effectivePolicy})`);\n if (dropped) {\n notifyDropped(dropped, 'queue-full');\n safeDispatchDropHook(label, dropped.key, 'queue-full', effectivePolicy);\n }\n return true;\n }\n if (effectivePolicy === 'drop-newest') {\n getLogger().debug(`offlineQueue:${label} full — dropped newest item (policy=${effectivePolicy})`);\n notifyDropped(item, 'queue-full');\n safeDispatchDropHook(label, item.key, 'queue-full', effectivePolicy);\n return false;\n }\n getLogger().warn(`offlineQueue:${label} full — rejecting enqueue (policy=${effectivePolicy})`);\n notifyDropped(item, 'queue-full');\n safeDispatchDropHook(label, item.key, 'queue-full', effectivePolicy);\n return false;\n};\n\nexport const enqueueApiRequest = (item: ApiQueueItem): boolean =>\n enqueueWithPolicy(apiQueue, item, 'api');\n\nexport const enqueueSyncRequest = (item: SyncQueueItem): boolean =>\n enqueueWithPolicy(syncQueue, item, 'sync');\n\nexport const removeApiQueueItem = (id: string) => {\n const index = apiQueue.findIndex((item) => item.id === id);\n if (index !== -1) {\n apiQueue.splice(index, 1);\n }\n};\n\nexport const removeSyncQueueItem = (id: string) => {\n const index = syncQueue.findIndex((item) => item.id === id);\n if (index !== -1) {\n syncQueue.splice(index, 1);\n }\n};\n\nexport const removeApiQueueItemsByKey = (key: string) => {\n for (let i = apiQueue.length - 1; i >= 0; i -= 1) {\n if (apiQueue[i]?.key === key) {\n apiQueue.splice(i, 1);\n }\n }\n};\n\n//? Run one dequeued item. If `run` throws, requeue it at the FRONT (so the\n//? next flush retries it in order) and signal the caller to stop this flush —\n//? otherwise a throwing item is silently lost AND the `isFlushing*` guard\n//? would be left stuck `true`, wedging every future flush.\nconst runQueueItem = <T extends { run: (s: Socket) => void }>(\n queue: T[],\n item: T,\n socketInstance: Socket,\n label: string,\n): boolean => {\n const [error] = tryCatchSync(() => { item.run(socketInstance); });\n if (error) {\n queue.unshift(item);\n getLogger().warn(`offlineQueue:${label} run threw — requeued item, pausing flush`, { error });\n return false;\n }\n return true;\n};\n\nexport const flushApiQueue = (canRun: () => boolean, socketInstance: Socket) => {\n if (isFlushingApi) return;\n evictExpired(apiQueue, 'api');\n if (apiQueue.length === 0) return;\n if (!canRun()) return;\n\n isFlushingApi = true;\n while (apiQueue.length > 0) {\n if (!canRun()) break;\n const item = apiQueue.shift();\n if (item && !runQueueItem(apiQueue, item, socketInstance, 'api')) break;\n }\n isFlushingApi = false;\n};\n\nexport const flushSyncQueue = (canRun: () => boolean, socketInstance: Socket) => {\n if (isFlushingSync) return;\n evictExpired(syncQueue, 'sync');\n if (syncQueue.length === 0) return;\n if (!canRun()) return;\n\n isFlushingSync = true;\n while (syncQueue.length > 0) {\n if (!canRun()) break;\n const item = syncQueue.shift();\n if (item && !runQueueItem(syncQueue, item, socketInstance, 'sync')) break;\n }\n isFlushingSync = false;\n};\n\nexport const getApiQueueSize = () => apiQueue.length;\nexport const getSyncQueueSize = () => syncQueue.length;\n","export interface ErrorParam {\n key: string;\n value: string | number | boolean;\n}\n\nexport interface ErrorResponseInput {\n status?: unknown;\n errorCode?: unknown;\n errorParams?: unknown;\n httpStatus?: unknown;\n}\n\nexport interface NormalizedErrorResponse {\n status: 'error';\n message: string;\n errorCode: string;\n errorParams?: ErrorParam[];\n httpStatus?: number;\n}\n\nexport const INVALID_ERROR_RESPONSE_CODE = 'error.invalidResponse';\n\nexport const isErrorParamArray = (value: unknown): value is ErrorParam[] => {\n return Array.isArray(value) && value.every((item) => {\n return (\n !!item\n && typeof item === 'object'\n && typeof (item as ErrorParam).key === 'string'\n && ['string', 'number', 'boolean'].includes(typeof (item as ErrorParam).value)\n );\n });\n};\n\nexport const normalizeErrorResponseCore = ({\n response,\n fallbackHttpStatus,\n fallbackErrorCode,\n resolveMessage,\n}: {\n response: ErrorResponseInput;\n fallbackHttpStatus?: number;\n fallbackErrorCode?: string;\n resolveMessage?: (args: { errorCode: string; errorParams?: ErrorParam[] }) => string;\n}): NormalizedErrorResponse => {\n const normalizedParams = isErrorParamArray(response.errorParams) ? response.errorParams : undefined;\n const errorCodeValue = typeof response.errorCode === 'string' ? response.errorCode.trim() : '';\n const hasErrorCode = errorCodeValue.length > 0;\n const finalErrorCode = hasErrorCode ? errorCodeValue : (fallbackErrorCode ?? INVALID_ERROR_RESPONSE_CODE);\n const providedHttpStatus = typeof response.httpStatus === 'number' ? response.httpStatus : undefined;\n\n const message = resolveMessage\n ? resolveMessage({ errorCode: finalErrorCode, errorParams: normalizedParams })\n : finalErrorCode;\n\n return {\n status: 'error',\n message,\n errorCode: finalErrorCode,\n errorParams: normalizedParams,\n httpStatus: providedHttpStatus ?? fallbackHttpStatus,\n };\n};\n\nexport const defaultHttpStatusForResponse = ({\n status,\n explicitHttpStatus,\n fallbackErrorStatus = 400,\n}: {\n status: 'success' | 'error';\n explicitHttpStatus?: number;\n fallbackErrorStatus?: number;\n}): number => {\n if (typeof explicitHttpStatus === 'number') return explicitHttpStatus;\n return status === 'success' ? 200 : fallbackErrorStatus;\n};\n","const VERSION_SEGMENT_REGEX = /^v\\d+$/;\n\nexport interface ServiceRouteParseSuccess {\n status: 'success';\n normalizedRouteName: string;\n service: string;\n routeName: string;\n}\n\nexport interface ServiceRouteParseError {\n status: 'error';\n reason: string;\n}\n\nexport type ServiceRouteParseResult = ServiceRouteParseSuccess | ServiceRouteParseError;\n\nexport interface TransportRouteParseSuccess {\n status: 'success';\n normalizedFullName: string;\n version: string;\n serviceRoute: ServiceRouteParseSuccess;\n}\n\nexport interface TransportRouteParseError {\n status: 'error';\n reason: string;\n}\n\nexport type TransportRouteParseResult = TransportRouteParseSuccess | TransportRouteParseError;\n\nconst normalizeRawRoute = (value: string): string => {\n return value.trim().replaceAll('\\\\', '/').replaceAll(/^\\/+/g, '').replaceAll(/\\/+$/g, '');\n};\n\nconst hasEmptySegments = (value: string): boolean => {\n return value.split('/').some((segment) => segment.length === 0);\n};\n\nexport const parseServiceRouteName = (value: string): ServiceRouteParseResult => {\n const normalized = normalizeRawRoute(value);\n if (!normalized) {\n return { status: 'error', reason: 'Route name cannot be empty.' };\n }\n\n if (hasEmptySegments(normalized)) {\n return { status: 'error', reason: 'Route name cannot contain empty segments.' };\n }\n\n const segments = normalized.split('/');\n if (segments.length < 2) {\n return { status: 'error', reason: 'Route name must include service and route segments.' };\n }\n\n const [service, ...routeSegments] = segments;\n const routeName = routeSegments.join('/');\n\n if (!service || !routeName) {\n return { status: 'error', reason: 'Route name must include service and route segments.' };\n }\n\n return {\n status: 'success',\n normalizedRouteName: `${service}/${routeName}`,\n service,\n routeName,\n };\n};\n\nexport const parseTransportRouteName = ({\n value,\n prefix,\n}: {\n value: string;\n prefix: 'api' | 'sync';\n}): TransportRouteParseResult => {\n const normalized = normalizeRawRoute(value);\n if (!normalized) {\n return { status: 'error', reason: 'Transport route cannot be empty.' };\n }\n\n if (hasEmptySegments(normalized)) {\n return { status: 'error', reason: 'Transport route cannot contain empty segments.' };\n }\n\n const segments = normalized.split('/');\n const routeSegments = segments[0] === prefix ? segments.slice(1) : segments;\n\n if (routeSegments.length < 3) {\n return {\n status: 'error',\n reason: `Transport route must use ${prefix}/{service}/{name}/{version} format.`,\n };\n }\n\n const version = routeSegments.at(-1);\n if (!version || !VERSION_SEGMENT_REGEX.test(version)) {\n return { status: 'error', reason: 'Route version segment must match v{number}.' };\n }\n\n const serviceRouteToken = routeSegments.slice(0, -1).join('/');\n const serviceRoute = parseServiceRouteName(serviceRouteToken);\n if (serviceRoute.status === 'error') {\n return {\n status: 'error',\n reason: serviceRoute.reason,\n };\n }\n\n return {\n status: 'success',\n normalizedFullName: `${prefix}/${serviceRoute.normalizedRouteName}/${version}`,\n version,\n serviceRoute,\n };\n};\n","const withIndex = (prefix: string, responseIndex: number | string): string => {\n return `${prefix}${String(responseIndex)}`;\n};\n\nexport const socketEventNames = {\n apiRequest: 'apiRequest',\n apiCancel: 'apiCancel',\n sync: 'sync',\n syncCancel: 'syncCancel',\n joinRoom: 'joinRoom',\n leaveRoom: 'leaveRoom',\n getJoinedRooms: 'getJoinedRooms',\n updateLocation: 'updateLocation',\n updateSession: 'updateSession',\n sessionReplaced: 'sessionReplaced',\n logout: 'logout',\n\n intentionalDisconnect: 'intentionalDisconnect',\n intentionalReconnect: 'intentionalReconnect',\n //? Client -> server activity heartbeat (mouse/keyboard/touch), throttled.\n //? Drives the server-side activity sampler that fires registered activity\n //? events (e.g. the built-in AFK detector).\n activity: 'activity',\n userAfk: 'userAfk',\n userBack: 'userBack',\n //? Emitted to a room's remaining members when a peer leaves — hard disconnect\n //? (socket close) or grace-expiry (presence MIS-003). Lets clients prune a\n //? departed peer from their roster without polling. The presence package owns\n //? the dispatch; core owns the wire-event name so client + server can't drift.\n userLeft: 'userLeft',\n\n connect: 'connect',\n disconnect: 'disconnect',\n reconnectAttempt: 'reconnect_attempt',\n connectError: 'connect_error',\n\n apiResponsePrefix: 'apiResponse-',\n apiStreamPrefix: 'apiStream-',\n syncResponsePrefix: 'sync-',\n syncProgressPrefix: 'sync-progress-',\n joinRoomResponsePrefix: 'joinRoom-',\n leaveRoomResponsePrefix: 'leaveRoom-',\n getJoinedRoomsResponsePrefix: 'getJoinedRooms-',\n} as const;\n\nexport const buildApiResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.apiResponsePrefix, responseIndex);\n};\n\nexport const buildApiStreamEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.apiStreamPrefix, responseIndex);\n};\n\nexport const buildSyncResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.syncResponsePrefix, responseIndex);\n};\n\nexport const buildSyncProgressEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.syncProgressPrefix, responseIndex);\n};\n\nexport const buildJoinRoomResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.joinRoomResponsePrefix, responseIndex);\n};\n\nexport const buildLeaveRoomResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.leaveRoomResponsePrefix, responseIndex);\n};\n\nexport const buildGetJoinedRoomsResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.getJoinedRoomsResponsePrefix, responseIndex);\n};\n","//? CSRF configuration registry. The framework's CSRF middleware\n//? (`packages/server/src/httpRoutes/csrfMiddleware.ts`) and the\n//? client-side fetch wrapper (`packages/core/src/csrf.ts`) both consult\n//? `getCsrfConfig()` so consumers can change the header name, cookie\n//? name, token length, or cookie options without forking.\n//?\n//? The default values match the framework's pre-registry hardcodes\n//? (header `x-csrf-token`, 32-byte hex token minted at session create) so\n//? consumers who never call `registerCsrfConfig` get the historical\n//? behaviour unchanged.\n\nimport { createRegistry } from './createRegistry';\n\nexport interface CsrfCookieOptions {\n sameSite?: 'strict' | 'lax' | 'none';\n secure?: boolean;\n /**\n * MUST be false for CSRF cookies — the client needs to read the value\n * with JS to attach it as a request header. The framework reads the\n * value from the session record (Redis-side) rather than the cookie,\n * so this flag is documentation/intent only.\n */\n httpOnly?: boolean;\n path?: string;\n maxAgeMs?: number;\n}\n\nexport interface CsrfConfig {\n /** Cookie name used to deliver the CSRF token to the browser. */\n cookieName: string;\n /** Request-header name the client attaches on state-changing requests. */\n headerName: string;\n /** Length in bytes of the random token minted by the session layer. */\n tokenLength: number;\n cookieOptions: CsrfCookieOptions;\n}\n\nexport const DEFAULT_CSRF_CONFIG: CsrfConfig = {\n cookieName: 'csrf-token',\n headerName: 'x-csrf-token',\n tokenLength: 32,\n cookieOptions: {\n sameSite: 'lax',\n //? `secure` intentionally UNSET so it resolves per-environment at serialize\n //? time (csrfRoute.ts) the SAME way the session cookie does — emit `Secure` in\n //? prod (SECURE=true) but NOT over plain HTTP in dev. A hardcoded `true` made\n //? the browser drop the login-absent double-submit cookie over http://, so\n //? every state-changing request 403'd. Force it on explicitly via\n //? `registerCsrfConfig({ cookieOptions: { secure: true } })` if desired.\n httpOnly: false,\n path: '/',\n maxAgeMs: 24 * 60 * 60 * 1000,\n },\n};\n\nconst registry = createRegistry<CsrfConfig, Partial<CsrfConfig>>(DEFAULT_CSRF_CONFIG, {\n //? Merge shallowly with the CURRENT config; `cookieOptions` is deep-merged\n //? so a partial override of (say) `sameSite` does not clobber `path` /\n //? `maxAgeMs`. Last-write-wins, and successive calls accumulate.\n transform: (input, current) => ({\n ...current,\n ...input,\n cookieOptions: { ...current.cookieOptions, ...input.cookieOptions },\n }),\n});\n\n/**\n * Override one or more CSRF settings. Merges shallowly with the current\n * config; `cookieOptions` is deep-merged so a partial override of (say)\n * `sameSite` does not clobber `path` / `maxAgeMs`. Last-write-wins.\n */\nexport const registerCsrfConfig = (input: Partial<CsrfConfig>): void => {\n registry.register(input);\n};\n\n/** Read the active config at call time (never at module load). */\nexport const getCsrfConfig = (): CsrfConfig => registry.get();\n\n/** Test-only — restore defaults between scenarios. */\nexport const resetCsrfConfigForTests = (): void => {\n registry.reset();\n};\n","import { captureException } from './sentrySetup';\n\nexport default async function tryCatch<T, P = void>(\n func: (values: P) => Promise<T> | T,\n params?: P,\n context?: Record<string, unknown>\n): Promise<[Error | null, T | null]> {\n try {\n const response = await func(params as P);\n return [null, response];\n } catch (error) {\n captureException(error, context);\n return [error instanceof Error ? error : new Error(String(error)), null];\n }\n}\n","//? Client-side CSRF helper. Lazily fetches and caches the session's CSRF\n//? token from `/auth/csrf`, then attaches it as `x-csrf-token` on\n//? subsequent HTTP fetches in cookie mode.\n//?\n//? Token-mode sessions are CSRF-immune because cross-origin POSTs do not\n//? auto-attach the sessionStorage value; the cache simply stays empty in\n//? that mode.\n\nimport { getCsrfConfig } from './csrfConfig';\nimport { getProjectConfig } from './projectConfig';\nimport { socket } from './socketState';\nimport tryCatch from './tryCatch';\nimport tryCatchSync from './tryCatchSync';\n\n//? Tokens are scoped to the HTTP invocation origin. In routed-http mode the\n//? Socket.io connection may terminate on a remote `system` service while the\n//? invocation goes same-origin through a local router; a single socket-derived\n//? cache would fetch the CSRF token from the wrong ingress.\nconst cachedTokens = new Map<string, string>();\nconst inflightFetches = new Map<string, Promise<string | null>>();\n\nconst resolveSocketOrigin = (): string => socket?.io.opts.hostname\n ? `${socket.io.opts.secure ? 'https' : 'http'}://${socket.io.opts.hostname}${socket.io.opts.port ? `:${socket.io.opts.port}` : ''}`\n : '';\n\nconst resolveRequestOrigin = (input?: RequestInfo | URL): string => {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- SSR runtime guard\n const browserOrigin = globalThis.window !== undefined && globalThis.location?.origin\n ? globalThis.location.origin\n : '';\n if (input !== undefined) {\n let raw: string;\n if (typeof input === 'string') raw = input;\n else if (input instanceof URL) raw = input.href;\n else raw = input.url;\n const fallbackOrigin = browserOrigin.length > 0 ? browserOrigin : resolveSocketOrigin();\n const [error, parsed] = tryCatchSync(() => new URL(raw, fallbackOrigin));\n if (!error && parsed) return parsed.origin;\n }\n return browserOrigin.length > 0 ? browserOrigin : resolveSocketOrigin();\n};\n\nconst fetchCsrfToken = async (origin: string): Promise<string | null> => {\n const base = origin;\n const [error, token] = await tryCatch<string | null, undefined>(async () => {\n const response = await fetch(`${base}/auth/csrf`, {\n method: 'GET',\n credentials: 'include',\n });\n if (!response.ok) return null;\n const body = (await response.json()) as { csrfToken?: string | null };\n return body.csrfToken ?? null;\n });\n if (error) return null;\n return token ?? null;\n};\n\n/**\n * Fetch (or return the cached) CSRF token for the active session. Returns\n * null in token mode or when no session exists. Callers should attach the\n * returned value as the `X-CSRF-Token` request header.\n */\nexport const getCsrfToken = async (origin = resolveRequestOrigin()): Promise<string | null> => {\n //? Token-mode sessions: skip entirely. The server doesn't enforce CSRF\n //? in token mode, and there's no value to attach.\n if (getProjectConfig().session.basedToken) {\n return null;\n }\n\n const cached = cachedTokens.get(origin);\n if (cached) return cached;\n const inflight = inflightFetches.get(origin);\n if (inflight) return inflight;\n\n const next = fetchCsrfToken(origin).then((token) => {\n if (token) cachedTokens.set(origin, token);\n inflightFetches.delete(origin);\n return token;\n });\n inflightFetches.set(origin, next);\n return next;\n};\n\n/** Drop the cached token. Call this on logout or when a 403 csrfMismatch is seen. */\nexport const clearCsrfToken = (): void => {\n cachedTokens.clear();\n inflightFetches.clear();\n};\n\nconst STATE_CHANGING = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);\n\nconst isCsrfMismatchResponse = async (response: Response): Promise<boolean> => {\n if (response.status !== 403) return false;\n const [error, isMismatch] = await tryCatch<boolean, undefined>(async () => {\n //? `response.clone()` is used here so the ORIGINAL response body stream is\n //? preserved for the caller (the clone is consumed, the original is not).\n //? This is important: `httpFetch` returns the original `response` reference\n //? to the caller so it can read its body, and cloning before reading is the\n //? correct way to peek at the body without consuming it.\n const cloned = response.clone();\n const body = (await cloned.json()) as { errorCode?: string };\n return body.errorCode === 'auth.csrfMismatch';\n });\n if (error) return false;\n return isMismatch ?? false;\n};\n\n/**\n * Drop-in replacement for `fetch` that automatically attaches the framework's\n * `X-CSRF-Token` and `X-Request-Id` headers to state-changing requests in\n * cookie mode. Use this for any project-side HTTP call that hits a\n * LuckyStack `/api/*`, `/sync/*`, or `/auth/api/*` endpoint.\n *\n * Behavior:\n * - GET / OPTIONS / HEAD: passes through without CSRF (read-only).\n * - Token mode: passes through without CSRF (immune by design).\n * - Cookie mode + state-changing method: lazily fetches `/auth/csrf` once,\n * caches, attaches as `x-csrf-token`. On a 403 `auth.csrfMismatch`\n * response, clears the cache and retries once.\n * - `credentials: 'include'` is forced unless the caller overrode it.\n *\n * @example\n * import { httpFetch } from '@luckystack/core/client';\n * const res = await httpFetch('/api/system/myRoute/v1', {\n * method: 'POST',\n * body: JSON.stringify({ ... }),\n * });\n */\nexport const httpFetch: typeof fetch = async (input, init = {}) => {\n const method = (init.method ?? 'GET').toUpperCase();\n const headers = new Headers(init.headers);\n const config = getProjectConfig();\n if (config.session.basedToken && !headers.has('Authorization')) {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- SSR runtime guard\n const token = globalThis.window === undefined\n ? null\n : tryCatchSync(() => globalThis.sessionStorage.getItem('token'))[1];\n if (token) headers.set('Authorization', `Bearer ${token}`);\n }\n if (!headers.has('Content-Type') && init.body && typeof init.body === 'string') {\n headers.set('Content-Type', 'application/json');\n }\n\n const csrfHeaderName = getCsrfConfig().headerName;\n const send = async (csrfToken: string | null): Promise<Response> => {\n const finalHeaders = new Headers(headers);\n if (csrfToken && STATE_CHANGING.has(method) && !finalHeaders.has(csrfHeaderName)) {\n finalHeaders.set(csrfHeaderName, csrfToken);\n }\n return fetch(input, {\n ...init,\n method,\n headers: finalHeaders,\n credentials: init.credentials ?? 'include',\n });\n };\n\n if (!STATE_CHANGING.has(method) || config.session.basedToken) {\n return send(null);\n }\n\n const requestOrigin = resolveRequestOrigin(input);\n const token = await getCsrfToken(requestOrigin);\n let response = await send(token);\n\n //? On csrfMismatch, the cached token is stale (session rotated). Clear and\n //? retry ONCE with a fresh fetch — covers the case where the user logged\n //? back in while a tab was idle. The retry is intentionally limited to one\n //? attempt: a second csrfMismatch means the session is broken in a way a\n //? fresh token won't fix (e.g. session expired), and retrying would loop.\n if (await isCsrfMismatchResponse(response)) {\n clearCsrfToken();\n const refreshed = await getCsrfToken(requestOrigin);\n if (refreshed) {\n response = await send(refreshed);\n }\n }\n\n return response;\n};\n","export default function sleep(ms: number) {\n return new Promise<void>((resolve) => setTimeout(resolve, ms));\n}\n","//? Pure page-route validator. Lives in @luckystack/core (no Node deps) so\n//? both the client-side router (`src/main.tsx`'s `getRoutes()`) AND the\n//? devkit-side scaffold CLI / hot-reload validator can consume it through\n//? the same single source of truth.\n//?\n//? Implements the invisible-parent folder convention: a folder starting\n//? with `privateFolderPrefix` is stripped from the URL but its CHILDREN\n//? remain routeable. A `page.tsx` placed directly inside such a folder\n//? (no non-private sibling segment) is invalid because it has no URL.\n//?\n//? Reserved framework folders (`_api`, `_sync`, `_components`, …) NEVER\n//? host a page, even nested — placing `page.tsx` anywhere under one of\n//? them is a hard error.\n\nexport interface PageRouteRules {\n /**\n * Single-character prefix that marks a folder as **invisible-parent** for\n * page routing. Default: `'_'`.\n */\n privateFolderPrefix: string;\n /**\n * Folder names that are reserved for framework-internal use and may NEVER\n * host a `page.tsx`, nested or otherwise. Scaffold + page-discovery emit\n * a hard error when a page is placed inside one.\n */\n scaffoldIgnoredFolders: readonly string[];\n}\n\nexport const DEFAULT_PAGE_ROUTE_RULES: PageRouteRules = {\n privateFolderPrefix: '_',\n scaffoldIgnoredFolders: [\n '_api',\n '_sync',\n '_function',\n '_functions',\n '_component',\n '_components',\n '_provider',\n '_providers',\n '_locale',\n '_locales',\n '_socket',\n '_sockets',\n '_shared',\n '_server',\n ],\n};\n\nexport interface PagePathValidationResult {\n /** `true` when the page file produces a routeable URL. */\n valid: boolean;\n /** Computed URL when `valid`. Always begins with `/`. `/` for root page. */\n route?: string;\n /** Human-readable explanation when `!valid`. */\n reason?: string;\n}\n\n/**\n * Validate a `page.tsx` file path relative to `src/`. Accepts both\n * forward and back slashes, with or without a leading `./` or `/`.\n * The filename **must** be `page.tsx` (or `page.jsx`); other filenames\n * return `valid: false`.\n *\n * Examples (defaults):\n * admin/page.tsx -> { valid: true, route: '/admin' }\n * _housing/renting/page.tsx -> { valid: true, route: '/renting' }\n * _housing/page.tsx -> { valid: false, reason: 'no URL segment left after stripping underscore folders' }\n * _api/something/page.tsx -> { valid: false, reason: 'page.tsx cannot live inside reserved folder _api' }\n * page.tsx -> { valid: true, route: '/' }\n * some/route.tsx -> { valid: false, reason: 'not a page file (expected page.tsx)' }\n */\nexport const validatePagePath = (\n srcRelativePath: string,\n rules: PageRouteRules = DEFAULT_PAGE_ROUTE_RULES,\n): PagePathValidationResult => {\n const normalized = srcRelativePath.replaceAll('\\\\', '/').replace(/^\\.\\//, '').replace(/^\\/+/, '');\n const segments = normalized.split('/').filter((segment) => segment.length > 0);\n\n if (segments.length === 0) {\n return { valid: false, reason: 'empty path' };\n }\n\n const filename = segments.at(-1);\n if (filename !== 'page.tsx' && filename !== 'page.jsx') {\n return { valid: false, reason: 'not a page file (expected page.tsx)' };\n }\n\n const folderSegments = segments.slice(0, -1);\n const prefix = rules.privateFolderPrefix;\n const reserved = rules.scaffoldIgnoredFolders;\n\n for (const reservedName of reserved) {\n if (folderSegments.includes(reservedName)) {\n return { valid: false, reason: `page.tsx cannot live inside reserved folder ${reservedName}` };\n }\n }\n\n const visibleSegments = folderSegments.filter((segment) => !segment.startsWith(prefix));\n\n if (folderSegments.length > 0 && visibleSegments.length === 0) {\n return { valid: false, reason: 'no URL segment left after stripping underscore folders' };\n }\n\n const route = visibleSegments.length === 0 ? '/' : `/${visibleSegments.join('/')}`;\n return { valid: true, route };\n};\n"],"mappings":"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","names":["registry","responseIndex","registry","token"]}
|
package/dist/client.js
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
tryCatchSync,
|
|
42
42
|
validatePagePath,
|
|
43
43
|
waitForSocket
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-GZEHPARN.js";
|
|
45
45
|
import {
|
|
46
46
|
getLogger,
|
|
47
47
|
registerLogger
|
|
@@ -286,7 +286,7 @@ var routedApiErrorCode = (kind) => {
|
|
|
286
286
|
if (kind === "invalid-response") return "api.invalidServerResponse";
|
|
287
287
|
return "api.ioUnavailable";
|
|
288
288
|
};
|
|
289
|
-
var
|
|
289
|
+
var resolveApiRequestHttpMethod = (apiName, version) => {
|
|
290
290
|
const lastSlash = apiName.lastIndexOf("/");
|
|
291
291
|
if (lastSlash > 0) {
|
|
292
292
|
const pagePath = apiName.slice(0, lastSlash);
|
|
@@ -535,7 +535,7 @@ function apiRequest(params) {
|
|
|
535
535
|
const effectiveTimeoutMs2 = timeoutMs ?? getProjectConfig().api.requestTimeoutMs;
|
|
536
536
|
const routed = await invokeRoutedHttp({
|
|
537
537
|
path: `/api/${sanitizedName}/${version}`,
|
|
538
|
-
method:
|
|
538
|
+
method: resolveApiRequestHttpMethod(sanitizedName, version),
|
|
539
539
|
data,
|
|
540
540
|
stream: onStream,
|
|
541
541
|
signals: [signal, externalSignal],
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/apiInterceptors.ts","../src/tryCatchClient.ts","../src/routedHttpInvocation.ts","../src/apiRequest.ts","../src/localesRegistry.ts","../src/middlewareRegistry.ts","../src/react/sessionContext.ts","../src/react/AvatarProvider.tsx","../src/react/useTheme.ts","../src/react/TranslationProvider.tsx","../src/react/notify.ts","../src/react/Middleware.tsx","../src/react/Router.tsx"],"sourcesContent":["//? EXT-03 — client-side request/response interceptor registry for `apiRequest`.\n//?\n//? `apiRequest` (apiRequest.ts) is a monolithic typed function and Rule 21 /\n//? the `no-unsafe-api-wrappers` lint forbid wrapping it (a wrapper erases the\n//? route/version inference). This registry is the sanctioned escape hatch: a\n//? consumer registers an interceptor here instead of wrapping the call, so the\n//? typed call site is untouched and route/version inference survives.\n//?\n//? Two seams, both fire-and-mutate (NOT replace — the dispatcher reserves a\n//? structured return for control flow):\n//? - request interceptors run just before the socket emit. A handler may\n//? mutate `ctx.data` in place (inject a correlation id / feature-flag\n//? context / tenant header-equivalent) and is observation-only otherwise.\n//? - response interceptors run when the response envelope arrives, before the\n//? awaiting caller resolves. Observation-only (metrics, breadcrumb, custom\n//? logging). The envelope is NOT mutated by this seam — response rewriting\n//? belongs on the server `preApiRespond` / `transformApiResponse` hooks.\n//?\n//? Handlers are best-effort: a throw / rejection is caught + logged and never\n//? breaks the request pipeline. Both `register*` functions return an\n//? unsubscribe closure, matching `registerClientHook`'s contract.\n\nexport interface ApiRequestInterceptorContext {\n /** Resolved route name (`<page>/<api>` or `system/<api>`). */\n name: string;\n /** Route version literal (e.g. `'v1'`). */\n version: string;\n /**\n * Outgoing request payload. Mutate this object in place to augment the\n * request (correlation id, feature-flag context, ...). Replacing the\n * reference has no effect — mutate keys on the existing object.\n */\n data: Record<string, unknown>;\n}\n\nexport interface ApiResponseInterceptorContext {\n name: string;\n version: string;\n /** The response envelope the caller is about to receive (read-only intent). */\n response: { status: 'success' | 'error'; [key: string]: unknown };\n}\n\nexport type ApiRequestInterceptor = (\n ctx: ApiRequestInterceptorContext,\n) => void | Promise<void>;\n\nexport type ApiResponseInterceptor = (\n ctx: ApiResponseInterceptorContext,\n) => void | Promise<void>;\n\nimport { getLogger } from './loggerRegistry';\n\nconst requestInterceptors = new Set<ApiRequestInterceptor>();\nconst responseInterceptors = new Set<ApiResponseInterceptor>();\n\n/**\n * Register an outgoing-request interceptor. Returns an unsubscribe function.\n * Multiple interceptors run in registration order; each may mutate `ctx.data`.\n */\nexport function registerApiRequestInterceptor(interceptor: ApiRequestInterceptor): () => void {\n requestInterceptors.add(interceptor);\n return () => {\n requestInterceptors.delete(interceptor);\n };\n}\n\n/**\n * Register an incoming-response interceptor. Returns an unsubscribe function.\n */\nexport function registerApiResponseInterceptor(interceptor: ApiResponseInterceptor): () => void {\n responseInterceptors.add(interceptor);\n return () => {\n responseInterceptors.delete(interceptor);\n };\n}\n\n//? Framework-internal — invoked by `apiRequest`. Awaited so a handler can do an\n//? async lookup (read a flag store, mint a trace id) before the emit. Snapshot\n//? the set so a handler unregistering mid-dispatch can't skip a sibling.\nexport async function dispatchApiRequestInterceptors(\n ctx: ApiRequestInterceptorContext,\n): Promise<void> {\n if (requestInterceptors.size === 0) return;\n //? Snapshot so a handler that unregisters itself (or a sibling) mid-dispatch\n //? doesn't skip an un-visited handler.\n const interceptors = [...requestInterceptors];\n for (const interceptor of interceptors) {\n try {\n await interceptor(ctx);\n } catch (error) {\n getLogger().error('[apiRequestInterceptor] handler threw', error);\n }\n }\n}\n\n//? Framework-internal — invoked by `apiRequest` when the response arrives.\n//? Fire-and-forget (the caller does not await) so an interceptor can't delay\n//? the resolve; async rejections are caught + logged.\nexport function dispatchApiResponseInterceptors(\n ctx: ApiResponseInterceptorContext,\n): void {\n if (responseInterceptors.size === 0) return;\n //? Snapshot (see `dispatchApiRequestInterceptors`).\n const interceptors = [...responseInterceptors];\n for (const interceptor of interceptors) {\n try {\n const result = interceptor(ctx);\n if (result && typeof (result as Promise<unknown>).catch === 'function') {\n (result as Promise<unknown>).catch((error: unknown) => {\n getLogger().error('[apiResponseInterceptor] async handler rejected', error);\n });\n }\n } catch (error) {\n getLogger().error('[apiResponseInterceptor] handler threw', error);\n }\n }\n}\n\n/** Test helper — drop every registered interceptor. Not part of the public API. */\nexport function _resetApiInterceptorsForTests(): void {\n requestInterceptors.clear();\n responseInterceptors.clear();\n}\n","//? Browser-safe sibling of `tryCatch`. Behaviourally identical (same tuple,\n//? same params, same auto-capture on the error path) but it does NOT statically\n//? import `./sentrySetup`. The server `tryCatch.ts` reaches `sentrySetup` →\n//? `errorTrackerRegistry` → `node:async_hooks` at module-eval; that static edge\n//? is harmless on the server but drags a `node:`-externalized module into the\n//? Vite CLIENT bundle whenever a client importer (LoginForm) pulls\n//? `shared/tryCatch.ts`. So the client-reachable shim resolves HERE instead, and\n//? the only path to capture is a LAZY dynamic import taken on the error branch —\n//? keeping the node-bearing module out of the client's static graph.\n//?\n//? The server's `tryCatch.ts` is intentionally left untouched (byte-for-byte) so\n//? the server capture path keeps its synchronous, statically-linked behaviour.\n//? On the client, capture is best-effort and async-via-microtask is fine.\n\nexport default async function tryCatch<T, P = void>(\n func: (values: P) => Promise<T> | T,\n params?: P,\n context?: Record<string, unknown>\n): Promise<[Error | null, T | null]> {\n try {\n const response = await func(params as P);\n return [null, response];\n } catch (error) {\n //? Lazy capture: the import is only evaluated on the error branch, so the\n //? `node:async_hooks`-bearing module never enters the client's static graph.\n const { captureException } = await import('./sentrySetup');\n captureException(error, context);\n return [error instanceof Error ? error : new Error(String(error)), null];\n }\n}\n","import { httpFetch } from './csrf';\nimport tryCatch from './tryCatchClient';\nimport tryCatchSync from './tryCatchSync';\nimport type { HttpMethodLiteral } from './apiMethodMapRegistry';\n\nexport type RoutedInvocationMethod = HttpMethodLiteral;\n\nexport interface RoutedInvocationResponse<TResponse> {\n kind: 'response';\n response: TResponse;\n httpStatus: number;\n}\n\nexport interface RoutedInvocationFailure {\n kind: 'aborted' | 'timeout' | 'network-error' | 'invalid-response';\n}\n\nexport type RoutedInvocationResult<TResponse> =\n | RoutedInvocationResponse<TResponse>\n | RoutedInvocationFailure;\n\nexport interface RoutedHttpInvocationInput<TStream> {\n path: string;\n method: RoutedInvocationMethod;\n data: Record<string, unknown>;\n stream?: (payload: TStream) => void;\n signals?: readonly (AbortSignal | null | undefined)[];\n timeoutMs?: number | false;\n}\n\ninterface ParsedSseEvent {\n event: string;\n data: unknown;\n}\n\nconst parseJson = (value: string): unknown => {\n const [error, parsed] = tryCatchSync(() => JSON.parse(value) as unknown);\n return error ? undefined : parsed;\n};\n\nconst parseSseFrame = (frame: string): ParsedSseEvent | null => {\n let event = 'message';\n const dataLines: string[] = [];\n for (const rawLine of frame.split('\\n')) {\n const line = rawLine.endsWith('\\r') ? rawLine.slice(0, -1) : rawLine;\n if (line.length === 0 || line.startsWith(':')) continue;\n if (line.startsWith('event:')) {\n event = line.slice(6).trimStart();\n continue;\n }\n if (line.startsWith('data:')) {\n dataLines.push(line.slice(5).trimStart());\n }\n }\n if (dataLines.length === 0) return null;\n return { event, data: parseJson(dataLines.join('\\n')) };\n};\n\nconst parseSseResponse = async <TResponse>(\n response: Response,\n onStream: ((payload: never) => void) | undefined,\n): Promise<TResponse | null> => {\n const reader = response.body?.getReader();\n if (!reader) return null;\n\n const decoder = new TextDecoder();\n let buffer = '';\n let finalResponse: TResponse | null = null;\n\n const consumeFrames = (flush: boolean): void => {\n buffer = buffer.replaceAll('\\r\\n', '\\n');\n let boundary = buffer.indexOf('\\n\\n');\n while (boundary !== -1) {\n const frame = buffer.slice(0, boundary);\n buffer = buffer.slice(boundary + 2);\n const parsed = parseSseFrame(frame);\n if (parsed?.event === 'stream' && parsed.data !== undefined) {\n onStream?.(parsed.data as never);\n } else if ((parsed?.event === 'final' || parsed?.event === 'error') && parsed.data !== undefined) {\n finalResponse = parsed.data as TResponse;\n }\n boundary = buffer.indexOf('\\n\\n');\n }\n\n if (flush && buffer.trim().length > 0) {\n const parsed = parseSseFrame(buffer);\n if (parsed?.event === 'stream' && parsed.data !== undefined) {\n onStream?.(parsed.data as never);\n } else if ((parsed?.event === 'final' || parsed?.event === 'error') && parsed.data !== undefined) {\n finalResponse = parsed.data as TResponse;\n }\n buffer = '';\n }\n };\n\n let readDone = false;\n while (!readDone) {\n const { done, value } = await reader.read();\n readDone = done;\n if (!done) {\n buffer += decoder.decode(value, { stream: true });\n consumeFrames(false);\n }\n }\n buffer += decoder.decode();\n consumeFrames(true);\n return finalResponse;\n};\n\nconst buildRequest = ({\n path,\n method,\n data,\n stream,\n signal,\n}: {\n path: string;\n method: RoutedInvocationMethod;\n data: Record<string, unknown>;\n stream: boolean;\n signal: AbortSignal;\n}): { url: string; init: RequestInit } => {\n const headers = new Headers();\n if (stream) headers.set('Accept', 'text/event-stream');\n\n if (method === 'GET') {\n const params = new URLSearchParams();\n params.set('__luckystack_data', JSON.stringify(data));\n if (stream) params.set('stream', 'true');\n const separator = path.includes('?') ? '&' : '?';\n return {\n url: `${path}${separator}${params.toString()}`,\n init: { method, headers, signal },\n };\n }\n\n headers.set('Content-Type', 'application/json');\n return {\n url: stream ? `${path}${path.includes('?') ? '&' : '?'}stream=true` : path,\n init: {\n method,\n headers,\n signal,\n body: JSON.stringify(data),\n },\n };\n};\n\nexport const invokeRoutedHttp = async <TResponse, TStream = Record<string, unknown>>({\n path,\n method,\n data,\n stream,\n signals = [],\n timeoutMs,\n}: RoutedHttpInvocationInput<TStream>): Promise<RoutedInvocationResult<TResponse>> => {\n if (signals.some((signal) => signal?.aborted)) return { kind: 'aborted' };\n\n const controller = new AbortController();\n const timeoutReason = Symbol('routed-invocation-timeout');\n const didTimeout = (): boolean => controller.signal.reason === timeoutReason;\n const abortFromCaller = (): void => {\n controller.abort();\n };\n for (const signal of signals) signal?.addEventListener('abort', abortFromCaller, { once: true });\n\n const timeout = typeof timeoutMs === 'number' && timeoutMs > 0\n ? setTimeout(() => {\n controller.abort(timeoutReason);\n }, timeoutMs)\n : null;\n\n const cleanup = (): void => {\n if (timeout) clearTimeout(timeout);\n for (const signal of signals) signal?.removeEventListener('abort', abortFromCaller);\n };\n\n const request = buildRequest({\n path,\n method,\n data,\n stream: typeof stream === 'function',\n signal: controller.signal,\n });\n const [fetchError, response] = await tryCatch(() => httpFetch(request.url, request.init));\n if (fetchError || !response) {\n cleanup();\n if (didTimeout()) return { kind: 'timeout' };\n if (controller.signal.aborted) return { kind: 'aborted' };\n return { kind: 'network-error' };\n }\n\n const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';\n const [parseError, parsed] = await tryCatch(async () => {\n if (contentType.includes('text/event-stream')) {\n return parseSseResponse<TResponse>(response, stream);\n }\n return (await response.json()) as TResponse;\n });\n cleanup();\n\n if (parseError || parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n if (didTimeout()) return { kind: 'timeout' };\n if (controller.signal.aborted) return { kind: 'aborted' };\n return { kind: 'invalid-response' };\n }\n\n return { kind: 'response', response: parsed, httpStatus: response.status };\n};\n","import { getProjectConfig } from \"./projectConfig\";\nimport { getLogger } from \"./loggerRegistry\";\nimport { getRegisteredApiMethod } from \"./apiMethodMapRegistry\";\nimport { inferHttpMethod, isGetMethodName } from \"./httpApiUtils\";\nimport { incrementResponseIndex, socket, waitForSocket } from \"./socketState\";\nimport type { ApiTypeMap, StreamPayload } from './apiTypeStubs';\nimport { notify } from \"./notifier\";\nimport { enqueueApiRequest, isOnline, removeApiQueueItem } from \"./offlineQueue\";\nimport { Socket } from \"socket.io-client\";\nimport { normalizeErrorResponseCore } from \"./responseNormalizer\";\nimport { parseServiceRouteName } from \"./serviceRoute\";\nimport tryCatchSync from \"./tryCatchSync\";\nimport {\n buildApiResponseEventName,\n buildApiStreamEventName,\n socketEventNames,\n} from \"./socketEvents\";\nimport {\n dispatchApiRequestInterceptors,\n dispatchApiResponseInterceptors,\n} from \"./apiInterceptors\";\nimport { invokeRoutedHttp } from './routedHttpInvocation';\n\n//? Abort controller logic:\n//? - abortable: true → always use abort controller\n//? - abortable: false → never use abort controller\n//? - abortable: undefined → use abort controller for GET APIs (from generated types)\n//? Keyed by `fullName` + a stable hash of the request `data` so the\n//? replace-previous (dedupe) semantics only fire for an IDENTICAL in-flight\n//? call. Two components polling the same GET route with DIFFERENT params no\n//? longer abort each other (each gets its own bucket); a genuine duplicate\n//? still supersedes the prior one.\n//? Size-capped at 1000 entries. All normal code paths (resolve, abort, disconnect,\n//? timeout) call `cleanupAbortController()` which deletes the entry. The cap\n//? is a safety net for edge cases where cleanup is skipped (socket torn down\n//? without a `disconnect` event, or a future code path that forgets to clean up).\nconst ABORT_CONTROLLERS_MAX = 1000;\nconst abortControllers = new Map<string, AbortController>();\n\n//? Stable, order-insensitive JSON stringify so `{a:1,b:2}` and `{b:2,a:1}`\n//? hash to the same abort-controller key. Falls back to a non-deduping unique\n//? token when the payload can't be serialised (circular ref), which is the\n//? safe choice — an unserialisable payload should never silently abort a\n//? sibling request.\nconst stableStringify = (value: unknown): string => {\n const seen = new WeakSet<object>();\n const walk = (input: unknown): unknown => {\n if (input === null || typeof input !== 'object') return input;\n if (seen.has(input)) return '[circular]';\n seen.add(input);\n if (Array.isArray(input)) return input.map((entry) => walk(entry));\n const record = input as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(record).toSorted()) {\n out[key] = walk(record[key]);\n }\n return out;\n };\n const [error, json] = tryCatchSync(() => JSON.stringify(walk(value)));\n if (error || typeof json !== 'string') {\n return `__unserializable__:${String(Date.now())}-${String(Math.random())}`;\n }\n return json;\n};\n\nconst buildAbortKey = (fullName: string, data: unknown): string =>\n `${fullName}::${stableStringify(data)}`;\n\nexport type ApiStreamEvent<T extends StreamPayload = StreamPayload> = T;\n\n/**\n * Check if an API is a GET method.\n *\n * Primary path: consult the registered `apiMethodMap` (generated from the\n * actual handler's `httpMethod` export or name-based inference at codegen\n * time). Falls back to name-prefix heuristic only when the map isn't\n * registered yet — typically because the consumer hasn't called\n * `registerApiMethodMap(...)` from their `socketInitializer.ts` boot file.\n */\nconst isGetMethodByPrefix = (apiName: string): boolean => isGetMethodName(apiName.toLowerCase());\n\ntype RoutedApiFailureKind = 'timeout' | 'aborted' | 'network-error' | 'invalid-response';\n\nconst routedApiErrorCode = (kind: RoutedApiFailureKind): string => {\n if (kind === 'timeout') return 'api.timeout';\n if (kind === 'aborted') return 'request.aborted';\n if (kind === 'invalid-response') return 'api.invalidServerResponse';\n return 'api.ioUnavailable';\n};\n\nconst resolveHttpMethod = (apiName: string, version: string) => {\n const lastSlash = apiName.lastIndexOf('/');\n if (lastSlash > 0) {\n const pagePath = apiName.slice(0, lastSlash);\n const leaf = apiName.slice(lastSlash + 1);\n const method = getRegisteredApiMethod(pagePath, leaf, version);\n if (method) return method;\n }\n return inferHttpMethod(apiName);\n};\n\nconst resolveGetMethod = (apiName: string, version: string): { isGet: boolean; usingHeuristic: boolean } => {\n //? Resolved-name shape: `pagePath/apiName`. apiMethodMap is nested by\n //? pagePath → apiName → version. Split at the last `/` so multi-segment\n //? page paths (e.g. 'admin/users') still resolve.\n const lastSlash = apiName.lastIndexOf('/');\n if (lastSlash > 0) {\n const pagePath = apiName.slice(0, lastSlash);\n const leaf = apiName.slice(lastSlash + 1);\n const method = getRegisteredApiMethod(pagePath, leaf, version);\n if (method) return { isGet: method === 'GET', usingHeuristic: false };\n }\n return { isGet: isGetMethodByPrefix(apiName), usingHeuristic: true };\n};\n\nconst canSendNow = (socketInstance: Socket) => {\n if (!socketInstance.connected) return false;\n return isOnline();\n};\n\nconst createQueueId = () => {\n return `${String(Date.now())}-${String(Math.random())}`;\n};\n\n//? Resolve logging flags at call time so registration order doesn't matter.\n//? `getLogging()` is called inside each handler instead of captured at load.\nconst getLogging = () => getProjectConfig().logging;\nconst shouldLogDev = () => getLogging().devLogs;\nconst shouldNotifyDev = () => getLogging().devNotifications;\nconst shouldLogStream = () => getLogging().stream;\n\nconst shouldUseAbortController = ({\n abortable,\n isGet,\n usingHeuristic,\n}: {\n abortable: boolean | undefined;\n isGet: boolean;\n usingHeuristic: boolean;\n}) => {\n if (abortable === true) return true;\n if (abortable === false) return false;\n //? CORE-O15: when `abortable` is unspecified and we are relying on the\n //? name-prefix heuristic (apiMethodMap not registered), default to\n //? non-abortable. A mis-named POST (e.g. `getReport`) would otherwise\n //? silently abort an identical in-flight call it has nothing to do with.\n //? Once `registerApiMethodMap` is called (normal boot), the generated map\n //? resolves the method and the heuristic path is never taken.\n if (usingHeuristic) return false;\n return isGet;\n};\n\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Type Helpers\n// ═══════════════════════════════════════════════════════════════════════════════\n\n// Check if data input is required (i.e., T does NOT allow empty object)\n// Unions like {a:1} | {b:1} do NOT allow {}, so data will be required\ntype DataRequired<T> = Record<string, never> extends T ? false : true;\n\ntype UnionToIntersection<U> =\n (U extends unknown ? (arg: U) => void : never) extends ((arg: infer I) => void)\n ? I\n : never;\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Global API Params - Union of ALL valid API calls with proper data enforcement\n// ═══════════════════════════════════════════════════════════════════════════════\ntype ApiRouteRecord = UnionToIntersection<{\n [P in keyof ApiTypeMap]: {\n [N in keyof ApiTypeMap[P] as P extends 'root'\n ? `system/${Extract<N, string>}`\n : `${Extract<P, string>}/${Extract<N, string>}`]: ApiTypeMap[P][N]\n }\n}[keyof ApiTypeMap]>;\n\ntype ApiFullName = Extract<keyof ApiRouteRecord, string>;\ntype VersionsForFullName<F extends ApiFullName> = keyof ApiRouteRecord[F] & string;\n\n// Force expansion of types to clear aliases in tooltips\ntype Prettify<T> = { [K in keyof T]: T[K] } & {};\n\n// Get input type for an API name (union if exists on multiple pages)\ntype InputForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = ApiRouteRecord[F][V] extends { input: infer I }\n ? I\n : never;\n\n// Get output type for an API name (union if exists on multiple pages)\ntype OutputForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = ApiRouteRecord[F][V] extends { output: infer O }\n ? O\n : never;\n\ntype StreamForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = ApiRouteRecord[F][V] extends { stream: infer S }\n ? S\n : never;\n\ntype ApiStreamCallbackForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> =\n [StreamForFullName<F, V>] extends [never]\n ? never\n : (event: ApiStreamEvent<Prettify<StreamForFullName<F, V> extends StreamPayload ? StreamForFullName<F, V> : StreamPayload>>) => void;\n\n// Build params type for a specific API name\ntype ApiParamsForFullName<\n F extends ApiFullName,\n V extends VersionsForFullName<F>\n> = DataRequired<InputForFullName<F, V>> extends true\n ? { name: F; version: V; data: Prettify<InputForFullName<F, V>>; abortable?: boolean; disableErrorMessage?: boolean; onStream?: ApiStreamCallbackForFullName<F, V>; signal?: AbortSignal; timeoutMs?: number | false; }\n : { name: F; version: V; data?: Prettify<InputForFullName<F, V>>; abortable?: boolean; disableErrorMessage?: boolean; onStream?: ApiStreamCallbackForFullName<F, V>; signal?: AbortSignal; timeoutMs?: number | false; };\n\ninterface RuntimeApiParams {\n name?: string;\n version?: string;\n data?: unknown;\n abortable?: boolean;\n disableErrorMessage?: boolean;\n onStream?: (event: ApiStreamEvent) => void;\n signal?: AbortSignal;\n /** Per-call response timeout (ms). Overrides `api.requestTimeoutMs`. `false` disables. */\n timeoutMs?: number | false;\n}\n\nexport interface ApiErrorResponse extends Record<string, unknown> {\n status: 'error';\n httpStatus: number;\n message: string;\n errorCode: string;\n errorParams?: { key: string; value: string | number | boolean }[];\n}\n\ninterface ApiSuccessResponse extends Record<string, unknown> {\n status: 'success';\n httpStatus: number;\n}\n\ntype ApiResponse = ApiErrorResponse | ApiSuccessResponse;\n\nconst normalizeApiError = ({\n response,\n fallbackErrorCode,\n fallbackHttpStatus = 500,\n}: {\n response: { status: 'error'; errorCode?: string; errorParams?: { key: string; value: string | number | boolean }[]; httpStatus?: number; message?: string };\n fallbackErrorCode: string;\n fallbackHttpStatus?: number;\n}): ApiErrorResponse => {\n const normalized = normalizeErrorResponseCore({\n response,\n fallbackErrorCode,\n fallbackHttpStatus,\n resolveMessage: ({ errorCode }) => {\n if (typeof response.message === 'string' && response.message.trim().length > 0) {\n return response.message;\n }\n return errorCode;\n },\n });\n\n return {\n status: 'error',\n message: normalized.message,\n errorCode: normalized.errorCode,\n errorParams: normalized.errorParams,\n httpStatus: normalized.httpStatus ?? fallbackHttpStatus,\n };\n};\n\n/**\n * Type-safe API request function.\n * \n * @example\n * ```typescript\n * // Full name usage - includes page in the name\n * const result = await apiRequest({ name: 'examples/publicApi', version: 'v1', data: { message: 'hello' } });\n * // result is typed correctly for publicApi\n * \n * // Global APIs use service-first naming\n * await apiRequest({ name: 'system/session', version: 'v1' });\n * ```\n */\n\nexport function apiRequest<F extends ApiFullName, V extends VersionsForFullName<F>>(\n params: ApiParamsForFullName<F, V>\n): Promise<Prettify<OutputForFullName<F, V>> | ApiErrorResponse> {\n type RequestOutput = Prettify<OutputForFullName<F, V> & ApiResponse>;\n const runtimeParams = params as RuntimeApiParams;\n const { name, version, disableErrorMessage = false, onStream, signal: externalSignal, timeoutMs } = runtimeParams;\n const payloadData = runtimeParams.data;\n\n return new Promise<RequestOutput>((resolve) => {\n void (async () => {\n //? B1 — if the consumer-supplied signal is already aborted at call\n //? time, short-circuit before we even touch the socket. Mirrors the\n //? `fetch(url, { signal })` contract.\n if (externalSignal?.aborted) {\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }) as RequestOutput);\n return;\n }\n if (!name || typeof name !== \"string\") {\n if (shouldLogDev()) {\n getLogger().error(\"apiRequest: Invalid name\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'api.invalidName' });\n }\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'api.invalidName' },\n fallbackErrorCode: 'api.invalidName',\n }) as RequestOutput);\n return;\n }\n\n if (!version || typeof version !== 'string') {\n if (shouldLogDev()) {\n getLogger().error(\"apiRequest: Invalid version\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'api.invalidVersion' });\n }\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'api.invalidVersion' },\n fallbackErrorCode: 'api.invalidVersion',\n }) as RequestOutput);\n return;\n }\n\n const parsedRoute = parseServiceRouteName(name);\n if (parsedRoute.status === 'error') {\n if (shouldLogDev()) {\n getLogger().error(`[apiRequest] Invalid service route name`, undefined, { name, reason: parsedRoute.reason });\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'routing.invalidServiceRouteName' });\n }\n resolve(normalizeErrorResponseCore({\n response: {\n status: 'error',\n errorCode: 'routing.invalidServiceRouteName',\n errorParams: [{ key: 'name', value: name }],\n },\n fallbackErrorCode: 'routing.invalidServiceRouteName',\n }) as RequestOutput);\n return;\n }\n\n const sanitizedName = parsedRoute.normalizedRouteName;\n\n const data = payloadData && typeof payloadData === \"object\" ? payloadData : {};\n\n if (!await waitForSocket()) {\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'api.ioUnavailable' },\n fallbackErrorCode: 'api.ioUnavailable',\n }) as RequestOutput);\n return;\n }\n if (!socket) {\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'api.ioUnavailable' },\n fallbackErrorCode: 'api.ioUnavailable',\n }) as RequestOutput);\n return;\n }\n\n //? Abort controller logic:\n //? - abortable: true → always use abort controller\n //? - abortable: false → never use abort controller\n //? - abortable: undefined → smart default (GET-like APIs get abort controller)\n //? Pass the full pagePath/apiName so the registry lookup can resolve via\n //? the generated apiMethodMap (falls back to the leaf-name prefix heuristic\n //? when the map isn't registered yet).\n const { isGet, usingHeuristic } = resolveGetMethod(sanitizedName, version);\n const useAbortController = shouldUseAbortController({\n abortable: runtimeParams.abortable,\n isGet,\n usingHeuristic,\n });\n const fullName = `api/${sanitizedName}/${version}`;\n //? Per-payload abort key: only an identical in-flight call to the same\n //? route supersedes the previous one (see `buildAbortKey` rationale).\n const abortKey = buildAbortKey(fullName, data);\n\n let signal: AbortSignal | null = null;\n let abortHandler: (() => void) | null = null;\n let queueId: string | null = null;\n let cleanupStreamListener: (() => void) | null = null;\n //? Response listener cleanup, registered once the `.once` handler is\n //? attached in `runRequest`. Lets the abort path proactively `off()` the\n //? lingering response listener instead of waiting for a response that was\n //? cancelled and will never arrive.\n let cleanupResponseListener: (() => void) | null = null;\n //? Response-timeout timer. Armed right after the request is emitted so a\n //? lost response (server restart/crash between emit and reply) settles the\n //? promise instead of hanging the awaiting caller forever.\n let responseTimeout: ReturnType<typeof setTimeout> | null = null;\n const clearResponseTimeout = () => {\n if (responseTimeout) {\n clearTimeout(responseTimeout);\n responseTimeout = null;\n }\n };\n\n const cleanupAbortController = () => {\n if (signal && abortHandler) {\n signal.removeEventListener(\"abort\", abortHandler);\n }\n abortControllers.delete(abortKey);\n };\n\n if (useAbortController) {\n if (abortControllers.has(abortKey)) {\n const prevAbortController = abortControllers.get(abortKey);\n prevAbortController?.abort();\n }\n //? Evict the oldest entry when the cap is reached (LRU-insert-order).\n //? Map preserves insertion order; the first key is the oldest in-flight entry.\n if (!abortControllers.has(abortKey) && abortControllers.size >= ABORT_CONTROLLERS_MAX) {\n const oldest = abortControllers.keys().next().value;\n if (oldest !== undefined) abortControllers.delete(oldest);\n }\n const abortController = new AbortController();\n abortControllers.set(abortKey, abortController);\n signal = abortController.signal;\n\n abortHandler = () => {\n clearResponseTimeout();\n cleanupStreamListener?.();\n cleanupStreamListener = null;\n cleanupResponseListener?.();\n cleanupResponseListener = null;\n cleanupAbortController();\n if (queueId) {\n removeApiQueueItem(queueId);\n }\n //? Resolve (not reject) with the same `request.aborted` envelope the\n //? external-signal abort path uses. Rejecting here produced an\n //? unhandled rejection for fire-and-forget GET calls that get\n //? superseded by the default replace-previous behaviour.\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }) as RequestOutput);\n };\n\n signal.addEventListener(\"abort\", abortHandler);\n }\n\n const runRequest = (socketInstance: Socket) => {\n if (!canSendNow(socketInstance)) {\n queueId ??= createQueueId();\n //? For the immediate reject-new path (`drop-newest` / `reject`\n //? policy) THIS item's `onDrop` fires SYNCHRONOUSLY inside\n //? `enqueueApiRequest`. We keep surfacing that as the historical\n //? `offline.queueFull` via the `!enqueued` branch, so suppress the\n //? `onDrop` resolve during the synchronous enqueue window and re-arm\n //? it only if the item was actually queued (so a LATER async eviction\n //? — drop-oldest by a future enqueue, or age expiry — still settles).\n let suppressOnDrop = true;\n //? `enqueueApiRequest` returns `false` when the offline queue is\n //? full and `dropPolicy: 'reject'` is active. Without honoring\n //? that signal the outer promise never settles and the caller\n //? awaits forever. Resolve with a normalized error envelope so\n //? callers can branch.\n const enqueued = enqueueApiRequest({\n id: queueId,\n key: fullName,\n run: (nextSocket) => {\n runRequest(nextSocket);\n },\n createdAt: Date.now(),\n //? CORE: mirror SYNC-09. Settle the awaiting caller when this\n //? QUEUED request is later EVICTED (drop-oldest by a future\n //? enqueue / age expiry) instead of ever running. Without it the\n //? promise hangs forever, since `run` is the only thing that could\n //? otherwise resolve it — `apiRequest` had drifted from the sibling\n //? `syncRequest` which already had this fix. Also tear down the\n //? abort-controller registry entry so it isn't leaked.\n onDrop: (reason) => {\n if (suppressOnDrop) return;\n cleanupAbortController();\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'offline.dropped', errorParams: [{ key: 'reason', value: reason }] },\n fallbackErrorCode: 'offline.dropped',\n }) as RequestOutput);\n },\n });\n //? Past the synchronous enqueue window: if the item is sitting in the\n //? queue, a future eviction should settle via `onDrop`.\n suppressOnDrop = false;\n if (!enqueued) {\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'offline.queueFull', httpStatus: 503 },\n fallbackErrorCode: 'offline.queueFull',\n fallbackHttpStatus: 503,\n }) as RequestOutput);\n } else if (externalSignal && queueId) {\n //? CORE-O14: if an external AbortSignal fires while the item is\n //? sitting in the offline queue, its listener inside runRequest has\n //? not been registered yet. Register a one-shot handler here so the\n //? abort is not silently ignored and the queue slot is reclaimed.\n const capturedQueueId = queueId;\n const queueAbortHandler = () => {\n removeApiQueueItem(capturedQueueId);\n cleanupAbortController();\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }) as RequestOutput);\n };\n externalSignal.addEventListener('abort', queueAbortHandler, { once: true });\n }\n return;\n }\n\n if (signal?.aborted) {\n return;\n }\n\n if (getProjectConfig().transport.invocation === 'routed-http') {\n void (async () => {\n const effectiveTimeoutMs = timeoutMs ?? getProjectConfig().api.requestTimeoutMs;\n const routed = await invokeRoutedHttp<ApiResponse, ApiStreamEvent>({\n path: `/api/${sanitizedName}/${version}`,\n method: resolveHttpMethod(sanitizedName, version),\n data: data as Record<string, unknown>,\n stream: onStream,\n signals: [signal, externalSignal],\n timeoutMs: effectiveTimeoutMs,\n });\n cleanupAbortController();\n if (queueId) removeApiQueueItem(queueId);\n\n if (routed.kind !== 'response') {\n const errorCode = routedApiErrorCode(routed.kind);\n resolve(normalizeApiError({\n response: {\n status: 'error',\n errorCode,\n httpStatus: routed.kind === 'timeout' ? 504 : undefined,\n },\n fallbackErrorCode: errorCode,\n fallbackHttpStatus: routed.kind === 'timeout' ? 504 : 500,\n }) as RequestOutput);\n return;\n }\n\n const response = routed.response;\n dispatchApiResponseInterceptors({ name: sanitizedName, version, response });\n if (response.status === 'error') {\n const normalizedError = normalizeErrorResponseCore({ response });\n if (!disableErrorMessage) {\n notify.error({\n key: normalizedError.errorCode || normalizedError.message,\n params: normalizedError.errorParams,\n });\n }\n Object.assign(response, normalizedError);\n }\n resolve(response as RequestOutput);\n })();\n return;\n }\n\n const tempIndex = incrementResponseIndex();\n socketInstance.emit(socketEventNames.apiRequest, { name: fullName, data, responseIndex: tempIndex });\n\n //? B1 — bridge the consumer's AbortSignal to the server. When the\n //? signal fires we emit `apiCancel { responseIndex }` so the server\n //? handler stops emitting new chunks. We also resolve locally with\n //? `request.aborted` so the awaiting caller settles (vs awaiting\n //? a response that will never arrive because we cancelled it).\n let cleanupExternalAbort: (() => void) | null = null;\n if (externalSignal) {\n const externalAbortHandler = () => {\n clearResponseTimeout();\n socketInstance.emit(socketEventNames.apiCancel, { responseIndex: tempIndex });\n cleanupStreamListener?.();\n cleanupStreamListener = null;\n cleanupResponseListener?.();\n cleanupResponseListener = null;\n cleanupAbortController();\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }) as RequestOutput);\n };\n externalSignal.addEventListener('abort', externalAbortHandler);\n cleanupExternalAbort = () => {\n externalSignal.removeEventListener('abort', externalAbortHandler);\n };\n }\n\n //? Arm the response timeout. A per-call `timeoutMs` overrides the\n //? `api.requestTimeoutMs` config default; either may be `false` to\n //? disable. On expiry we tear down every listener/controller and settle\n //? with a 504 envelope so the awaiting caller never hangs.\n clearResponseTimeout();\n const effectiveTimeoutMs = timeoutMs ?? getProjectConfig().api.requestTimeoutMs;\n if (typeof effectiveTimeoutMs === 'number' && effectiveTimeoutMs > 0) {\n responseTimeout = setTimeout(() => {\n responseTimeout = null;\n cleanupStreamListener?.();\n cleanupStreamListener = null;\n cleanupResponseListener?.();\n cleanupResponseListener = null;\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n cleanupAbortController();\n if (queueId) {\n removeApiQueueItem(queueId);\n }\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'api.timeout', httpStatus: 504 },\n fallbackErrorCode: 'api.timeout',\n fallbackHttpStatus: 504,\n }) as RequestOutput);\n }, effectiveTimeoutMs);\n }\n\n if (typeof onStream === 'function') {\n const streamEventName = buildApiStreamEventName(tempIndex);\n const streamListener = (streamPayload: ApiStreamEvent) => {\n if (signal?.aborted) {\n return;\n }\n\n if (shouldLogStream()) {\n getLogger().debug(`Server API Stream(${String(tempIndex)})`, { APINAME: sanitizedName, streamPayload });\n }\n\n onStream(streamPayload);\n };\n\n socketInstance.on(streamEventName, streamListener);\n cleanupStreamListener = () => {\n socketInstance.off(streamEventName, streamListener);\n };\n }\n\n if (shouldLogDev()) {\n getLogger().debug(`Client API Request(${String(tempIndex)})`, { APINAME: sanitizedName, data });\n }\n\n //? Inside the handler we type as the runtime envelope (ApiResponse).\n //? Project-specific output narrowing only matters at the call site,\n //? where `RequestOutput = OutputForFullName<F,V> & ApiResponse` is\n //? returned via `resolve(... as RequestOutput)`. When core is type-\n //? checked in isolation (e.g. per-package tsup dts build), ApiTypeMap\n //? is empty so OutputForFullName collapses to never; using ApiResponse\n //? here keeps the body type-safe in both cases.\n const responseEventName = buildApiResponseEventName(tempIndex);\n const responseListener = (response: ApiResponse) => {\n if (signal?.aborted) {\n clearResponseTimeout();\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n return;\n }\n\n clearResponseTimeout();\n cleanupStreamListener?.();\n cleanupStreamListener = null;\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n //? Call cleanup (not just null) so that any watcher layered on top of\n //? cleanupResponseListener (e.g. the CORE-O13 disconnect watcher) is\n //? properly torn down when a normal response arrives.\n cleanupResponseListener?.();\n cleanupResponseListener = null;\n\n const status = response.status;\n\n //? EXT-03 — fire client response interceptors when the envelope\n //? arrives (observation-only: metrics, breadcrumb, custom logging).\n dispatchApiResponseInterceptors({ name: sanitizedName, version, response });\n\n if (shouldLogDev()) {\n getLogger().debug(`Server API Response(${String(tempIndex)})`, { ...response, APINAME: sanitizedName });\n }\n\n if (status === \"error\") {\n const normalizedError = normalizeErrorResponseCore({ response });\n\n if (!disableErrorMessage) {\n if (normalizedError.errorCode) {\n notify.error({ key: normalizedError.errorCode, params: normalizedError.errorParams });\n } else {\n notify.error({ key: normalizedError.message });\n }\n }\n\n Object.assign(response, normalizedError);\n cleanupAbortController();\n resolve(response as RequestOutput);\n return;\n }\n\n cleanupAbortController();\n\n resolve(response as RequestOutput);\n };\n\n socketInstance.once(responseEventName, responseListener);\n cleanupResponseListener = () => {\n socketInstance.off(responseEventName, responseListener);\n };\n\n //? CORE-O13: when timeout is disabled AND there is no abort controller\n //? a server restart between emit and reply leaves this promise pending\n //? forever, leaking the response listener. The disconnect event is the\n //? authoritative signal that no reply will ever arrive on this socket.\n const disconnectHandler = () => {\n if (cleanupResponseListener) {\n cleanupResponseListener();\n cleanupResponseListener = null;\n }\n cleanupStreamListener?.();\n cleanupStreamListener = null;\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n clearResponseTimeout();\n cleanupAbortController();\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'api.disconnected' },\n fallbackErrorCode: 'api.disconnected',\n }) as RequestOutput);\n };\n socketInstance.once('disconnect', disconnectHandler);\n //? Wrap the response listener to also remove the disconnect watcher on\n //? a normal reply (avoids a dangling listener on every successful call).\n const originalCleanupResponseListener = cleanupResponseListener;\n cleanupResponseListener = () => {\n socketInstance.off('disconnect', disconnectHandler);\n originalCleanupResponseListener();\n };\n };\n\n //? EXT-03 — fire client request interceptors just before the emit. They\n //? may mutate `data` in place (correlation id / feature-flag context).\n //? `data` is the same reference passed to `socketInstance.emit`, so the\n //? mutation is on the wire. Awaited so an async interceptor (flag lookup,\n //? trace-id mint) completes first; failures are caught + logged inside.\n await dispatchApiRequestInterceptors({\n name: sanitizedName,\n version,\n data: data as Record<string, unknown>,\n });\n\n //? CORE-O12: re-check the external abort signal after the interceptor\n //? await. A slow async interceptor adds uncounted latency; if the caller\n //? aborted during that window the signal is already fired but nothing has\n //? yet attached its listener (that happens inside runRequest). Settling\n //? here avoids emitting a request whose response will never be consumed.\n if (externalSignal?.aborted) {\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }) as RequestOutput);\n return;\n }\n\n runRequest(socket);\n })();\n });\n}","//? Locales registry — lets the framework's TranslationProvider + notify\n//? read the consumer's translation JSON files without baking them into the\n//? package. Consumer overlay: `luckystack/i18n/locales.ts` calls\n//? `registerLocales({ en: enJson, nl: nlJson, ... })`.\n//?\n//? Language source: a separate registry — `registerLanguageSource(fn)` —\n//? lets the consumer wire up where the active language comes from\n//? (typically the SessionProvider's current `session.language`). When no\n//? source is registered, the framework falls back to\n//? `getProjectConfig().defaultLanguage`.\n\nimport { getProjectConfig } from './projectConfig';\n\nexport type LocalesMap = Record<string, unknown>;\nexport type LanguageSource = () => string | null | undefined;\n\nlet activeLocales: LocalesMap = {};\nlet activeLanguageSource: LanguageSource | null = null;\n\nexport const registerLocales = (locales: LocalesMap): void => {\n activeLocales = locales;\n};\n\nexport const getRegisteredLocales = (): LocalesMap => activeLocales;\n\nexport const getDefaultLocale = (): string => getProjectConfig().defaultLanguage || 'en';\n\nexport const registerLanguageSource = (fn: LanguageSource): void => {\n activeLanguageSource = fn;\n};\n\nexport const getActiveLanguage = (): string => {\n if (activeLanguageSource) {\n const lang = activeLanguageSource();\n if (typeof lang === 'string' && lang.length > 0) return lang;\n }\n return getDefaultLocale();\n};\n\n//? Lookup a language's translation tree by code. Falls back to default\n//? language when the code isn't registered. Used by TranslationProvider\n//? + notify to resolve the active translations.\nexport const getLocaleByCode = (code: string): unknown => {\n if (code in activeLocales) return activeLocales[code];\n const fallback = getDefaultLocale();\n if (fallback in activeLocales) return activeLocales[fallback];\n return {};\n};\n","//? Middleware handler registry. The framework's `Middleware` + `Router`\n//? components (in `./react/`) call the registered handler on every\n//? navigation change to decide whether the user is allowed on the target\n//? route. Per-page `export const middleware` (see `registerPageMiddleware`\n//? below) is the canonical path; a consumer wanting a cross-cutting GLOBAL\n//? guard registers it from the client bootstrap (typically `main.tsx`) via\n//? `registerMiddlewareHandler(...)`. No separate\n//? `src/_functions/middlewareHandler.ts` file is required.\n\nimport type { BaseSessionLayout } from './sessionTypes';\n\nexport interface MiddlewareInput {\n location: string;\n searchParams: Record<string, string>;\n session: BaseSessionLayout | null;\n}\n\nexport type MiddlewareResult =\n | { success: true; redirect?: undefined }\n | { success: false; redirect: string }\n | undefined;\n\nexport type MiddlewareHandler = (input: MiddlewareInput) => MiddlewareResult | Promise<MiddlewareResult>;\n\n//? Default handler: allow by default. Per-page `middleware` exports on each\n//? `page.tsx` are the canonical route-guard path (see `registerPageMiddleware`\n//? below); the global handler is only consulted as a fallback for pages\n//? that don't declare their own guard. Allowing-by-default keeps public\n//? routes (`/`, `/login`, `/register`, ...) working without forcing every\n//? consumer to ship a `src/_functions/middlewareHandler.ts` boilerplate\n//? file just to opt-in to \"let the user through\".\n//?\n//? Consumers who need a cross-cutting global guard (telemetry, server-\n//? reachability check, maintenance banner gate) call\n//? `registerMiddlewareHandler(...)` directly from `main.tsx` with their own\n//? function — no separate file required.\nconst DEFAULT_HANDLER: MiddlewareHandler = () => ({ success: true });\n\nlet activeHandler: MiddlewareHandler = DEFAULT_HANDLER;\n\nexport const registerMiddlewareHandler = (handler: MiddlewareHandler): void => {\n activeHandler = handler;\n};\n\nexport const getMiddlewareHandler = (): MiddlewareHandler => activeHandler;\n\n//? Per-page middleware registry. Counterpart to `registerMiddlewareHandler`\n//? for the common case where each page declares its own guard locally\n//? (`export const middleware = ({ session }) => {...}` from page.tsx).\n//? The framework's `<Middleware>` component checks this map FIRST and only\n//? falls back to the global handler when no per-page entry is registered.\n//?\n//? `PageMiddleware<TSession>` matches the existing `MiddlewareHandler`\n//? signature so any handler shape is reusable on either side. The generic\n//? is purely ergonomic for consumer-side type narrowing — module\n//? augmentation of `BaseSessionLayout` flows through.\n\nexport type PageMiddleware<TSession extends BaseSessionLayout = BaseSessionLayout> = (\n input: { location: string; searchParams: Record<string, string>; session: TSession | null },\n) => MiddlewareResult | Promise<MiddlewareResult>;\n\nconst pageMiddlewares = new Map<string, PageMiddleware>();\n\nexport const registerPageMiddleware = (path: string, middleware: PageMiddleware): void => {\n pageMiddlewares.set(path, middleware);\n};\n\nexport const getPageMiddleware = (path: string): PageMiddleware | undefined =>\n pageMiddlewares.get(path);\n\nexport const hasPageMiddleware = (path: string): boolean => pageMiddlewares.has(path);\n\n/** Test helper — drop every registered per-page middleware. */\nexport const clearPageMiddlewaresForTests = (): void => {\n pageMiddlewares.clear();\n};\n","//? Shared session context. Defined in core so framework-React components\n//? (Middleware, Router, useSession-aware UI) can consume it without\n//? pulling in app code. The consumer's `SessionProvider` (in their\n//? template's `_providers/`) does the actual fetch/socket/lifecycle work\n//? and writes to this context via `<SessionContext value={...}>`.\n//?\n//? The session shape inside the context is intentionally loose\n//? (`BaseSessionLayout | null`) so app code with extended session types\n//? can read it through a typed `useSession<MySession>()` cast.\n\nimport { createContext, use } from 'react';\nimport type { BaseSessionLayout } from '../sessionTypes';\nimport {\n dispatchClientHook,\n dispatchVetoableClientHook,\n type ClientHookStopSignal,\n} from '../clientHookBus';\n\nexport interface SessionContextValue<TSession extends BaseSessionLayout = BaseSessionLayout> {\n session: TSession | null;\n sessionLoaded: boolean;\n}\n\nconst DEFAULT_VALUE: SessionContextValue = {\n session: null,\n sessionLoaded: false,\n};\n\nexport const SessionContext = createContext<SessionContextValue>(DEFAULT_VALUE);\n\nexport function useSession<TSession extends BaseSessionLayout = BaseSessionLayout>(): SessionContextValue<TSession> {\n return use(SessionContext) as SessionContextValue<TSession>;\n}\n\n//? Non-React accessor for code that runs outside the component tree\n//? (e.g. notify() called from a non-component). Kept in sync with the\n//? context by the consumer's SessionProvider via `setLatestSession()`.\nlet latestSession: BaseSessionLayout | null = null;\n\nexport const setLatestSession = (session: BaseSessionLayout | null): void => {\n const previous = latestSession;\n latestSession = session;\n\n //? Fire client-side lifecycle hooks on null↔non-null transitions. Covers\n //? both fresh login AND existing-session restore on page reload — both\n //? cases need the same downstream actions (room auto-join, analytics\n //? identify, etc). Same-id updates (avatar refresh, profile change) do\n //? NOT fire postLogin; if a `sessionUpdate` hook is needed later, add it\n //? as a third branch without breaking existing handlers.\n if (previous === null && session !== null) {\n dispatchClientHook('postLogin', { session });\n } else if (previous !== null && session === null) {\n dispatchClientHook('postLogout', { previousSession: previous });\n }\n};\n\n//? Caller-supplied `TSession` is used to narrow the return type at the\n//? callsite (consumers extend `BaseSessionLayout`). The lint rule sees it as\n//? \"used once\" but the parameter exists solely to surface the consumer's\n//? widened session type through the read API.\n// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- callsite-narrowing use\nexport const getCurrentSession = <TSession extends BaseSessionLayout = BaseSessionLayout>(): TSession | null =>\n latestSession as TSession | null;\n\n/**\n * Vetoable entry point for committing a null → session transition (i.e. a\n * fresh login or a page-load session restore). Dispatches the `preLogin`\n * client hook to every registered handler in parallel; if any returns a\n * stop signal, the transition is aborted and `{ committed: false, signal }`\n * is returned. Otherwise, `setLatestSession(session)` runs synchronously\n * (which fires `postLogin`) and `{ committed: true }` comes back.\n *\n * Consumer's `SessionProvider` should `await` this for non-null session\n * commits and roll local React state back to `null` when a veto comes\n * through, so the UI doesn't render a half-logged-in state.\n */\nexport type ProposeLoginResult =\n | { stopped: false; committed: true }\n | { stopped: true; signal: ClientHookStopSignal; committed: false };\n\nexport const proposeLogin = async (\n session: BaseSessionLayout,\n): Promise<ProposeLoginResult> => {\n const veto = await dispatchVetoableClientHook('preLogin', { candidateSession: session });\n if (veto.stopped) return { stopped: true, signal: veto.signal, committed: false };\n setLatestSession(session);\n return { stopped: false, committed: true };\n};\n","/* eslint-disable react-refresh/only-export-components */\nimport { createContext, use, useState, useMemo, useCallback, ReactNode } from 'react';\n\nexport type AvatarStatus = 'avatar' | 'fallback';\n\ninterface AvatarContextType {\n avatarStatuses: Record<string, AvatarStatus>;\n setAvatarStatus: (key: string, status: AvatarStatus) => void;\n}\n\nconst AvatarContext = createContext<AvatarContextType | null>(null);\n\nexport const AvatarProvider = ({ children }: { children: ReactNode }) => {\n const [avatarStatuses, setAvatarStatuses] = useState<Record<string, AvatarStatus>>({});\n\n const setAvatarStatus = useCallback((key: string, status: AvatarStatus) => {\n setAvatarStatuses(prev => ({ ...prev, [key]: status }));\n }, []);\n\n const contextValue = useMemo(() => ({\n avatarStatuses,\n setAvatarStatus,\n }), [avatarStatuses, setAvatarStatus]);\n\n return (\n <AvatarContext value={contextValue}>\n {children}\n </AvatarContext>\n );\n};\n\nexport function useAvatarContext() {\n const ctx = use(AvatarContext);\n if (!ctx) throw new Error('useAvatarContext must be used within AvatarProvider');\n return ctx;\n}\n","//? `useTheme()` — framework-owned theme state hook. Default value comes\n//? from `getProjectConfig().defaultTheme` (the union literal exported by\n//? the consumer's `config.ts`). Toggling sets the `.dark` class on\n//? `<html>`. Persistence (e.g. saving to `User.theme` via\n//? `settings/updateUser`) is up to the consumer — the hook only owns\n//? local state + DOM class.\n\nimport { useState, useCallback } from 'react';\nimport { getProjectConfig } from '../projectConfig';\n\nexport type Theme = 'light' | 'dark';\n\nconst applyDomClass = (theme: Theme): void => {\n if (typeof document === 'undefined') return;\n document.documentElement.classList.toggle('dark', theme === 'dark');\n};\n\nexport const useTheme = () => {\n const initialTheme = (getProjectConfig().defaultTheme) ?? 'light';\n const [theme, setThemeState] = useState<Theme>(initialTheme);\n\n const setTheme = useCallback((next: Theme) => {\n setThemeState(next);\n applyDomClass(next);\n }, []);\n\n return { theme, setTheme };\n};\n","/* eslint-disable react-refresh/only-export-components */\n//? Framework-owned translation provider. Reads the registered locales map\n//? (consumer registers via `registerLocales({...})` in their overlay) and\n//? the active language source (consumer registers via\n//? `registerLanguageSource(fn)`, typically `() => session?.language`).\n//? Default language falls back to `getProjectConfig().defaultLanguage`.\n\nimport { createContext, ReactNode, use, useEffect, useMemo, useState, Dispatch, SetStateAction } from 'react';\nimport {\n getActiveLanguage,\n getDefaultLocale,\n getLocaleByCode,\n getRegisteredLocales,\n} from '../localesRegistry';\n\nexport type TranslationRecord = Record<string, unknown>;\n\ninterface TranslationContextValue {\n translations: TranslationRecord;\n setLanguage: Dispatch<SetStateAction<string>>;\n}\n\nconst TranslationContext = createContext<TranslationContextValue | null>(null);\n\nconst resolveTranslations = (code: string): TranslationRecord => {\n const value = getLocaleByCode(code);\n if (value && typeof value === 'object') return value as TranslationRecord;\n return {};\n};\n\nexport function TranslationProvider({ children }: { children: ReactNode }) {\n const [language, setLanguage] = useState<string>(() => getActiveLanguage());\n const translations = useMemo(() => resolveTranslations(language), [language]);\n\n //? Poll the registered language source so changes to session.language\n //? (or whatever the consumer wired) flip the active language without a\n //? full remount. Cheap: shallow string compare every ~250ms.\n useEffect(() => {\n const handle = window.setInterval(() => {\n const next = getActiveLanguage();\n if (next !== language) setLanguage(next);\n }, 250);\n return () => { window.clearInterval(handle); };\n }, [language]);\n\n const contextValue = useMemo(() => ({ translations, setLanguage }), [translations]);\n\n return (\n <TranslationContext value={contextValue}>\n {children}\n </TranslationContext>\n );\n}\n\nexport function useTranslation() {\n const context = use(TranslationContext);\n if (!context) throw new Error('useTranslation must be used within a TranslationProvider');\n return context.translations;\n}\n\nexport function useUpdateLanguage() {\n const context = use(TranslationContext);\n if (!context) throw new Error('useUpdateLanguage must be used within a TranslationProvider');\n return context.setLanguage;\n}\n\nexport interface TranslateParam { key: string; value: string | number | boolean }\n\n//? Resolve `a.b.c` style dot-notation keys in a translation tree. Returns\n//? the key itself if it can't be resolved (so untranslated strings stand\n//? out in the UI rather than vanishing). Supports `{{param}}` substitution.\nexport function translate({ translationList, key, params }: {\n translationList: TranslationRecord;\n key: unknown;\n params?: TranslateParam[];\n}): string {\n if (typeof key !== 'string' || key.length === 0) return '';\n\n const parts = key.split('.');\n let result: unknown = translationList;\n for (const part of parts) {\n if (result && typeof result === 'object' && part in result) {\n result = (result as Record<string, unknown>)[part];\n } else {\n return key;\n }\n }\n if (typeof result !== 'string') return key;\n if (!Array.isArray(params) || params.length === 0) return result;\n\n let finalResult = result;\n for (const param of params) {\n if (!param.key) continue;\n //? Escape key before building the RegExp — a key containing regex\n //? meta-characters (e.g. `.`, `+`, `(`) would otherwise mis-substitute\n //? or cause ReDoS. Use a function-form replacer to prevent `$`-sequences\n //? in the translation value from being interpreted by String.prototype.replace.\n const escapedKey = param.key.replaceAll(/[.*+?^${}()|[\\]\\\\]/g, String.raw`\\$&`);\n const regex = new RegExp(`\\\\{\\\\{${escapedKey}\\\\}\\\\}`, 'g');\n const replacement = String(param.value);\n finalResult = finalResult.replace(regex, () => replacement);\n }\n return finalResult;\n}\n\n//? Tiny convenience hook for the common case: pass `{ key, params }` and\n//? get the resolved string back.\nexport const useTranslator = () => {\n const translations = useTranslation();\n return ({ key, params }: { key: string; params?: TranslateParam[] }) =>\n translate({ translationList: translations, key, params });\n};\n\n//? Re-export helpers from the registry so consumers have a single import\n//? point for translation customization.\nexport { getActiveLanguage, getDefaultLocale, getRegisteredLocales };\n","//? Framework-owned i18n-backed toast notify. Reads the active language\n//? source + the registered locales map to translate `{ key, params }`\n//? before handing off to `sonner`. Registers itself with\n//? `registerNotifier` on import so framework packages (apiRequest,\n//? syncRequest) automatically emit toasts in the consumer's locale.\n//?\n//? Importing this module triggers the registration as a side effect —\n//? add `import { i18nNotify } from '@luckystack/core/client';` to the\n//? consumer's main entry once.\n\nimport { toast } from 'sonner';\nimport { registerNotifier } from '../notifier';\nimport { getActiveLanguage, getLocaleByCode } from '../localesRegistry';\nimport { translate, type TranslateParam } from './TranslationProvider';\n\ninterface NotifyInput {\n key: string;\n params?: TranslateParam[];\n}\n\nconst resolve = (input: NotifyInput): string => {\n const language = getActiveLanguage();\n const list = getLocaleByCode(language);\n return translate({\n translationList: (list && typeof list === 'object' ? list : {}) as Record<string, unknown>,\n key: input.key,\n params: input.params,\n });\n};\n\nconst notify = {\n success: (input: NotifyInput) => { toast.success(resolve(input)); },\n error: (input: NotifyInput) => { toast.error(resolve(input)); },\n info: (input: NotifyInput) => { toast.info(resolve(input)); },\n warning: (input: NotifyInput) => { toast.warning(resolve(input)); },\n};\n\n//? Side-effect: register with core so framework packages emit through\n//? this i18n-backed implementation.\nregisterNotifier(notify);\n\nexport default notify;\n","//? Framework-owned route guard. Wraps protected pages — runs the registered\r\n//? middleware handler (`registerMiddlewareHandler` in core) on every route\r\n//? change and renders children only when allowed. Redirects via React\r\n//? Router's `useNavigate` when the handler returns a redirect. Shows a\r\n//? loader only after a 200ms delay so fast checks don't flash a spinner.\r\n\r\nimport { ReactNode, useEffect, useRef, useState } from 'react';\r\nimport { useLocation, useNavigate } from 'react-router-dom';\r\nimport { getMiddlewareHandler, getPageMiddleware } from '../middlewareRegistry';\r\nimport { useSession } from './sessionContext';\r\nimport { getProjectConfig } from '../projectConfig';\r\n\r\nconst LOADER_DELAY_MS = 200;\r\n\r\nfunction MiddlewareLoader() {\r\n const [show, setShow] = useState(false);\r\n\r\n useEffect(() => {\r\n const id = globalThis.setTimeout(() => { setShow(true); }, LOADER_DELAY_MS);\r\n return () => { globalThis.clearTimeout(id); };\r\n }, []);\r\n\r\n if (!show) return null;\r\n\r\n return (\r\n <div className=\"w-full h-full flex items-center justify-center\" aria-busy=\"true\" aria-live=\"polite\">\r\n <div\r\n className=\"w-8 h-8 rounded-full border-2 border-container2-border border-t-primary animate-spin\"\r\n role=\"status\"\r\n aria-label=\"Loading\"\r\n />\r\n </div>\r\n );\r\n}\r\n\r\nexport default function Middleware({ children }: { children: ReactNode }) {\r\n const [allowed, setAllowed] = useState(false);\r\n const [checking, setChecking] = useState(true);\r\n\r\n const location = useLocation();\r\n const navigate = useNavigate();\r\n const { session, sessionLoaded } = useSession();\r\n\r\n //? Track the guarded route so a SESSION-only change re-evaluates the guard\r\n //? WITHOUT unmounting the page. A session update (avatar, roomCodes,\r\n //? preferences pushed via `updateSession`) used to flip `checking` back to\r\n //? true on every change → the whole page unmounted + remounted, wiping page\r\n //? state and aborting in-flight work. Joining a room (which saves the session\r\n //? and broadcasts `updateSession`) therefore looked like a full page refresh.\r\n //? Now we only show the loader / hide the page on a real ROUTE change; a bare\r\n //? session change re-checks in the background and only navigates away if the\r\n //? new session is no longer allowed.\r\n const routeKey = `${location.pathname}\u0000${location.search}`;\r\n const guardedRouteRef = useRef<string | null>(null);\r\n\r\n useEffect(() => {\r\n let isMounted = true;\r\n const isRouteChange = guardedRouteRef.current !== routeKey;\r\n guardedRouteRef.current = routeKey;\r\n if (isRouteChange) {\r\n setAllowed(false);\r\n setChecking(true);\r\n }\r\n\r\n void (async () => {\r\n const params = new URLSearchParams(location.search);\r\n const queryObject: Record<string, string> = {};\r\n for (const [key, value] of params.entries()) {\r\n queryObject[key] = value;\r\n }\r\n\r\n //? Wait for the SessionProvider to finish its initial fetch — up to\r\n //? 5 seconds. Without this, we'd evaluate the middleware against a\r\n //? not-yet-loaded session and bounce the user back to /login.\r\n let count = 0;\r\n while (!sessionLoaded) {\r\n await new Promise(res => setTimeout(res, 10));\r\n count++;\r\n if (count > 500) break;\r\n }\r\n\r\n //? Per-page middleware first (declared in each page.tsx via\r\n //? `export const middleware`), then the global fallback handler\r\n //? registered via `registerMiddlewareHandler`. This lets pages own\r\n //? their own guards while keeping a central catch-all for\r\n //? cross-cutting cases (server-reachability checks, analytics).\r\n const pageMw = getPageMiddleware(location.pathname);\r\n const handler = pageMw ?? getMiddlewareHandler();\r\n const result = await handler({\r\n location: location.pathname,\r\n searchParams: queryObject,\r\n session,\r\n });\r\n\r\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- isMounted intentional, see comment below\r\n if (!isMounted) return;\r\n\r\n if (result?.success) {\r\n setAllowed(true);\r\n } else if (result?.redirect) {\r\n void navigate(result.redirect);\r\n } else {\r\n //? Navigate to a safe, deterministic fallback instead of `navigate(-1)`,\r\n //? which chains backwards out of the app on repeated denials (e.g. when\r\n //? history is shallow or the previous page was also guarded).\r\n void navigate(getProjectConfig().loginRedirectUrl ?? '/');\r\n }\r\n\r\n setChecking(false);\r\n })();\r\n\r\n //! isMounted check is load-bearing — without it, repeated middleware\r\n //? denials chain navigate(-1) and walk too far up history. Keep it.\r\n return () => {\r\n isMounted = false;\r\n };\r\n }, [routeKey, location.pathname, location.search, navigate, session, sessionLoaded]);\r\n\r\n if (checking) return <MiddlewareLoader />;\r\n if (!allowed) return null;\r\n return <div className=\"w-full h-full\">{children}</div>;\r\n}\r\n","//? Wrapper around react-router-dom's `useNavigate` that runs the\n//? registered middleware handler before navigating. Returns the guarded\n//? navigate function. Use everywhere instead of `useNavigate` directly so\n//? programmatic navigations honor the same auth/redirect rules as\n//? `<Middleware>`-wrapped page renders.\n\nimport { useLocation, useNavigate } from 'react-router-dom';\nimport { getMiddlewareHandler, getPageMiddleware } from '../middlewareRegistry';\nimport { useSession } from './sessionContext';\n\nconst getParams = (locationSearch: string) => {\n const params = new URLSearchParams(locationSearch);\n const queryObject: Record<string, string> = {};\n for (const [key, value] of params.entries()) {\n queryObject[key] = value;\n }\n return queryObject;\n};\n\nexport default function useRouter() {\n const navigateHandler = useNavigate();\n const location = useLocation();\n const { session } = useSession();\n\n const navigate = async (path: string) => {\n const queryObject = getParams(location.search);\n //? Per-page middleware first (declared via `export const middleware`\n //? on the target page.tsx), then the global fallback. Mirrors the\n //? `<Middleware>` component's resolution order so programmatic\n //? navigations honor the same guards as direct URL hits.\n const pageMw = getPageMiddleware(path);\n const handler = pageMw ?? getMiddlewareHandler();\n const result = await handler({ location: path, searchParams: queryObject, session });\n\n if (result?.success) {\n return navigateHandler(path);\n }\n if (result && !result.success && result.redirect) {\n return navigateHandler(result.redirect);\n }\n return;\n };\n\n return navigate;\n}\n"],"mappings":"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","names":["resolve","effectiveTimeoutMs","createContext","use","useState","useCallback","useState","useCallback","createContext","use","useMemo","useState","jsx","createContext","useState","useMemo","use","notify","useEffect","useState","jsx","useState","useEffect","useLocation","useNavigate","useNavigate","useLocation"]}
|
|
1
|
+
{"version":3,"sources":["../src/apiInterceptors.ts","../src/tryCatchClient.ts","../src/routedHttpInvocation.ts","../src/apiRequest.ts","../src/localesRegistry.ts","../src/middlewareRegistry.ts","../src/react/sessionContext.ts","../src/react/AvatarProvider.tsx","../src/react/useTheme.ts","../src/react/TranslationProvider.tsx","../src/react/notify.ts","../src/react/Middleware.tsx","../src/react/Router.tsx"],"sourcesContent":["//? EXT-03 — client-side request/response interceptor registry for `apiRequest`.\n//?\n//? `apiRequest` (apiRequest.ts) is a monolithic typed function and Rule 21 /\n//? the `no-unsafe-api-wrappers` lint forbid wrapping it (a wrapper erases the\n//? route/version inference). This registry is the sanctioned escape hatch: a\n//? consumer registers an interceptor here instead of wrapping the call, so the\n//? typed call site is untouched and route/version inference survives.\n//?\n//? Two seams, both fire-and-mutate (NOT replace — the dispatcher reserves a\n//? structured return for control flow):\n//? - request interceptors run just before the socket emit. A handler may\n//? mutate `ctx.data` in place (inject a correlation id / feature-flag\n//? context / tenant header-equivalent) and is observation-only otherwise.\n//? - response interceptors run when the response envelope arrives, before the\n//? awaiting caller resolves. Observation-only (metrics, breadcrumb, custom\n//? logging). The envelope is NOT mutated by this seam — response rewriting\n//? belongs on the server `preApiRespond` / `transformApiResponse` hooks.\n//?\n//? Handlers are best-effort: a throw / rejection is caught + logged and never\n//? breaks the request pipeline. Both `register*` functions return an\n//? unsubscribe closure, matching `registerClientHook`'s contract.\n\nexport interface ApiRequestInterceptorContext {\n /** Resolved route name (`<page>/<api>` or `system/<api>`). */\n name: string;\n /** Route version literal (e.g. `'v1'`). */\n version: string;\n /**\n * Outgoing request payload. Mutate this object in place to augment the\n * request (correlation id, feature-flag context, ...). Replacing the\n * reference has no effect — mutate keys on the existing object.\n */\n data: Record<string, unknown>;\n}\n\nexport interface ApiResponseInterceptorContext {\n name: string;\n version: string;\n /** The response envelope the caller is about to receive (read-only intent). */\n response: { status: 'success' | 'error'; [key: string]: unknown };\n}\n\nexport type ApiRequestInterceptor = (\n ctx: ApiRequestInterceptorContext,\n) => void | Promise<void>;\n\nexport type ApiResponseInterceptor = (\n ctx: ApiResponseInterceptorContext,\n) => void | Promise<void>;\n\nimport { getLogger } from './loggerRegistry';\n\nconst requestInterceptors = new Set<ApiRequestInterceptor>();\nconst responseInterceptors = new Set<ApiResponseInterceptor>();\n\n/**\n * Register an outgoing-request interceptor. Returns an unsubscribe function.\n * Multiple interceptors run in registration order; each may mutate `ctx.data`.\n */\nexport function registerApiRequestInterceptor(interceptor: ApiRequestInterceptor): () => void {\n requestInterceptors.add(interceptor);\n return () => {\n requestInterceptors.delete(interceptor);\n };\n}\n\n/**\n * Register an incoming-response interceptor. Returns an unsubscribe function.\n */\nexport function registerApiResponseInterceptor(interceptor: ApiResponseInterceptor): () => void {\n responseInterceptors.add(interceptor);\n return () => {\n responseInterceptors.delete(interceptor);\n };\n}\n\n//? Framework-internal — invoked by `apiRequest`. Awaited so a handler can do an\n//? async lookup (read a flag store, mint a trace id) before the emit. Snapshot\n//? the set so a handler unregistering mid-dispatch can't skip a sibling.\nexport async function dispatchApiRequestInterceptors(\n ctx: ApiRequestInterceptorContext,\n): Promise<void> {\n if (requestInterceptors.size === 0) return;\n //? Snapshot so a handler that unregisters itself (or a sibling) mid-dispatch\n //? doesn't skip an un-visited handler.\n const interceptors = [...requestInterceptors];\n for (const interceptor of interceptors) {\n try {\n await interceptor(ctx);\n } catch (error) {\n getLogger().error('[apiRequestInterceptor] handler threw', error);\n }\n }\n}\n\n//? Framework-internal — invoked by `apiRequest` when the response arrives.\n//? Fire-and-forget (the caller does not await) so an interceptor can't delay\n//? the resolve; async rejections are caught + logged.\nexport function dispatchApiResponseInterceptors(\n ctx: ApiResponseInterceptorContext,\n): void {\n if (responseInterceptors.size === 0) return;\n //? Snapshot (see `dispatchApiRequestInterceptors`).\n const interceptors = [...responseInterceptors];\n for (const interceptor of interceptors) {\n try {\n const result = interceptor(ctx);\n if (result && typeof (result as Promise<unknown>).catch === 'function') {\n (result as Promise<unknown>).catch((error: unknown) => {\n getLogger().error('[apiResponseInterceptor] async handler rejected', error);\n });\n }\n } catch (error) {\n getLogger().error('[apiResponseInterceptor] handler threw', error);\n }\n }\n}\n\n/** Test helper — drop every registered interceptor. Not part of the public API. */\nexport function _resetApiInterceptorsForTests(): void {\n requestInterceptors.clear();\n responseInterceptors.clear();\n}\n","//? Browser-safe sibling of `tryCatch`. Behaviourally identical (same tuple,\n//? same params, same auto-capture on the error path) but it does NOT statically\n//? import `./sentrySetup`. The server `tryCatch.ts` reaches `sentrySetup` →\n//? `errorTrackerRegistry` → `node:async_hooks` at module-eval; that static edge\n//? is harmless on the server but drags a `node:`-externalized module into the\n//? Vite CLIENT bundle whenever a client importer (LoginForm) pulls\n//? `shared/tryCatch.ts`. So the client-reachable shim resolves HERE instead, and\n//? the only path to capture is a LAZY dynamic import taken on the error branch —\n//? keeping the node-bearing module out of the client's static graph.\n//?\n//? The server's `tryCatch.ts` is intentionally left untouched (byte-for-byte) so\n//? the server capture path keeps its synchronous, statically-linked behaviour.\n//? On the client, capture is best-effort and async-via-microtask is fine.\n\nexport default async function tryCatch<T, P = void>(\n func: (values: P) => Promise<T> | T,\n params?: P,\n context?: Record<string, unknown>\n): Promise<[Error | null, T | null]> {\n try {\n const response = await func(params as P);\n return [null, response];\n } catch (error) {\n //? Lazy capture: the import is only evaluated on the error branch, so the\n //? `node:async_hooks`-bearing module never enters the client's static graph.\n const { captureException } = await import('./sentrySetup');\n captureException(error, context);\n return [error instanceof Error ? error : new Error(String(error)), null];\n }\n}\n","import { httpFetch } from './csrf';\nimport tryCatch from './tryCatchClient';\nimport tryCatchSync from './tryCatchSync';\nimport type { HttpMethodLiteral } from './apiMethodMapRegistry';\n\nexport type RoutedInvocationMethod = HttpMethodLiteral;\n\nexport interface RoutedInvocationResponse<TResponse> {\n kind: 'response';\n response: TResponse;\n httpStatus: number;\n}\n\nexport interface RoutedInvocationFailure {\n kind: 'aborted' | 'timeout' | 'network-error' | 'invalid-response';\n}\n\nexport type RoutedInvocationResult<TResponse> =\n | RoutedInvocationResponse<TResponse>\n | RoutedInvocationFailure;\n\nexport interface RoutedHttpInvocationInput<TStream> {\n path: string;\n method: RoutedInvocationMethod;\n data: Record<string, unknown>;\n stream?: (payload: TStream) => void;\n signals?: readonly (AbortSignal | null | undefined)[];\n timeoutMs?: number | false;\n}\n\ninterface ParsedSseEvent {\n event: string;\n data: unknown;\n}\n\nconst parseJson = (value: string): unknown => {\n const [error, parsed] = tryCatchSync(() => JSON.parse(value) as unknown);\n return error ? undefined : parsed;\n};\n\nconst parseSseFrame = (frame: string): ParsedSseEvent | null => {\n let event = 'message';\n const dataLines: string[] = [];\n for (const rawLine of frame.split('\\n')) {\n const line = rawLine.endsWith('\\r') ? rawLine.slice(0, -1) : rawLine;\n if (line.length === 0 || line.startsWith(':')) continue;\n if (line.startsWith('event:')) {\n event = line.slice(6).trimStart();\n continue;\n }\n if (line.startsWith('data:')) {\n dataLines.push(line.slice(5).trimStart());\n }\n }\n if (dataLines.length === 0) return null;\n return { event, data: parseJson(dataLines.join('\\n')) };\n};\n\nconst parseSseResponse = async <TResponse>(\n response: Response,\n onStream: ((payload: never) => void) | undefined,\n): Promise<TResponse | null> => {\n const reader = response.body?.getReader();\n if (!reader) return null;\n\n const decoder = new TextDecoder();\n let buffer = '';\n let finalResponse: TResponse | null = null;\n\n const consumeFrames = (flush: boolean): void => {\n buffer = buffer.replaceAll('\\r\\n', '\\n');\n let boundary = buffer.indexOf('\\n\\n');\n while (boundary !== -1) {\n const frame = buffer.slice(0, boundary);\n buffer = buffer.slice(boundary + 2);\n const parsed = parseSseFrame(frame);\n if (parsed?.event === 'stream' && parsed.data !== undefined) {\n onStream?.(parsed.data as never);\n } else if ((parsed?.event === 'final' || parsed?.event === 'error') && parsed.data !== undefined) {\n finalResponse = parsed.data as TResponse;\n }\n boundary = buffer.indexOf('\\n\\n');\n }\n\n if (flush && buffer.trim().length > 0) {\n const parsed = parseSseFrame(buffer);\n if (parsed?.event === 'stream' && parsed.data !== undefined) {\n onStream?.(parsed.data as never);\n } else if ((parsed?.event === 'final' || parsed?.event === 'error') && parsed.data !== undefined) {\n finalResponse = parsed.data as TResponse;\n }\n buffer = '';\n }\n };\n\n let readDone = false;\n while (!readDone) {\n const { done, value } = await reader.read();\n readDone = done;\n if (!done) {\n buffer += decoder.decode(value, { stream: true });\n consumeFrames(false);\n }\n }\n buffer += decoder.decode();\n consumeFrames(true);\n return finalResponse;\n};\n\nconst buildRequest = ({\n path,\n method,\n data,\n stream,\n signal,\n}: {\n path: string;\n method: RoutedInvocationMethod;\n data: Record<string, unknown>;\n stream: boolean;\n signal: AbortSignal;\n}): { url: string; init: RequestInit } => {\n const headers = new Headers();\n if (stream) headers.set('Accept', 'text/event-stream');\n\n if (method === 'GET') {\n const params = new URLSearchParams();\n params.set('__luckystack_data', JSON.stringify(data));\n if (stream) params.set('stream', 'true');\n const separator = path.includes('?') ? '&' : '?';\n return {\n url: `${path}${separator}${params.toString()}`,\n init: { method, headers, signal },\n };\n }\n\n headers.set('Content-Type', 'application/json');\n return {\n url: stream ? `${path}${path.includes('?') ? '&' : '?'}stream=true` : path,\n init: {\n method,\n headers,\n signal,\n body: JSON.stringify(data),\n },\n };\n};\n\nexport const invokeRoutedHttp = async <TResponse, TStream = Record<string, unknown>>({\n path,\n method,\n data,\n stream,\n signals = [],\n timeoutMs,\n}: RoutedHttpInvocationInput<TStream>): Promise<RoutedInvocationResult<TResponse>> => {\n if (signals.some((signal) => signal?.aborted)) return { kind: 'aborted' };\n\n const controller = new AbortController();\n const timeoutReason = Symbol('routed-invocation-timeout');\n const didTimeout = (): boolean => controller.signal.reason === timeoutReason;\n const abortFromCaller = (): void => {\n controller.abort();\n };\n for (const signal of signals) signal?.addEventListener('abort', abortFromCaller, { once: true });\n\n const timeout = typeof timeoutMs === 'number' && timeoutMs > 0\n ? setTimeout(() => {\n controller.abort(timeoutReason);\n }, timeoutMs)\n : null;\n\n const cleanup = (): void => {\n if (timeout) clearTimeout(timeout);\n for (const signal of signals) signal?.removeEventListener('abort', abortFromCaller);\n };\n\n const request = buildRequest({\n path,\n method,\n data,\n stream: typeof stream === 'function',\n signal: controller.signal,\n });\n const [fetchError, response] = await tryCatch(() => httpFetch(request.url, request.init));\n if (fetchError || !response) {\n cleanup();\n if (didTimeout()) return { kind: 'timeout' };\n if (controller.signal.aborted) return { kind: 'aborted' };\n return { kind: 'network-error' };\n }\n\n const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';\n const [parseError, parsed] = await tryCatch(async () => {\n if (contentType.includes('text/event-stream')) {\n return parseSseResponse<TResponse>(response, stream);\n }\n return (await response.json()) as TResponse;\n });\n cleanup();\n\n if (parseError || parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n if (didTimeout()) return { kind: 'timeout' };\n if (controller.signal.aborted) return { kind: 'aborted' };\n return { kind: 'invalid-response' };\n }\n\n return { kind: 'response', response: parsed, httpStatus: response.status };\n};\n","import { getProjectConfig } from \"./projectConfig\";\nimport { getLogger } from \"./loggerRegistry\";\nimport { getRegisteredApiMethod } from \"./apiMethodMapRegistry\";\nimport { inferHttpMethod, isGetMethodName } from \"./httpApiUtils\";\nimport { incrementResponseIndex, socket, waitForSocket } from \"./socketState\";\nimport type { ApiTypeMap, StreamPayload } from './apiTypeStubs';\nimport { notify } from \"./notifier\";\nimport { enqueueApiRequest, isOnline, removeApiQueueItem } from \"./offlineQueue\";\nimport { Socket } from \"socket.io-client\";\nimport { normalizeErrorResponseCore } from \"./responseNormalizer\";\nimport { parseServiceRouteName } from \"./serviceRoute\";\nimport tryCatchSync from \"./tryCatchSync\";\nimport {\n buildApiResponseEventName,\n buildApiStreamEventName,\n socketEventNames,\n} from \"./socketEvents\";\nimport {\n dispatchApiRequestInterceptors,\n dispatchApiResponseInterceptors,\n} from \"./apiInterceptors\";\nimport { invokeRoutedHttp } from './routedHttpInvocation';\n\n//? Abort controller logic:\n//? - abortable: true → always use abort controller\n//? - abortable: false → never use abort controller\n//? - abortable: undefined → use abort controller for GET APIs (from generated types)\n//? Keyed by `fullName` + a stable hash of the request `data` so the\n//? replace-previous (dedupe) semantics only fire for an IDENTICAL in-flight\n//? call. Two components polling the same GET route with DIFFERENT params no\n//? longer abort each other (each gets its own bucket); a genuine duplicate\n//? still supersedes the prior one.\n//? Size-capped at 1000 entries. All normal code paths (resolve, abort, disconnect,\n//? timeout) call `cleanupAbortController()` which deletes the entry. The cap\n//? is a safety net for edge cases where cleanup is skipped (socket torn down\n//? without a `disconnect` event, or a future code path that forgets to clean up).\nconst ABORT_CONTROLLERS_MAX = 1000;\nconst abortControllers = new Map<string, AbortController>();\n\n//? Stable, order-insensitive JSON stringify so `{a:1,b:2}` and `{b:2,a:1}`\n//? hash to the same abort-controller key. Falls back to a non-deduping unique\n//? token when the payload can't be serialised (circular ref), which is the\n//? safe choice — an unserialisable payload should never silently abort a\n//? sibling request.\nconst stableStringify = (value: unknown): string => {\n const seen = new WeakSet<object>();\n const walk = (input: unknown): unknown => {\n if (input === null || typeof input !== 'object') return input;\n if (seen.has(input)) return '[circular]';\n seen.add(input);\n if (Array.isArray(input)) return input.map((entry) => walk(entry));\n const record = input as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(record).toSorted()) {\n out[key] = walk(record[key]);\n }\n return out;\n };\n const [error, json] = tryCatchSync(() => JSON.stringify(walk(value)));\n if (error || typeof json !== 'string') {\n return `__unserializable__:${String(Date.now())}-${String(Math.random())}`;\n }\n return json;\n};\n\nconst buildAbortKey = (fullName: string, data: unknown): string =>\n `${fullName}::${stableStringify(data)}`;\n\nexport type ApiStreamEvent<T extends StreamPayload = StreamPayload> = T;\n\n/**\n * Check if an API is a GET method.\n *\n * Primary path: consult the registered `apiMethodMap` (generated from the\n * actual handler's `httpMethod` export or name-based inference at codegen\n * time). Falls back to name-prefix heuristic only when the map isn't\n * registered yet — typically because an older consumer wrapper has not called\n * `registerApiMethodMap(...)` from `src/_sockets/apiRequest.ts` before re-exporting this helper.\n */\nconst isGetMethodByPrefix = (apiName: string): boolean => isGetMethodName(apiName.toLowerCase());\n\ntype RoutedApiFailureKind = 'timeout' | 'aborted' | 'network-error' | 'invalid-response';\n\nconst routedApiErrorCode = (kind: RoutedApiFailureKind): string => {\n if (kind === 'timeout') return 'api.timeout';\n if (kind === 'aborted') return 'request.aborted';\n if (kind === 'invalid-response') return 'api.invalidServerResponse';\n return 'api.ioUnavailable';\n};\n\nexport const resolveApiRequestHttpMethod = (apiName: string, version: string) => {\n const lastSlash = apiName.lastIndexOf('/');\n if (lastSlash > 0) {\n const pagePath = apiName.slice(0, lastSlash);\n const leaf = apiName.slice(lastSlash + 1);\n const method = getRegisteredApiMethod(pagePath, leaf, version);\n if (method) return method;\n }\n return inferHttpMethod(apiName);\n};\n\nconst resolveGetMethod = (apiName: string, version: string): { isGet: boolean; usingHeuristic: boolean } => {\n //? Resolved-name shape: `pagePath/apiName`. apiMethodMap is nested by\n //? pagePath → apiName → version. Split at the last `/` so multi-segment\n //? page paths (e.g. 'admin/users') still resolve.\n const lastSlash = apiName.lastIndexOf('/');\n if (lastSlash > 0) {\n const pagePath = apiName.slice(0, lastSlash);\n const leaf = apiName.slice(lastSlash + 1);\n const method = getRegisteredApiMethod(pagePath, leaf, version);\n if (method) return { isGet: method === 'GET', usingHeuristic: false };\n }\n return { isGet: isGetMethodByPrefix(apiName), usingHeuristic: true };\n};\n\nconst canSendNow = (socketInstance: Socket) => {\n if (!socketInstance.connected) return false;\n return isOnline();\n};\n\nconst createQueueId = () => {\n return `${String(Date.now())}-${String(Math.random())}`;\n};\n\n//? Resolve logging flags at call time so registration order doesn't matter.\n//? `getLogging()` is called inside each handler instead of captured at load.\nconst getLogging = () => getProjectConfig().logging;\nconst shouldLogDev = () => getLogging().devLogs;\nconst shouldNotifyDev = () => getLogging().devNotifications;\nconst shouldLogStream = () => getLogging().stream;\n\nconst shouldUseAbortController = ({\n abortable,\n isGet,\n usingHeuristic,\n}: {\n abortable: boolean | undefined;\n isGet: boolean;\n usingHeuristic: boolean;\n}) => {\n if (abortable === true) return true;\n if (abortable === false) return false;\n //? CORE-O15: when `abortable` is unspecified and we are relying on the\n //? name-prefix heuristic (apiMethodMap not registered), default to\n //? non-abortable. A mis-named POST (e.g. `getReport`) would otherwise\n //? silently abort an identical in-flight call it has nothing to do with.\n //? Once `registerApiMethodMap` is called (normal boot), the generated map\n //? resolves the method and the heuristic path is never taken.\n if (usingHeuristic) return false;\n return isGet;\n};\n\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Type Helpers\n// ═══════════════════════════════════════════════════════════════════════════════\n\n// Check if data input is required (i.e., T does NOT allow empty object)\n// Unions like {a:1} | {b:1} do NOT allow {}, so data will be required\ntype DataRequired<T> = Record<string, never> extends T ? false : true;\n\ntype UnionToIntersection<U> =\n (U extends unknown ? (arg: U) => void : never) extends ((arg: infer I) => void)\n ? I\n : never;\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Global API Params - Union of ALL valid API calls with proper data enforcement\n// ═══════════════════════════════════════════════════════════════════════════════\ntype ApiRouteRecord = UnionToIntersection<{\n [P in keyof ApiTypeMap]: {\n [N in keyof ApiTypeMap[P] as P extends 'root'\n ? `system/${Extract<N, string>}`\n : `${Extract<P, string>}/${Extract<N, string>}`]: ApiTypeMap[P][N]\n }\n}[keyof ApiTypeMap]>;\n\ntype ApiFullName = Extract<keyof ApiRouteRecord, string>;\ntype VersionsForFullName<F extends ApiFullName> = keyof ApiRouteRecord[F] & string;\n\n// Force expansion of types to clear aliases in tooltips\ntype Prettify<T> = { [K in keyof T]: T[K] } & {};\n\n// Get input type for an API name (union if exists on multiple pages)\ntype InputForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = ApiRouteRecord[F][V] extends { input: infer I }\n ? I\n : never;\n\n// Get output type for an API name (union if exists on multiple pages)\ntype OutputForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = ApiRouteRecord[F][V] extends { output: infer O }\n ? O\n : never;\n\ntype StreamForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = ApiRouteRecord[F][V] extends { stream: infer S }\n ? S\n : never;\n\ntype ApiStreamCallbackForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> =\n [StreamForFullName<F, V>] extends [never]\n ? never\n : (event: ApiStreamEvent<Prettify<StreamForFullName<F, V> extends StreamPayload ? StreamForFullName<F, V> : StreamPayload>>) => void;\n\n// Build params type for a specific API name\ntype ApiParamsForFullName<\n F extends ApiFullName,\n V extends VersionsForFullName<F>\n> = DataRequired<InputForFullName<F, V>> extends true\n ? { name: F; version: V; data: Prettify<InputForFullName<F, V>>; abortable?: boolean; disableErrorMessage?: boolean; onStream?: ApiStreamCallbackForFullName<F, V>; signal?: AbortSignal; timeoutMs?: number | false; }\n : { name: F; version: V; data?: Prettify<InputForFullName<F, V>>; abortable?: boolean; disableErrorMessage?: boolean; onStream?: ApiStreamCallbackForFullName<F, V>; signal?: AbortSignal; timeoutMs?: number | false; };\n\ninterface RuntimeApiParams {\n name?: string;\n version?: string;\n data?: unknown;\n abortable?: boolean;\n disableErrorMessage?: boolean;\n onStream?: (event: ApiStreamEvent) => void;\n signal?: AbortSignal;\n /** Per-call response timeout (ms). Overrides `api.requestTimeoutMs`. `false` disables. */\n timeoutMs?: number | false;\n}\n\nexport interface ApiErrorResponse extends Record<string, unknown> {\n status: 'error';\n httpStatus: number;\n message: string;\n errorCode: string;\n errorParams?: { key: string; value: string | number | boolean }[];\n}\n\ninterface ApiSuccessResponse extends Record<string, unknown> {\n status: 'success';\n httpStatus: number;\n}\n\ntype ApiResponse = ApiErrorResponse | ApiSuccessResponse;\n\nconst normalizeApiError = ({\n response,\n fallbackErrorCode,\n fallbackHttpStatus = 500,\n}: {\n response: { status: 'error'; errorCode?: string; errorParams?: { key: string; value: string | number | boolean }[]; httpStatus?: number; message?: string };\n fallbackErrorCode: string;\n fallbackHttpStatus?: number;\n}): ApiErrorResponse => {\n const normalized = normalizeErrorResponseCore({\n response,\n fallbackErrorCode,\n fallbackHttpStatus,\n resolveMessage: ({ errorCode }) => {\n if (typeof response.message === 'string' && response.message.trim().length > 0) {\n return response.message;\n }\n return errorCode;\n },\n });\n\n return {\n status: 'error',\n message: normalized.message,\n errorCode: normalized.errorCode,\n errorParams: normalized.errorParams,\n httpStatus: normalized.httpStatus ?? fallbackHttpStatus,\n };\n};\n\n/**\n * Type-safe API request function.\n * \n * @example\n * ```typescript\n * // Full name usage - includes page in the name\n * const result = await apiRequest({ name: 'examples/publicApi', version: 'v1', data: { message: 'hello' } });\n * // result is typed correctly for publicApi\n * \n * // Global APIs use service-first naming\n * await apiRequest({ name: 'system/session', version: 'v1' });\n * ```\n */\n\nexport function apiRequest<F extends ApiFullName, V extends VersionsForFullName<F>>(\n params: ApiParamsForFullName<F, V>\n): Promise<Prettify<OutputForFullName<F, V>> | ApiErrorResponse> {\n type RequestOutput = Prettify<OutputForFullName<F, V> & ApiResponse>;\n const runtimeParams = params as RuntimeApiParams;\n const { name, version, disableErrorMessage = false, onStream, signal: externalSignal, timeoutMs } = runtimeParams;\n const payloadData = runtimeParams.data;\n\n return new Promise<RequestOutput>((resolve) => {\n void (async () => {\n //? B1 — if the consumer-supplied signal is already aborted at call\n //? time, short-circuit before we even touch the socket. Mirrors the\n //? `fetch(url, { signal })` contract.\n if (externalSignal?.aborted) {\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }) as RequestOutput);\n return;\n }\n if (!name || typeof name !== \"string\") {\n if (shouldLogDev()) {\n getLogger().error(\"apiRequest: Invalid name\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'api.invalidName' });\n }\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'api.invalidName' },\n fallbackErrorCode: 'api.invalidName',\n }) as RequestOutput);\n return;\n }\n\n if (!version || typeof version !== 'string') {\n if (shouldLogDev()) {\n getLogger().error(\"apiRequest: Invalid version\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'api.invalidVersion' });\n }\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'api.invalidVersion' },\n fallbackErrorCode: 'api.invalidVersion',\n }) as RequestOutput);\n return;\n }\n\n const parsedRoute = parseServiceRouteName(name);\n if (parsedRoute.status === 'error') {\n if (shouldLogDev()) {\n getLogger().error(`[apiRequest] Invalid service route name`, undefined, { name, reason: parsedRoute.reason });\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'routing.invalidServiceRouteName' });\n }\n resolve(normalizeErrorResponseCore({\n response: {\n status: 'error',\n errorCode: 'routing.invalidServiceRouteName',\n errorParams: [{ key: 'name', value: name }],\n },\n fallbackErrorCode: 'routing.invalidServiceRouteName',\n }) as RequestOutput);\n return;\n }\n\n const sanitizedName = parsedRoute.normalizedRouteName;\n\n const data = payloadData && typeof payloadData === \"object\" ? payloadData : {};\n\n if (!await waitForSocket()) {\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'api.ioUnavailable' },\n fallbackErrorCode: 'api.ioUnavailable',\n }) as RequestOutput);\n return;\n }\n if (!socket) {\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'api.ioUnavailable' },\n fallbackErrorCode: 'api.ioUnavailable',\n }) as RequestOutput);\n return;\n }\n\n //? Abort controller logic:\n //? - abortable: true → always use abort controller\n //? - abortable: false → never use abort controller\n //? - abortable: undefined → smart default (GET-like APIs get abort controller)\n //? Pass the full pagePath/apiName so the registry lookup can resolve via\n //? the generated apiMethodMap (falls back to the leaf-name prefix heuristic\n //? when the map isn't registered yet).\n const { isGet, usingHeuristic } = resolveGetMethod(sanitizedName, version);\n const useAbortController = shouldUseAbortController({\n abortable: runtimeParams.abortable,\n isGet,\n usingHeuristic,\n });\n const fullName = `api/${sanitizedName}/${version}`;\n //? Per-payload abort key: only an identical in-flight call to the same\n //? route supersedes the previous one (see `buildAbortKey` rationale).\n const abortKey = buildAbortKey(fullName, data);\n\n let signal: AbortSignal | null = null;\n let abortHandler: (() => void) | null = null;\n let queueId: string | null = null;\n let cleanupStreamListener: (() => void) | null = null;\n //? Response listener cleanup, registered once the `.once` handler is\n //? attached in `runRequest`. Lets the abort path proactively `off()` the\n //? lingering response listener instead of waiting for a response that was\n //? cancelled and will never arrive.\n let cleanupResponseListener: (() => void) | null = null;\n //? Response-timeout timer. Armed right after the request is emitted so a\n //? lost response (server restart/crash between emit and reply) settles the\n //? promise instead of hanging the awaiting caller forever.\n let responseTimeout: ReturnType<typeof setTimeout> | null = null;\n const clearResponseTimeout = () => {\n if (responseTimeout) {\n clearTimeout(responseTimeout);\n responseTimeout = null;\n }\n };\n\n const cleanupAbortController = () => {\n if (signal && abortHandler) {\n signal.removeEventListener(\"abort\", abortHandler);\n }\n abortControllers.delete(abortKey);\n };\n\n if (useAbortController) {\n if (abortControllers.has(abortKey)) {\n const prevAbortController = abortControllers.get(abortKey);\n prevAbortController?.abort();\n }\n //? Evict the oldest entry when the cap is reached (LRU-insert-order).\n //? Map preserves insertion order; the first key is the oldest in-flight entry.\n if (!abortControllers.has(abortKey) && abortControllers.size >= ABORT_CONTROLLERS_MAX) {\n const oldest = abortControllers.keys().next().value;\n if (oldest !== undefined) abortControllers.delete(oldest);\n }\n const abortController = new AbortController();\n abortControllers.set(abortKey, abortController);\n signal = abortController.signal;\n\n abortHandler = () => {\n clearResponseTimeout();\n cleanupStreamListener?.();\n cleanupStreamListener = null;\n cleanupResponseListener?.();\n cleanupResponseListener = null;\n cleanupAbortController();\n if (queueId) {\n removeApiQueueItem(queueId);\n }\n //? Resolve (not reject) with the same `request.aborted` envelope the\n //? external-signal abort path uses. Rejecting here produced an\n //? unhandled rejection for fire-and-forget GET calls that get\n //? superseded by the default replace-previous behaviour.\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }) as RequestOutput);\n };\n\n signal.addEventListener(\"abort\", abortHandler);\n }\n\n const runRequest = (socketInstance: Socket) => {\n if (!canSendNow(socketInstance)) {\n queueId ??= createQueueId();\n //? For the immediate reject-new path (`drop-newest` / `reject`\n //? policy) THIS item's `onDrop` fires SYNCHRONOUSLY inside\n //? `enqueueApiRequest`. We keep surfacing that as the historical\n //? `offline.queueFull` via the `!enqueued` branch, so suppress the\n //? `onDrop` resolve during the synchronous enqueue window and re-arm\n //? it only if the item was actually queued (so a LATER async eviction\n //? — drop-oldest by a future enqueue, or age expiry — still settles).\n let suppressOnDrop = true;\n //? `enqueueApiRequest` returns `false` when the offline queue is\n //? full and `dropPolicy: 'reject'` is active. Without honoring\n //? that signal the outer promise never settles and the caller\n //? awaits forever. Resolve with a normalized error envelope so\n //? callers can branch.\n const enqueued = enqueueApiRequest({\n id: queueId,\n key: fullName,\n run: (nextSocket) => {\n runRequest(nextSocket);\n },\n createdAt: Date.now(),\n //? CORE: mirror SYNC-09. Settle the awaiting caller when this\n //? QUEUED request is later EVICTED (drop-oldest by a future\n //? enqueue / age expiry) instead of ever running. Without it the\n //? promise hangs forever, since `run` is the only thing that could\n //? otherwise resolve it — `apiRequest` had drifted from the sibling\n //? `syncRequest` which already had this fix. Also tear down the\n //? abort-controller registry entry so it isn't leaked.\n onDrop: (reason) => {\n if (suppressOnDrop) return;\n cleanupAbortController();\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'offline.dropped', errorParams: [{ key: 'reason', value: reason }] },\n fallbackErrorCode: 'offline.dropped',\n }) as RequestOutput);\n },\n });\n //? Past the synchronous enqueue window: if the item is sitting in the\n //? queue, a future eviction should settle via `onDrop`.\n suppressOnDrop = false;\n if (!enqueued) {\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'offline.queueFull', httpStatus: 503 },\n fallbackErrorCode: 'offline.queueFull',\n fallbackHttpStatus: 503,\n }) as RequestOutput);\n } else if (externalSignal && queueId) {\n //? CORE-O14: if an external AbortSignal fires while the item is\n //? sitting in the offline queue, its listener inside runRequest has\n //? not been registered yet. Register a one-shot handler here so the\n //? abort is not silently ignored and the queue slot is reclaimed.\n const capturedQueueId = queueId;\n const queueAbortHandler = () => {\n removeApiQueueItem(capturedQueueId);\n cleanupAbortController();\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }) as RequestOutput);\n };\n externalSignal.addEventListener('abort', queueAbortHandler, { once: true });\n }\n return;\n }\n\n if (signal?.aborted) {\n return;\n }\n\n if (getProjectConfig().transport.invocation === 'routed-http') {\n void (async () => {\n const effectiveTimeoutMs = timeoutMs ?? getProjectConfig().api.requestTimeoutMs;\n const routed = await invokeRoutedHttp<ApiResponse, ApiStreamEvent>({\n path: `/api/${sanitizedName}/${version}`,\n method: resolveApiRequestHttpMethod(sanitizedName, version),\n data: data as Record<string, unknown>,\n stream: onStream,\n signals: [signal, externalSignal],\n timeoutMs: effectiveTimeoutMs,\n });\n cleanupAbortController();\n if (queueId) removeApiQueueItem(queueId);\n\n if (routed.kind !== 'response') {\n const errorCode = routedApiErrorCode(routed.kind);\n resolve(normalizeApiError({\n response: {\n status: 'error',\n errorCode,\n httpStatus: routed.kind === 'timeout' ? 504 : undefined,\n },\n fallbackErrorCode: errorCode,\n fallbackHttpStatus: routed.kind === 'timeout' ? 504 : 500,\n }) as RequestOutput);\n return;\n }\n\n const response = routed.response;\n dispatchApiResponseInterceptors({ name: sanitizedName, version, response });\n if (response.status === 'error') {\n const normalizedError = normalizeErrorResponseCore({ response });\n if (!disableErrorMessage) {\n notify.error({\n key: normalizedError.errorCode || normalizedError.message,\n params: normalizedError.errorParams,\n });\n }\n Object.assign(response, normalizedError);\n }\n resolve(response as RequestOutput);\n })();\n return;\n }\n\n const tempIndex = incrementResponseIndex();\n socketInstance.emit(socketEventNames.apiRequest, { name: fullName, data, responseIndex: tempIndex });\n\n //? B1 — bridge the consumer's AbortSignal to the server. When the\n //? signal fires we emit `apiCancel { responseIndex }` so the server\n //? handler stops emitting new chunks. We also resolve locally with\n //? `request.aborted` so the awaiting caller settles (vs awaiting\n //? a response that will never arrive because we cancelled it).\n let cleanupExternalAbort: (() => void) | null = null;\n if (externalSignal) {\n const externalAbortHandler = () => {\n clearResponseTimeout();\n socketInstance.emit(socketEventNames.apiCancel, { responseIndex: tempIndex });\n cleanupStreamListener?.();\n cleanupStreamListener = null;\n cleanupResponseListener?.();\n cleanupResponseListener = null;\n cleanupAbortController();\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }) as RequestOutput);\n };\n externalSignal.addEventListener('abort', externalAbortHandler);\n cleanupExternalAbort = () => {\n externalSignal.removeEventListener('abort', externalAbortHandler);\n };\n }\n\n //? Arm the response timeout. A per-call `timeoutMs` overrides the\n //? `api.requestTimeoutMs` config default; either may be `false` to\n //? disable. On expiry we tear down every listener/controller and settle\n //? with a 504 envelope so the awaiting caller never hangs.\n clearResponseTimeout();\n const effectiveTimeoutMs = timeoutMs ?? getProjectConfig().api.requestTimeoutMs;\n if (typeof effectiveTimeoutMs === 'number' && effectiveTimeoutMs > 0) {\n responseTimeout = setTimeout(() => {\n responseTimeout = null;\n cleanupStreamListener?.();\n cleanupStreamListener = null;\n cleanupResponseListener?.();\n cleanupResponseListener = null;\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n cleanupAbortController();\n if (queueId) {\n removeApiQueueItem(queueId);\n }\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'api.timeout', httpStatus: 504 },\n fallbackErrorCode: 'api.timeout',\n fallbackHttpStatus: 504,\n }) as RequestOutput);\n }, effectiveTimeoutMs);\n }\n\n if (typeof onStream === 'function') {\n const streamEventName = buildApiStreamEventName(tempIndex);\n const streamListener = (streamPayload: ApiStreamEvent) => {\n if (signal?.aborted) {\n return;\n }\n\n if (shouldLogStream()) {\n getLogger().debug(`Server API Stream(${String(tempIndex)})`, { APINAME: sanitizedName, streamPayload });\n }\n\n onStream(streamPayload);\n };\n\n socketInstance.on(streamEventName, streamListener);\n cleanupStreamListener = () => {\n socketInstance.off(streamEventName, streamListener);\n };\n }\n\n if (shouldLogDev()) {\n getLogger().debug(`Client API Request(${String(tempIndex)})`, { APINAME: sanitizedName, data });\n }\n\n //? Inside the handler we type as the runtime envelope (ApiResponse).\n //? Project-specific output narrowing only matters at the call site,\n //? where `RequestOutput = OutputForFullName<F,V> & ApiResponse` is\n //? returned via `resolve(... as RequestOutput)`. When core is type-\n //? checked in isolation (e.g. per-package tsup dts build), ApiTypeMap\n //? is empty so OutputForFullName collapses to never; using ApiResponse\n //? here keeps the body type-safe in both cases.\n const responseEventName = buildApiResponseEventName(tempIndex);\n const responseListener = (response: ApiResponse) => {\n if (signal?.aborted) {\n clearResponseTimeout();\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n return;\n }\n\n clearResponseTimeout();\n cleanupStreamListener?.();\n cleanupStreamListener = null;\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n //? Call cleanup (not just null) so that any watcher layered on top of\n //? cleanupResponseListener (e.g. the CORE-O13 disconnect watcher) is\n //? properly torn down when a normal response arrives.\n cleanupResponseListener?.();\n cleanupResponseListener = null;\n\n const status = response.status;\n\n //? EXT-03 — fire client response interceptors when the envelope\n //? arrives (observation-only: metrics, breadcrumb, custom logging).\n dispatchApiResponseInterceptors({ name: sanitizedName, version, response });\n\n if (shouldLogDev()) {\n getLogger().debug(`Server API Response(${String(tempIndex)})`, { ...response, APINAME: sanitizedName });\n }\n\n if (status === \"error\") {\n const normalizedError = normalizeErrorResponseCore({ response });\n\n if (!disableErrorMessage) {\n if (normalizedError.errorCode) {\n notify.error({ key: normalizedError.errorCode, params: normalizedError.errorParams });\n } else {\n notify.error({ key: normalizedError.message });\n }\n }\n\n Object.assign(response, normalizedError);\n cleanupAbortController();\n resolve(response as RequestOutput);\n return;\n }\n\n cleanupAbortController();\n\n resolve(response as RequestOutput);\n };\n\n socketInstance.once(responseEventName, responseListener);\n cleanupResponseListener = () => {\n socketInstance.off(responseEventName, responseListener);\n };\n\n //? CORE-O13: when timeout is disabled AND there is no abort controller\n //? a server restart between emit and reply leaves this promise pending\n //? forever, leaking the response listener. The disconnect event is the\n //? authoritative signal that no reply will ever arrive on this socket.\n const disconnectHandler = () => {\n if (cleanupResponseListener) {\n cleanupResponseListener();\n cleanupResponseListener = null;\n }\n cleanupStreamListener?.();\n cleanupStreamListener = null;\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n clearResponseTimeout();\n cleanupAbortController();\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'api.disconnected' },\n fallbackErrorCode: 'api.disconnected',\n }) as RequestOutput);\n };\n socketInstance.once('disconnect', disconnectHandler);\n //? Wrap the response listener to also remove the disconnect watcher on\n //? a normal reply (avoids a dangling listener on every successful call).\n const originalCleanupResponseListener = cleanupResponseListener;\n cleanupResponseListener = () => {\n socketInstance.off('disconnect', disconnectHandler);\n originalCleanupResponseListener();\n };\n };\n\n //? EXT-03 — fire client request interceptors just before the emit. They\n //? may mutate `data` in place (correlation id / feature-flag context).\n //? `data` is the same reference passed to `socketInstance.emit`, so the\n //? mutation is on the wire. Awaited so an async interceptor (flag lookup,\n //? trace-id mint) completes first; failures are caught + logged inside.\n await dispatchApiRequestInterceptors({\n name: sanitizedName,\n version,\n data: data as Record<string, unknown>,\n });\n\n //? CORE-O12: re-check the external abort signal after the interceptor\n //? await. A slow async interceptor adds uncounted latency; if the caller\n //? aborted during that window the signal is already fired but nothing has\n //? yet attached its listener (that happens inside runRequest). Settling\n //? here avoids emitting a request whose response will never be consumed.\n if (externalSignal?.aborted) {\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }) as RequestOutput);\n return;\n }\n\n runRequest(socket);\n })();\n });\n}","//? Locales registry — lets the framework's TranslationProvider + notify\n//? read the consumer's translation JSON files without baking them into the\n//? package. Consumer overlay: `luckystack/i18n/locales.ts` calls\n//? `registerLocales({ en: enJson, nl: nlJson, ... })`.\n//?\n//? Language source: a separate registry — `registerLanguageSource(fn)` —\n//? lets the consumer wire up where the active language comes from\n//? (typically the SessionProvider's current `session.language`). When no\n//? source is registered, the framework falls back to\n//? `getProjectConfig().defaultLanguage`.\n\nimport { getProjectConfig } from './projectConfig';\n\nexport type LocalesMap = Record<string, unknown>;\nexport type LanguageSource = () => string | null | undefined;\n\nlet activeLocales: LocalesMap = {};\nlet activeLanguageSource: LanguageSource | null = null;\n\nexport const registerLocales = (locales: LocalesMap): void => {\n activeLocales = locales;\n};\n\nexport const getRegisteredLocales = (): LocalesMap => activeLocales;\n\nexport const getDefaultLocale = (): string => getProjectConfig().defaultLanguage || 'en';\n\nexport const registerLanguageSource = (fn: LanguageSource): void => {\n activeLanguageSource = fn;\n};\n\nexport const getActiveLanguage = (): string => {\n if (activeLanguageSource) {\n const lang = activeLanguageSource();\n if (typeof lang === 'string' && lang.length > 0) return lang;\n }\n return getDefaultLocale();\n};\n\n//? Lookup a language's translation tree by code. Falls back to default\n//? language when the code isn't registered. Used by TranslationProvider\n//? + notify to resolve the active translations.\nexport const getLocaleByCode = (code: string): unknown => {\n if (code in activeLocales) return activeLocales[code];\n const fallback = getDefaultLocale();\n if (fallback in activeLocales) return activeLocales[fallback];\n return {};\n};\n","//? Middleware handler registry. The framework's `Middleware` + `Router`\n//? components (in `./react/`) call the registered handler on every\n//? navigation change to decide whether the user is allowed on the target\n//? route. Per-page `export const middleware` (see `registerPageMiddleware`\n//? below) is the canonical path; a consumer wanting a cross-cutting GLOBAL\n//? guard registers it from the client bootstrap (typically `main.tsx`) via\n//? `registerMiddlewareHandler(...)`. No separate\n//? `src/_functions/middlewareHandler.ts` file is required.\n\nimport type { BaseSessionLayout } from './sessionTypes';\n\nexport interface MiddlewareInput {\n location: string;\n searchParams: Record<string, string>;\n session: BaseSessionLayout | null;\n}\n\nexport type MiddlewareResult =\n | { success: true; redirect?: undefined }\n | { success: false; redirect: string }\n | undefined;\n\nexport type MiddlewareHandler = (input: MiddlewareInput) => MiddlewareResult | Promise<MiddlewareResult>;\n\n//? Default handler: allow by default. Per-page `middleware` exports on each\n//? `page.tsx` are the canonical route-guard path (see `registerPageMiddleware`\n//? below); the global handler is only consulted as a fallback for pages\n//? that don't declare their own guard. Allowing-by-default keeps public\n//? routes (`/`, `/login`, `/register`, ...) working without forcing every\n//? consumer to ship a `src/_functions/middlewareHandler.ts` boilerplate\n//? file just to opt-in to \"let the user through\".\n//?\n//? Consumers who need a cross-cutting global guard (telemetry, server-\n//? reachability check, maintenance banner gate) call\n//? `registerMiddlewareHandler(...)` directly from `main.tsx` with their own\n//? function — no separate file required.\nconst DEFAULT_HANDLER: MiddlewareHandler = () => ({ success: true });\n\nlet activeHandler: MiddlewareHandler = DEFAULT_HANDLER;\n\nexport const registerMiddlewareHandler = (handler: MiddlewareHandler): void => {\n activeHandler = handler;\n};\n\nexport const getMiddlewareHandler = (): MiddlewareHandler => activeHandler;\n\n//? Per-page middleware registry. Counterpart to `registerMiddlewareHandler`\n//? for the common case where each page declares its own guard locally\n//? (`export const middleware = ({ session }) => {...}` from page.tsx).\n//? The framework's `<Middleware>` component checks this map FIRST and only\n//? falls back to the global handler when no per-page entry is registered.\n//?\n//? `PageMiddleware<TSession>` matches the existing `MiddlewareHandler`\n//? signature so any handler shape is reusable on either side. The generic\n//? is purely ergonomic for consumer-side type narrowing — module\n//? augmentation of `BaseSessionLayout` flows through.\n\nexport type PageMiddleware<TSession extends BaseSessionLayout = BaseSessionLayout> = (\n input: { location: string; searchParams: Record<string, string>; session: TSession | null },\n) => MiddlewareResult | Promise<MiddlewareResult>;\n\nconst pageMiddlewares = new Map<string, PageMiddleware>();\n\nexport const registerPageMiddleware = (path: string, middleware: PageMiddleware): void => {\n pageMiddlewares.set(path, middleware);\n};\n\nexport const getPageMiddleware = (path: string): PageMiddleware | undefined =>\n pageMiddlewares.get(path);\n\nexport const hasPageMiddleware = (path: string): boolean => pageMiddlewares.has(path);\n\n/** Test helper — drop every registered per-page middleware. */\nexport const clearPageMiddlewaresForTests = (): void => {\n pageMiddlewares.clear();\n};\n","//? Shared session context. Defined in core so framework-React components\n//? (Middleware, Router, useSession-aware UI) can consume it without\n//? pulling in app code. The consumer's `SessionProvider` (in their\n//? template's `_providers/`) does the actual fetch/socket/lifecycle work\n//? and writes to this context via `<SessionContext value={...}>`.\n//?\n//? The session shape inside the context is intentionally loose\n//? (`BaseSessionLayout | null`) so app code with extended session types\n//? can read it through a typed `useSession<MySession>()` cast.\n\nimport { createContext, use } from 'react';\nimport type { BaseSessionLayout } from '../sessionTypes';\nimport {\n dispatchClientHook,\n dispatchVetoableClientHook,\n type ClientHookStopSignal,\n} from '../clientHookBus';\n\nexport interface SessionContextValue<TSession extends BaseSessionLayout = BaseSessionLayout> {\n session: TSession | null;\n sessionLoaded: boolean;\n}\n\nconst DEFAULT_VALUE: SessionContextValue = {\n session: null,\n sessionLoaded: false,\n};\n\nexport const SessionContext = createContext<SessionContextValue>(DEFAULT_VALUE);\n\nexport function useSession<TSession extends BaseSessionLayout = BaseSessionLayout>(): SessionContextValue<TSession> {\n return use(SessionContext) as SessionContextValue<TSession>;\n}\n\n//? Non-React accessor for code that runs outside the component tree\n//? (e.g. notify() called from a non-component). Kept in sync with the\n//? context by the consumer's SessionProvider via `setLatestSession()`.\nlet latestSession: BaseSessionLayout | null = null;\n\nexport const setLatestSession = (session: BaseSessionLayout | null): void => {\n const previous = latestSession;\n latestSession = session;\n\n //? Fire client-side lifecycle hooks on null↔non-null transitions. Covers\n //? both fresh login AND existing-session restore on page reload — both\n //? cases need the same downstream actions (room auto-join, analytics\n //? identify, etc). Same-id updates (avatar refresh, profile change) do\n //? NOT fire postLogin; if a `sessionUpdate` hook is needed later, add it\n //? as a third branch without breaking existing handlers.\n if (previous === null && session !== null) {\n dispatchClientHook('postLogin', { session });\n } else if (previous !== null && session === null) {\n dispatchClientHook('postLogout', { previousSession: previous });\n }\n};\n\n//? Caller-supplied `TSession` is used to narrow the return type at the\n//? callsite (consumers extend `BaseSessionLayout`). The lint rule sees it as\n//? \"used once\" but the parameter exists solely to surface the consumer's\n//? widened session type through the read API.\n// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- callsite-narrowing use\nexport const getCurrentSession = <TSession extends BaseSessionLayout = BaseSessionLayout>(): TSession | null =>\n latestSession as TSession | null;\n\n/**\n * Vetoable entry point for committing a null → session transition (i.e. a\n * fresh login or a page-load session restore). Dispatches the `preLogin`\n * client hook to every registered handler in parallel; if any returns a\n * stop signal, the transition is aborted and `{ committed: false, signal }`\n * is returned. Otherwise, `setLatestSession(session)` runs synchronously\n * (which fires `postLogin`) and `{ committed: true }` comes back.\n *\n * Consumer's `SessionProvider` should `await` this for non-null session\n * commits and roll local React state back to `null` when a veto comes\n * through, so the UI doesn't render a half-logged-in state.\n */\nexport type ProposeLoginResult =\n | { stopped: false; committed: true }\n | { stopped: true; signal: ClientHookStopSignal; committed: false };\n\nexport const proposeLogin = async (\n session: BaseSessionLayout,\n): Promise<ProposeLoginResult> => {\n const veto = await dispatchVetoableClientHook('preLogin', { candidateSession: session });\n if (veto.stopped) return { stopped: true, signal: veto.signal, committed: false };\n setLatestSession(session);\n return { stopped: false, committed: true };\n};\n","/* eslint-disable react-refresh/only-export-components */\nimport { createContext, use, useState, useMemo, useCallback, ReactNode } from 'react';\n\nexport type AvatarStatus = 'avatar' | 'fallback';\n\ninterface AvatarContextType {\n avatarStatuses: Record<string, AvatarStatus>;\n setAvatarStatus: (key: string, status: AvatarStatus) => void;\n}\n\nconst AvatarContext = createContext<AvatarContextType | null>(null);\n\nexport const AvatarProvider = ({ children }: { children: ReactNode }) => {\n const [avatarStatuses, setAvatarStatuses] = useState<Record<string, AvatarStatus>>({});\n\n const setAvatarStatus = useCallback((key: string, status: AvatarStatus) => {\n setAvatarStatuses(prev => ({ ...prev, [key]: status }));\n }, []);\n\n const contextValue = useMemo(() => ({\n avatarStatuses,\n setAvatarStatus,\n }), [avatarStatuses, setAvatarStatus]);\n\n return (\n <AvatarContext value={contextValue}>\n {children}\n </AvatarContext>\n );\n};\n\nexport function useAvatarContext() {\n const ctx = use(AvatarContext);\n if (!ctx) throw new Error('useAvatarContext must be used within AvatarProvider');\n return ctx;\n}\n","//? `useTheme()` — framework-owned theme state hook. Default value comes\n//? from `getProjectConfig().defaultTheme` (the union literal exported by\n//? the consumer's `config.ts`). Toggling sets the `.dark` class on\n//? `<html>`. Persistence (e.g. saving to `User.theme` via\n//? `settings/updateUser`) is up to the consumer — the hook only owns\n//? local state + DOM class.\n\nimport { useState, useCallback } from 'react';\nimport { getProjectConfig } from '../projectConfig';\n\nexport type Theme = 'light' | 'dark';\n\nconst applyDomClass = (theme: Theme): void => {\n if (typeof document === 'undefined') return;\n document.documentElement.classList.toggle('dark', theme === 'dark');\n};\n\nexport const useTheme = () => {\n const initialTheme = (getProjectConfig().defaultTheme) ?? 'light';\n const [theme, setThemeState] = useState<Theme>(initialTheme);\n\n const setTheme = useCallback((next: Theme) => {\n setThemeState(next);\n applyDomClass(next);\n }, []);\n\n return { theme, setTheme };\n};\n","/* eslint-disable react-refresh/only-export-components */\n//? Framework-owned translation provider. Reads the registered locales map\n//? (consumer registers via `registerLocales({...})` in their overlay) and\n//? the active language source (consumer registers via\n//? `registerLanguageSource(fn)`, typically `() => session?.language`).\n//? Default language falls back to `getProjectConfig().defaultLanguage`.\n\nimport { createContext, ReactNode, use, useEffect, useMemo, useState, Dispatch, SetStateAction } from 'react';\nimport {\n getActiveLanguage,\n getDefaultLocale,\n getLocaleByCode,\n getRegisteredLocales,\n} from '../localesRegistry';\n\nexport type TranslationRecord = Record<string, unknown>;\n\ninterface TranslationContextValue {\n translations: TranslationRecord;\n setLanguage: Dispatch<SetStateAction<string>>;\n}\n\nconst TranslationContext = createContext<TranslationContextValue | null>(null);\n\nconst resolveTranslations = (code: string): TranslationRecord => {\n const value = getLocaleByCode(code);\n if (value && typeof value === 'object') return value as TranslationRecord;\n return {};\n};\n\nexport function TranslationProvider({ children }: { children: ReactNode }) {\n const [language, setLanguage] = useState<string>(() => getActiveLanguage());\n const translations = useMemo(() => resolveTranslations(language), [language]);\n\n //? Poll the registered language source so changes to session.language\n //? (or whatever the consumer wired) flip the active language without a\n //? full remount. Cheap: shallow string compare every ~250ms.\n useEffect(() => {\n const handle = window.setInterval(() => {\n const next = getActiveLanguage();\n if (next !== language) setLanguage(next);\n }, 250);\n return () => { window.clearInterval(handle); };\n }, [language]);\n\n const contextValue = useMemo(() => ({ translations, setLanguage }), [translations]);\n\n return (\n <TranslationContext value={contextValue}>\n {children}\n </TranslationContext>\n );\n}\n\nexport function useTranslation() {\n const context = use(TranslationContext);\n if (!context) throw new Error('useTranslation must be used within a TranslationProvider');\n return context.translations;\n}\n\nexport function useUpdateLanguage() {\n const context = use(TranslationContext);\n if (!context) throw new Error('useUpdateLanguage must be used within a TranslationProvider');\n return context.setLanguage;\n}\n\nexport interface TranslateParam { key: string; value: string | number | boolean }\n\n//? Resolve `a.b.c` style dot-notation keys in a translation tree. Returns\n//? the key itself if it can't be resolved (so untranslated strings stand\n//? out in the UI rather than vanishing). Supports `{{param}}` substitution.\nexport function translate({ translationList, key, params }: {\n translationList: TranslationRecord;\n key: unknown;\n params?: TranslateParam[];\n}): string {\n if (typeof key !== 'string' || key.length === 0) return '';\n\n const parts = key.split('.');\n let result: unknown = translationList;\n for (const part of parts) {\n if (result && typeof result === 'object' && part in result) {\n result = (result as Record<string, unknown>)[part];\n } else {\n return key;\n }\n }\n if (typeof result !== 'string') return key;\n if (!Array.isArray(params) || params.length === 0) return result;\n\n let finalResult = result;\n for (const param of params) {\n if (!param.key) continue;\n //? Escape key before building the RegExp — a key containing regex\n //? meta-characters (e.g. `.`, `+`, `(`) would otherwise mis-substitute\n //? or cause ReDoS. Use a function-form replacer to prevent `$`-sequences\n //? in the translation value from being interpreted by String.prototype.replace.\n const escapedKey = param.key.replaceAll(/[.*+?^${}()|[\\]\\\\]/g, String.raw`\\$&`);\n const regex = new RegExp(`\\\\{\\\\{${escapedKey}\\\\}\\\\}`, 'g');\n const replacement = String(param.value);\n finalResult = finalResult.replace(regex, () => replacement);\n }\n return finalResult;\n}\n\n//? Tiny convenience hook for the common case: pass `{ key, params }` and\n//? get the resolved string back.\nexport const useTranslator = () => {\n const translations = useTranslation();\n return ({ key, params }: { key: string; params?: TranslateParam[] }) =>\n translate({ translationList: translations, key, params });\n};\n\n//? Re-export helpers from the registry so consumers have a single import\n//? point for translation customization.\nexport { getActiveLanguage, getDefaultLocale, getRegisteredLocales };\n","//? Framework-owned i18n-backed toast notify. Reads the active language\n//? source + the registered locales map to translate `{ key, params }`\n//? before handing off to `sonner`. Registers itself with\n//? `registerNotifier` on import so framework packages (apiRequest,\n//? syncRequest) automatically emit toasts in the consumer's locale.\n//?\n//? Importing this module triggers the registration as a side effect —\n//? add `import { i18nNotify } from '@luckystack/core/client';` to the\n//? consumer's main entry once.\n\nimport { toast } from 'sonner';\nimport { registerNotifier } from '../notifier';\nimport { getActiveLanguage, getLocaleByCode } from '../localesRegistry';\nimport { translate, type TranslateParam } from './TranslationProvider';\n\ninterface NotifyInput {\n key: string;\n params?: TranslateParam[];\n}\n\nconst resolve = (input: NotifyInput): string => {\n const language = getActiveLanguage();\n const list = getLocaleByCode(language);\n return translate({\n translationList: (list && typeof list === 'object' ? list : {}) as Record<string, unknown>,\n key: input.key,\n params: input.params,\n });\n};\n\nconst notify = {\n success: (input: NotifyInput) => { toast.success(resolve(input)); },\n error: (input: NotifyInput) => { toast.error(resolve(input)); },\n info: (input: NotifyInput) => { toast.info(resolve(input)); },\n warning: (input: NotifyInput) => { toast.warning(resolve(input)); },\n};\n\n//? Side-effect: register with core so framework packages emit through\n//? this i18n-backed implementation.\nregisterNotifier(notify);\n\nexport default notify;\n","//? Framework-owned route guard. Wraps protected pages — runs the registered\r\n//? middleware handler (`registerMiddlewareHandler` in core) on every route\r\n//? change and renders children only when allowed. Redirects via React\r\n//? Router's `useNavigate` when the handler returns a redirect. Shows a\r\n//? loader only after a 200ms delay so fast checks don't flash a spinner.\r\n\r\nimport { ReactNode, useEffect, useRef, useState } from 'react';\r\nimport { useLocation, useNavigate } from 'react-router-dom';\r\nimport { getMiddlewareHandler, getPageMiddleware } from '../middlewareRegistry';\r\nimport { useSession } from './sessionContext';\r\nimport { getProjectConfig } from '../projectConfig';\r\n\r\nconst LOADER_DELAY_MS = 200;\r\n\r\nfunction MiddlewareLoader() {\r\n const [show, setShow] = useState(false);\r\n\r\n useEffect(() => {\r\n const id = globalThis.setTimeout(() => { setShow(true); }, LOADER_DELAY_MS);\r\n return () => { globalThis.clearTimeout(id); };\r\n }, []);\r\n\r\n if (!show) return null;\r\n\r\n return (\r\n <div className=\"w-full h-full flex items-center justify-center\" aria-busy=\"true\" aria-live=\"polite\">\r\n <div\r\n className=\"w-8 h-8 rounded-full border-2 border-container2-border border-t-primary animate-spin\"\r\n role=\"status\"\r\n aria-label=\"Loading\"\r\n />\r\n </div>\r\n );\r\n}\r\n\r\nexport default function Middleware({ children }: { children: ReactNode }) {\r\n const [allowed, setAllowed] = useState(false);\r\n const [checking, setChecking] = useState(true);\r\n\r\n const location = useLocation();\r\n const navigate = useNavigate();\r\n const { session, sessionLoaded } = useSession();\r\n\r\n //? Track the guarded route so a SESSION-only change re-evaluates the guard\r\n //? WITHOUT unmounting the page. A session update (avatar, roomCodes,\r\n //? preferences pushed via `updateSession`) used to flip `checking` back to\r\n //? true on every change → the whole page unmounted + remounted, wiping page\r\n //? state and aborting in-flight work. Joining a room (which saves the session\r\n //? and broadcasts `updateSession`) therefore looked like a full page refresh.\r\n //? Now we only show the loader / hide the page on a real ROUTE change; a bare\r\n //? session change re-checks in the background and only navigates away if the\r\n //? new session is no longer allowed.\r\n const routeKey = `${location.pathname}\u0000${location.search}`;\r\n const guardedRouteRef = useRef<string | null>(null);\r\n\r\n useEffect(() => {\r\n let isMounted = true;\r\n const isRouteChange = guardedRouteRef.current !== routeKey;\r\n guardedRouteRef.current = routeKey;\r\n if (isRouteChange) {\r\n setAllowed(false);\r\n setChecking(true);\r\n }\r\n\r\n void (async () => {\r\n const params = new URLSearchParams(location.search);\r\n const queryObject: Record<string, string> = {};\r\n for (const [key, value] of params.entries()) {\r\n queryObject[key] = value;\r\n }\r\n\r\n //? Wait for the SessionProvider to finish its initial fetch — up to\r\n //? 5 seconds. Without this, we'd evaluate the middleware against a\r\n //? not-yet-loaded session and bounce the user back to /login.\r\n let count = 0;\r\n while (!sessionLoaded) {\r\n await new Promise(res => setTimeout(res, 10));\r\n count++;\r\n if (count > 500) break;\r\n }\r\n\r\n //? Per-page middleware first (declared in each page.tsx via\r\n //? `export const middleware`), then the global fallback handler\r\n //? registered via `registerMiddlewareHandler`. This lets pages own\r\n //? their own guards while keeping a central catch-all for\r\n //? cross-cutting cases (server-reachability checks, analytics).\r\n const pageMw = getPageMiddleware(location.pathname);\r\n const handler = pageMw ?? getMiddlewareHandler();\r\n const result = await handler({\r\n location: location.pathname,\r\n searchParams: queryObject,\r\n session,\r\n });\r\n\r\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- isMounted intentional, see comment below\r\n if (!isMounted) return;\r\n\r\n if (result?.success) {\r\n setAllowed(true);\r\n } else if (result?.redirect) {\r\n void navigate(result.redirect);\r\n } else {\r\n //? Navigate to a safe, deterministic fallback instead of `navigate(-1)`,\r\n //? which chains backwards out of the app on repeated denials (e.g. when\r\n //? history is shallow or the previous page was also guarded).\r\n void navigate(getProjectConfig().loginRedirectUrl ?? '/');\r\n }\r\n\r\n setChecking(false);\r\n })();\r\n\r\n //! isMounted check is load-bearing — without it, repeated middleware\r\n //? denials chain navigate(-1) and walk too far up history. Keep it.\r\n return () => {\r\n isMounted = false;\r\n };\r\n }, [routeKey, location.pathname, location.search, navigate, session, sessionLoaded]);\r\n\r\n if (checking) return <MiddlewareLoader />;\r\n if (!allowed) return null;\r\n return <div className=\"w-full h-full\">{children}</div>;\r\n}\r\n","//? Wrapper around react-router-dom's `useNavigate` that runs the\n//? registered middleware handler before navigating. Returns the guarded\n//? navigate function. Use everywhere instead of `useNavigate` directly so\n//? programmatic navigations honor the same auth/redirect rules as\n//? `<Middleware>`-wrapped page renders.\n\nimport { useLocation, useNavigate } from 'react-router-dom';\nimport { getMiddlewareHandler, getPageMiddleware } from '../middlewareRegistry';\nimport { useSession } from './sessionContext';\n\nconst getParams = (locationSearch: string) => {\n const params = new URLSearchParams(locationSearch);\n const queryObject: Record<string, string> = {};\n for (const [key, value] of params.entries()) {\n queryObject[key] = value;\n }\n return queryObject;\n};\n\nexport default function useRouter() {\n const navigateHandler = useNavigate();\n const location = useLocation();\n const { session } = useSession();\n\n const navigate = async (path: string) => {\n const queryObject = getParams(location.search);\n //? Per-page middleware first (declared via `export const middleware`\n //? on the target page.tsx), then the global fallback. Mirrors the\n //? `<Middleware>` component's resolution order so programmatic\n //? navigations honor the same guards as direct URL hits.\n const pageMw = getPageMiddleware(path);\n const handler = pageMw ?? getMiddlewareHandler();\n const result = await handler({ location: path, searchParams: queryObject, session });\n\n if (result?.success) {\n return navigateHandler(path);\n }\n if (result && !result.success && result.redirect) {\n return navigateHandler(result.redirect);\n }\n return;\n };\n\n return navigate;\n}\n"],"mappings":"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","names":["resolve","effectiveTimeoutMs","createContext","use","useState","useCallback","useState","useCallback","createContext","use","useMemo","useState","jsx","createContext","useState","useMemo","use","notify","useEffect","useState","jsx","useState","useEffect","useLocation","useNavigate","useNavigate","useLocation"]}
|
package/dist/index.js
CHANGED
package/docs/app-bootstrap.md
CHANGED
|
@@ -84,7 +84,7 @@ Shape: `apiMethodMap[pagePath][apiName][version] = 'GET' | ...`.
|
|
|
84
84
|
|
|
85
85
|
### `registerApiMethodMap(map: ApiMethodMap): void`
|
|
86
86
|
|
|
87
|
-
Stores the generated method map
|
|
87
|
+
Stores the generated method map. The canonical consumer wrapper `src/_sockets/apiRequest.ts` imports `apiMethodMap` from `apiTypes.generated.ts`, registers it at module load, then re-exports the core request helper. This registration is required for routed HTTP because route names alone cannot reliably reveal their declared HTTP method.
|
|
88
88
|
|
|
89
89
|
### `getRegisteredApiMethod(pagePath, apiName, version): HttpMethodLiteral | undefined`
|
|
90
90
|
|
|
@@ -412,7 +412,6 @@ import {
|
|
|
412
412
|
registerLogger,
|
|
413
413
|
registerNotifier,
|
|
414
414
|
registerRuntimeMapsProvider,
|
|
415
|
-
registerApiMethodMap,
|
|
416
415
|
registerBindAddress,
|
|
417
416
|
} from '@luckystack/core';
|
|
418
417
|
import { PrismaClient } from '@prisma/client';
|
|
@@ -429,10 +428,19 @@ registerLogger({
|
|
|
429
428
|
});
|
|
430
429
|
registerNotifier(toastNotifier);
|
|
431
430
|
registerRuntimeMapsProvider(prodRuntimeMaps);
|
|
432
|
-
registerApiMethodMap(generatedApiMethodMap);
|
|
433
431
|
registerBindAddress({ ip: '0.0.0.0', port: 8080 });
|
|
434
432
|
```
|
|
435
433
|
|
|
434
|
+
The browser-side method map is registered separately by the project wrapper:
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
import { registerApiMethodMap } from '@luckystack/core/client';
|
|
438
|
+
import { apiMethodMap } from './apiTypes.generated';
|
|
439
|
+
|
|
440
|
+
registerApiMethodMap(apiMethodMap);
|
|
441
|
+
export { apiRequest } from '@luckystack/core/client';
|
|
442
|
+
```
|
|
443
|
+
|
|
436
444
|
## Related
|
|
437
445
|
|
|
438
446
|
- Function INDEX: `packages/core/CLAUDE.md`
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/apiMethodMapRegistry.ts","../src/httpApiUtils.ts","../src/socketState.ts","../src/notifier.ts","../src/tryCatchSync.ts","../src/clientHookBus.ts","../src/offlineQueue.ts","../src/responseNormalizer.ts","../src/serviceRoute.ts","../src/socketEvents.ts","../src/csrfConfig.ts","../src/tryCatch.ts","../src/csrf.ts","../src/sleep.ts","../src/pageRouteValidation.ts"],"sourcesContent":["//? Generated `apiMethodMap` registry. Replaces the prefix-heuristic\n//? in `isGetMethod` (apiRequest.ts) so abort-controller selection uses\n//? the actual HTTP method declared on the handler instead of guessing\n//? from the name prefix.\n//?\n//? Wiring: the project's `src/_sockets/socketInitializer.ts` (or any boot\n//? entry that imports `apiTypes.generated.ts`) calls\n//? `registerApiMethodMap(apiMethodMap)` once at module load. Until then,\n//? lookups return `undefined` and `isGetMethod` falls back to the\n//? prefix heuristic (preserving previous behavior for transitional code).\n\nexport type HttpMethodLiteral = 'GET' | 'POST' | 'PUT' | 'DELETE';\n\nexport type ApiMethodMap = Record<string, Record<string, Record<string, HttpMethodLiteral>>>;\n\nimport { createRegistry } from './createRegistry';\n\nconst registry = createRegistry<ApiMethodMap | null>(null);\n\nexport const registerApiMethodMap = (map: ApiMethodMap): void => {\n registry.register(map);\n};\n\nexport const getRegisteredApiMethod = (\n pagePath: string,\n apiName: string,\n version: string,\n): HttpMethodLiteral | undefined => {\n const activeMap = registry.get();\n if (!activeMap) return undefined;\n //? The map shape is `Record<page, Record<name, Record<version, method>>>`;\n //? TS treats each indexed access as defined (no `noUncheckedIndexedAccess`),\n //? so the optional chains look redundant. They're not — a missing key returns\n //? undefined at runtime, which is the contract this function is documenting.\n /* eslint-disable @typescript-eslint/no-unnecessary-condition -- runtime-defensive index access */\n return activeMap[pagePath]?.[apiName]?.[version];\n /* eslint-enable @typescript-eslint/no-unnecessary-condition */\n};\n\nexport const isApiMethodMapRegistered = (): boolean => registry.isRegistered();\n","/**\n * HTTP API Utilities\n * \n * Helpers for HTTP API request handling including:\n * - Smart HTTP method detection based on API name\n * - Method validation\n */\n\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';\n\n/**\n * Single source of truth for the GET-prefix naming heuristic\n * (`get` / `fetch` / `list`). Used by both `inferHttpMethod` here and the\n * client transport's `isGetMethod` fallback in `apiRequest.ts`, so the two\n * prefix heuristics can't drift apart. Expects an already-lowercased leaf name.\n */\nexport const isGetMethodName = (methodNameLower: string): boolean =>\n methodNameLower.startsWith('get') ||\n methodNameLower.startsWith('fetch') ||\n methodNameLower.startsWith('list');\n\n/**\n * Infer the HTTP method from an API name based on common naming conventions.\n * \n * - Names starting with \"get\" → GET\n * - Names starting with \"delete\" or \"remove\" → DELETE\n * - Names starting with \"update\" or \"edit\" → PUT\n * - Everything else → POST (create operations, actions)\n * \n * @example\n * ```typescript\n * inferHttpMethod('getUserData') // → 'GET'\n * inferHttpMethod('deleteAccount') // → 'DELETE'\n * inferHttpMethod('updateProfile') // → 'PUT'\n * inferHttpMethod('createUser') // → 'POST'\n * inferHttpMethod('sendEmail') // → 'POST'\n * ```\n */\nexport const inferHttpMethod = (apiName: string): HttpMethod => {\n // Extract the method name from the full path (e.g., \"api/examples/getUserData\" → \"getUserData\")\n const nameParts = apiName.split('/');\n const lastPart = nameParts.at(-1)?.toLowerCase() ?? '';\n const methodName = /^v\\d+$/.test(lastPart)\n ? (nameParts.at(-2) ?? '').toLowerCase()\n : lastPart;\n\n if (isGetMethodName(methodName)) {\n return 'GET';\n }\n\n if (methodName.startsWith('delete') || methodName.startsWith('remove')) {\n return 'DELETE';\n }\n\n if (methodName.startsWith('update') || methodName.startsWith('edit') || methodName.startsWith('patch')) {\n return 'PUT';\n }\n\n // Default: POST for create operations and general actions\n return 'POST';\n};\n\n/**\n * Get the effective HTTP method for an API.\n * Uses explicit export if available, otherwise infers from name.\n */\nexport const getEffectiveHttpMethod = (\n apiConfig: { httpMethod?: HttpMethod },\n apiName: string\n): HttpMethod => {\n return apiConfig.httpMethod ?? inferHttpMethod(apiName);\n};\n\n/**\n * Check if an HTTP method is valid for an API.\n *\n * @param requestMethod - The method from the HTTP request\n * @param allowedMethod - The method the API accepts\n * @returns true only when the method matches the route's allowed method.\n *\n * NOTE: `OPTIONS` is intentionally NOT treated as allowed here. A custom-route\n * author wiring this into their dispatch must answer CORS preflights BEFORE the\n * route check — letting `OPTIONS` through would execute the handler on a request\n * the CSRF middleware treats as non-state-changing (CSRF-exempt), opening a\n * bypass on method-locked routes.\n */\nexport const isMethodAllowed = (\n requestMethod: string,\n allowedMethod: HttpMethod\n): boolean => {\n return requestMethod === allowedMethod;\n};\n","import type { Socket } from 'socket.io-client';\n\n// Shared, mutable socket-client state. Single source of truth for the socket\n// instance, consumed by transport helpers in @luckystack/core (apiRequest)\n// and @luckystack/sync (syncRequest), and written by the project's React\n// hook wiring in `src/_sockets/socketInitializer.ts` via `setSocket(io(...))`.\n//\n// The `export let socket` form gives importers a live binding — they always\n// see the current value after `setSocket` replaces it.\n\n// eslint-disable-next-line import-x/no-mutable-exports\nexport let socket: Socket | null = null;\n\n//? One-shot waiters: promises that resolve the moment a non-null socket is\n//? set. Replacing the original `setInterval` poll (10 ms × 500 iterations)\n//? with a subscribe-and-notify pattern eliminates the busy-loop overhead and\n//? guarantees sub-tick delivery rather than up-to-10 ms latency.\nlet waiters: ((s: Socket) => void)[] = [];\n\n//? Absolute deadline for `waitForSocket` — prevents a permanently-dangling\n//? promise in environments where the socket is never created (SSR, tests).\nconst WAIT_FOR_SOCKET_TIMEOUT_MS = 5000;\n\nexport const setSocket = (next: Socket | null): void => {\n socket = next;\n //? Resolve all pending waiters the moment a real socket arrives.\n if (next) {\n const pending = waiters;\n waiters = [];\n for (const resolve of pending) {\n resolve(next);\n }\n }\n};\n\nlet responseIndex = 0;\n\nexport const incrementResponseIndex = (): number => {\n responseIndex = responseIndex + 1;\n return responseIndex;\n};\n\nexport const waitForSocket = (): Promise<Socket | null> => {\n if (socket) return Promise.resolve(socket);\n return new Promise<Socket | null>((resolve) => {\n const timer = setTimeout(() => {\n //? Time out: remove our waiter slot and resolve with null so the\n //? caller surfaces a \"socket unavailable\" path instead of hanging.\n waiters = waiters.filter((w) => w !== onSocket);\n resolve(null);\n }, WAIT_FOR_SOCKET_TIMEOUT_MS);\n\n const onSocket = (s: Socket): void => {\n clearTimeout(timer);\n resolve(s);\n };\n\n waiters.push(onSocket);\n });\n};\n","//? Client-side notifier DI. Framework packages (apiRequest, syncRequest,\n//? offline queue flush) emit toasts on transport errors. Default is silent\n//? (no-op) so tests and SSR contexts don't need a toaster installed.\n//?\n//? The project's entry point registers a notifier backed by its UI toolkit\n//? (sonner + i18n) via `registerNotifier({ error, success, ... })`.\n\nimport { createRegistry } from './createRegistry';\n\nexport interface NotifyParam {\n key: string;\n value: string | number | boolean;\n}\n\nexport interface NotifyInput {\n key: string;\n params?: NotifyParam[];\n}\n\nexport interface Notifier {\n success: (input: NotifyInput) => void;\n error: (input: NotifyInput) => void;\n info: (input: NotifyInput) => void;\n warning: (input: NotifyInput) => void;\n}\n\nconst noopNotifier: Notifier = {\n success: () => { /* no-op */ },\n error: () => { /* no-op */ },\n info: () => { /* no-op */ },\n warning: () => { /* no-op */ },\n};\n\nconst registry = createRegistry<Notifier>(noopNotifier);\n\nexport const registerNotifier = (notifier: Notifier): void => {\n registry.register(notifier);\n};\n\nexport const getNotifier = (): Notifier => registry.get();\n\n//? Delegating wrapper matching the project's existing `notify` shape so the\n//? switchover inside framework packages is a one-line import change.\nexport const notify: Notifier = {\n success: (input) => { registry.get().success(input); },\n error: (input) => { registry.get().error(input); },\n info: (input) => { registry.get().info(input); },\n warning: (input) => { registry.get().warning(input); },\n};\n","//? Synchronous sibling of `tryCatch`. The async `tryCatch` returns a Promise\n//? and auto-captures to the error tracker — neither of which fits a hot,\n//? genuinely-synchronous path (error-formatter dispatch, cookie/CSRF parse)\n//? that must stay synchronous and decides its own logging.\n//?\n//? Unlike the async `tryCatch`, this helper does NOT auto-capture to the\n//? error tracker: the call sites that need it are synchronous fallbacks that\n//? already do their own contextual logging and deliberately keep the error\n//? path side-effect-light. Capture explicitly on the error tuple when needed.\n\n//? `P = void` mirrors the async `tryCatch<T, P = void>` so a params-less call\n//? can pass just the result type: `tryCatchSync<URL>(() => new URL(raw))`.\n//? Without the default TS demands both type args (TS2558).\nexport default function tryCatchSync<T, P = void>(\n func: (values: P) => T,\n params?: P,\n): [Error | null, T | null] {\n try {\n const response = func(params as P);\n return [null, response];\n } catch (error) {\n return [error instanceof Error ? error : new Error(String(error)), null];\n }\n}\n","//? Client-side hook bus. Counterpart to the server-side `registerHook` /\n//? `dispatchHook` system in `hookRegistry.ts`, but lives here in the\n//? browser-safe surface and listens for client-side lifecycle moments:\n//? session login, session logout (and room for `sessionUpdate` later).\n//?\n//? Fired from `react/sessionContext.ts` when `setLatestSession` observes\n//? a null → non-null (postLogin) or non-null → null (postLogout) transition.\n//? That makes the fire-point framework-controlled — consumers don't have to\n//? add their own useEffect in `SessionProvider` to detect these moments.\n//?\n//? Handlers are best-effort: errors are caught + logged but don't break the\n//? session pipeline. Async handlers fire-and-forget; the caller of\n//? `setLatestSession` does not await.\n//?\n//? Typical use:\n//? import { registerClientHook } from '@luckystack/core/client';\n//?\n//? registerClientHook('postLogin', ({ session }) => {\n//? const letter = session.name?.[0]?.toLowerCase();\n//? if (letter) joinRoom(`letter-${letter}`);\n//? });\n//?\n//? Consumers with extended session shapes get narrow typing via the same\n//? `BaseSessionLayout` module augmentation pattern the rest of the framework\n//? uses (see ARCHITECTURE_EXTENSION_POINTS.md `@luckystack/login` section).\n\nimport type { BaseSessionLayout } from './sessionTypes';\nimport { getLogger } from './loggerRegistry';\n\nexport interface ClientHookPayloadMap {\n //? Fires BEFORE the framework commits a null → session transition.\n //? Handlers may return a `ClientHookStopSignal` to veto the login (e.g.\n //? account suspended since last visit, feature-flag gate, geo block).\n //? Only fires when the consumer commits the transition through the\n //? vetoable entry point — see `proposeLogin` in `react/sessionContext.ts`.\n preLogin: { candidateSession: BaseSessionLayout };\n postLogin: { session: BaseSessionLayout };\n postLogout: { previousSession: BaseSessionLayout };\n //? Fires when the offline queue drops a request (full queue with a\n //? `drop-oldest`/`drop-newest`/`reject` policy, or an item past `maxAgeMs`).\n //? The in-memory queue is not durable across refreshes; this is the seam to\n //? surface a \"your change wasn't sent\" toast or to persist the item\n //? elsewhere. `queue` says which queue, `reason` why it was dropped.\n queueItemDropped: {\n queue: 'api' | 'sync';\n key: string;\n reason: 'queue-full' | 'expired';\n dropPolicy: 'drop-oldest' | 'drop-newest' | 'reject';\n };\n}\n\nexport type ClientHookName = keyof ClientHookPayloadMap;\n\n//? Stop signal returned by a `pre*` client hook handler to veto the\n//? framework's main flow. Mirrors the server-side `HookStopSignal` shape so\n//? `errorCode` flows through the same i18n / responseNormalizer pipeline a\n//? consumer already wires up.\nexport interface ClientHookStopSignal {\n stop: true;\n errorCode: string;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- mirrors server-side HookResult, see hooks/types.ts\nexport type ClientHookResult = void | undefined | ClientHookStopSignal;\n\nexport type ClientDispatchResult =\n | { stopped: false }\n | { stopped: true; signal: ClientHookStopSignal };\n\nexport type ClientHookHandler<N extends ClientHookName> = (\n payload: ClientHookPayloadMap[N],\n) => ClientHookResult | Promise<ClientHookResult>;\n\n//? Storage uses `unknown` handler sets so the generic `N` doesn't have to\n//? narrow at every access — the cast happens once at read/write inside the\n//? typed wrappers below, which is sound because each hook name's Set only\n//? ever sees handlers registered under that same name.\ntype AnyHandler = (payload: unknown) => ClientHookResult | Promise<ClientHookResult>;\nconst handlerSets: Partial<Record<ClientHookName, Set<AnyHandler>>> = {};\n\n/**\n * Subscribe to a client-side lifecycle hook. Returns an unsubscribe function\n * — call it to remove the handler (e.g. inside a React effect cleanup).\n *\n * Multiple handlers can be registered for the same hook; they all fire on\n * each dispatch. A handler that throws (sync) or rejects (async) is logged\n * to `console.error` and does not affect other handlers.\n */\nexport function registerClientHook<N extends ClientHookName>(\n name: N,\n handler: ClientHookHandler<N>,\n): () => void {\n const existing = handlerSets[name];\n const set = existing ?? new Set<AnyHandler>();\n if (!existing) handlerSets[name] = set;\n const wrapped = handler as AnyHandler;\n set.add(wrapped);\n return () => {\n set.delete(wrapped);\n };\n}\n\n/**\n * Fire a client-side hook (fire-and-forget). Framework-internal — consumers\n * should not call this directly. Used for `post*` hooks where any stop-signal\n * returned by a handler is intentionally ignored: a `postLogin` listener that\n * tried to \"stop\" the framework would be too late — the transition already\n * happened. Use `dispatchVetoableClientHook` for `pre*` hooks where the\n * caller actually awaits the veto decision.\n */\nexport function dispatchClientHook<N extends ClientHookName>(\n name: N,\n payload: ClientHookPayloadMap[N],\n): void {\n const set = handlerSets[name];\n if (!set || set.size === 0) return;\n //? Snapshot the set before iterating so a handler that unregisters itself\n //? (or a sibling) mid-dispatch doesn't skip un-visited handlers — Set\n //? iteration honors mid-iter deletes per ECMAScript spec, which we don't\n //? want here.\n const handlers = [...set];\n for (const handler of handlers) {\n try {\n const result = handler(payload);\n if (result && typeof (result as Promise<unknown>).catch === 'function') {\n (result as Promise<unknown>).catch((error: unknown) => {\n getLogger().error(`[clientHook:${name}] async handler rejected`, error);\n });\n }\n } catch (error) {\n getLogger().error(`[clientHook:${name}] handler threw`, error);\n }\n }\n}\n\n/**\n * Awaits every registered handler for a `pre*` client hook. If any handler\n * returns a `ClientHookStopSignal`, dispatch short-circuits and returns\n * `{ stopped: true, signal }`; callers must respect the veto (skip the\n * pending action). Handlers that throw or reject are logged but treated as\n * `undefined` — a buggy handler must not silently block the user flow.\n *\n * Mirrors `dispatchHook` from `@luckystack/core` so consumer/framework code\n * can use the same stop-signal pattern on both sides of the wire.\n */\nexport async function dispatchVetoableClientHook<N extends ClientHookName>(\n name: N,\n payload: ClientHookPayloadMap[N],\n): Promise<ClientDispatchResult> {\n const set = handlerSets[name];\n if (!set || set.size === 0) return { stopped: false };\n //? Snapshot the set (see `dispatchClientHook` for why).\n const handlers = [...set];\n for (const handler of handlers) {\n let result: ClientHookResult;\n try {\n result = await handler(payload);\n } catch (error) {\n getLogger().error(`[clientHook:${name}] vetoable handler threw`, error);\n continue;\n }\n if (result?.stop) {\n return { stopped: true, signal: result };\n }\n }\n return { stopped: false };\n}\n\n/** Test helper — drop every registered handler. Not part of the public API. */\nexport function _resetClientHooksForTests(): void {\n for (const key of Object.keys(handlerSets) as ClientHookName[]) {\n handlerSets[key]?.clear();\n }\n}\n","import { Socket } from \"socket.io-client\";\nimport { getProjectConfig } from \"./projectConfig\";\nimport { getLogger } from \"./loggerRegistry\";\nimport tryCatchSync from \"./tryCatchSync\";\nimport { dispatchClientHook } from \"./clientHookBus\";\n\n/** Why a queued item was dropped (handed to its `onDrop` callback). */\nexport type QueueDropReason = 'expired' | 'queue-full';\n\ninterface ApiQueueItem {\n id: string;\n key: string;\n run: (socketInstance: Socket) => void;\n createdAt: number;\n /**\n * Per-request override of `projectConfig.offlineQueue.dropPolicy`. See\n * `SyncQueueItem.dropPolicy` for details.\n */\n dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';\n /**\n * Optional callback invoked when THIS item is dropped (evicted for age or\n * because the queue was full / rejected) instead of ever running (SYNC-09).\n * The transport wires this to settle the pending `syncRequest`/`apiRequest`\n * promise with an error envelope, so an evicted offline request resolves\n * rather than hanging forever. Invoked at most once; failures are swallowed.\n */\n onDrop?: (reason: QueueDropReason) => void;\n}\n\ninterface SyncQueueItem {\n id: string;\n key: string;\n run: (socketInstance: Socket) => void;\n createdAt: number;\n /**\n * Per-request override of `projectConfig.offlineQueue.dropPolicy`. Lets a\n * specific sync (\"editor cursor move\") pick `'drop-oldest'` while the app\n * default stays `'reject'` for safer sends. When omitted, falls back to\n * the global config.\n */\n dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';\n /** See {@link ApiQueueItem.onDrop} (SYNC-09). */\n onDrop?: (reason: QueueDropReason) => void;\n}\n\nconst apiQueue: ApiQueueItem[] = [];\nconst syncQueue: SyncQueueItem[] = [];\n\n//? Independent flush guards so a long api flush doesn't no-op a concurrent\n//? sync flush (or vice versa).\nlet isFlushingApi = false;\nlet isFlushingSync = false;\n\nexport const isOnline = () => {\n if (typeof navigator === \"undefined\") return true;\n return navigator.onLine;\n};\n\n//? Common drop shape — every queue item the framework drops carries these.\ninterface DroppableItem {\n createdAt: number;\n key: string;\n dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';\n onDrop?: (reason: QueueDropReason) => void;\n}\n\n//? Notify a dropped item's owner exactly once. Swallows callback errors so a\n//? throwing `onDrop` can't wedge the eviction/enqueue loop (SYNC-09).\nconst notifyDropped = (item: DroppableItem, reason: QueueDropReason): void => {\n if (!item.onDrop) return;\n tryCatchSync(() => { item.onDrop?.(reason); });\n};\n\n//? Dispatch the `queueItemDropped` client hook, guarded so a misbehaving\n//? handler set can't interrupt the eviction/enqueue loop (CORE-N8).\nconst safeDispatchDropHook = (\n queue: 'api' | 'sync',\n key: string,\n reason: QueueDropReason,\n dropPolicy: 'drop-oldest' | 'drop-newest' | 'reject',\n): void => {\n tryCatchSync(() => {\n dispatchClientHook('queueItemDropped', { queue, key, reason, dropPolicy });\n });\n};\n\nconst evictExpired = (\n queue: DroppableItem[],\n label: 'api' | 'sync',\n): void => {\n const { maxAgeMs, dropPolicy: globalPolicy } = getProjectConfig().offlineQueue;\n if (maxAgeMs <= 0) return;\n const cutoff = Date.now() - maxAgeMs;\n for (let i = queue.length - 1; i >= 0; i -= 1) {\n const item = queue[i];\n if (item && item.createdAt < cutoff) {\n queue.splice(i, 1);\n notifyDropped(item, 'expired');\n safeDispatchDropHook(label, item.key, 'expired', item.dropPolicy ?? globalPolicy);\n }\n }\n};\n\nconst enqueueWithPolicy = <T extends DroppableItem>(\n queue: T[],\n item: T,\n label: 'api' | 'sync',\n): boolean => {\n evictExpired(queue, label);\n const { maxSize, dropPolicy: globalPolicy } = getProjectConfig().offlineQueue;\n //? Per-request override wins over the global policy. Lets specific\n //? syncs pick 'drop-oldest' for liveness while the app default stays\n //? 'reject' for safer sends.\n const effectivePolicy = item.dropPolicy ?? globalPolicy;\n if (queue.length < maxSize) {\n queue.push(item);\n return true;\n }\n if (effectivePolicy === 'drop-oldest') {\n const dropped = queue.shift();\n queue.push(item);\n getLogger().debug(`offlineQueue:${label} full — dropped oldest item (policy=${effectivePolicy})`);\n if (dropped) {\n notifyDropped(dropped, 'queue-full');\n safeDispatchDropHook(label, dropped.key, 'queue-full', effectivePolicy);\n }\n return true;\n }\n if (effectivePolicy === 'drop-newest') {\n getLogger().debug(`offlineQueue:${label} full — dropped newest item (policy=${effectivePolicy})`);\n notifyDropped(item, 'queue-full');\n safeDispatchDropHook(label, item.key, 'queue-full', effectivePolicy);\n return false;\n }\n getLogger().warn(`offlineQueue:${label} full — rejecting enqueue (policy=${effectivePolicy})`);\n notifyDropped(item, 'queue-full');\n safeDispatchDropHook(label, item.key, 'queue-full', effectivePolicy);\n return false;\n};\n\nexport const enqueueApiRequest = (item: ApiQueueItem): boolean =>\n enqueueWithPolicy(apiQueue, item, 'api');\n\nexport const enqueueSyncRequest = (item: SyncQueueItem): boolean =>\n enqueueWithPolicy(syncQueue, item, 'sync');\n\nexport const removeApiQueueItem = (id: string) => {\n const index = apiQueue.findIndex((item) => item.id === id);\n if (index !== -1) {\n apiQueue.splice(index, 1);\n }\n};\n\nexport const removeSyncQueueItem = (id: string) => {\n const index = syncQueue.findIndex((item) => item.id === id);\n if (index !== -1) {\n syncQueue.splice(index, 1);\n }\n};\n\nexport const removeApiQueueItemsByKey = (key: string) => {\n for (let i = apiQueue.length - 1; i >= 0; i -= 1) {\n if (apiQueue[i]?.key === key) {\n apiQueue.splice(i, 1);\n }\n }\n};\n\n//? Run one dequeued item. If `run` throws, requeue it at the FRONT (so the\n//? next flush retries it in order) and signal the caller to stop this flush —\n//? otherwise a throwing item is silently lost AND the `isFlushing*` guard\n//? would be left stuck `true`, wedging every future flush.\nconst runQueueItem = <T extends { run: (s: Socket) => void }>(\n queue: T[],\n item: T,\n socketInstance: Socket,\n label: string,\n): boolean => {\n const [error] = tryCatchSync(() => { item.run(socketInstance); });\n if (error) {\n queue.unshift(item);\n getLogger().warn(`offlineQueue:${label} run threw — requeued item, pausing flush`, { error });\n return false;\n }\n return true;\n};\n\nexport const flushApiQueue = (canRun: () => boolean, socketInstance: Socket) => {\n if (isFlushingApi) return;\n evictExpired(apiQueue, 'api');\n if (apiQueue.length === 0) return;\n if (!canRun()) return;\n\n isFlushingApi = true;\n while (apiQueue.length > 0) {\n if (!canRun()) break;\n const item = apiQueue.shift();\n if (item && !runQueueItem(apiQueue, item, socketInstance, 'api')) break;\n }\n isFlushingApi = false;\n};\n\nexport const flushSyncQueue = (canRun: () => boolean, socketInstance: Socket) => {\n if (isFlushingSync) return;\n evictExpired(syncQueue, 'sync');\n if (syncQueue.length === 0) return;\n if (!canRun()) return;\n\n isFlushingSync = true;\n while (syncQueue.length > 0) {\n if (!canRun()) break;\n const item = syncQueue.shift();\n if (item && !runQueueItem(syncQueue, item, socketInstance, 'sync')) break;\n }\n isFlushingSync = false;\n};\n\nexport const getApiQueueSize = () => apiQueue.length;\nexport const getSyncQueueSize = () => syncQueue.length;\n","export interface ErrorParam {\n key: string;\n value: string | number | boolean;\n}\n\nexport interface ErrorResponseInput {\n status?: unknown;\n errorCode?: unknown;\n errorParams?: unknown;\n httpStatus?: unknown;\n}\n\nexport interface NormalizedErrorResponse {\n status: 'error';\n message: string;\n errorCode: string;\n errorParams?: ErrorParam[];\n httpStatus?: number;\n}\n\nexport const INVALID_ERROR_RESPONSE_CODE = 'error.invalidResponse';\n\nexport const isErrorParamArray = (value: unknown): value is ErrorParam[] => {\n return Array.isArray(value) && value.every((item) => {\n return (\n !!item\n && typeof item === 'object'\n && typeof (item as ErrorParam).key === 'string'\n && ['string', 'number', 'boolean'].includes(typeof (item as ErrorParam).value)\n );\n });\n};\n\nexport const normalizeErrorResponseCore = ({\n response,\n fallbackHttpStatus,\n fallbackErrorCode,\n resolveMessage,\n}: {\n response: ErrorResponseInput;\n fallbackHttpStatus?: number;\n fallbackErrorCode?: string;\n resolveMessage?: (args: { errorCode: string; errorParams?: ErrorParam[] }) => string;\n}): NormalizedErrorResponse => {\n const normalizedParams = isErrorParamArray(response.errorParams) ? response.errorParams : undefined;\n const errorCodeValue = typeof response.errorCode === 'string' ? response.errorCode.trim() : '';\n const hasErrorCode = errorCodeValue.length > 0;\n const finalErrorCode = hasErrorCode ? errorCodeValue : (fallbackErrorCode ?? INVALID_ERROR_RESPONSE_CODE);\n const providedHttpStatus = typeof response.httpStatus === 'number' ? response.httpStatus : undefined;\n\n const message = resolveMessage\n ? resolveMessage({ errorCode: finalErrorCode, errorParams: normalizedParams })\n : finalErrorCode;\n\n return {\n status: 'error',\n message,\n errorCode: finalErrorCode,\n errorParams: normalizedParams,\n httpStatus: providedHttpStatus ?? fallbackHttpStatus,\n };\n};\n\nexport const defaultHttpStatusForResponse = ({\n status,\n explicitHttpStatus,\n fallbackErrorStatus = 400,\n}: {\n status: 'success' | 'error';\n explicitHttpStatus?: number;\n fallbackErrorStatus?: number;\n}): number => {\n if (typeof explicitHttpStatus === 'number') return explicitHttpStatus;\n return status === 'success' ? 200 : fallbackErrorStatus;\n};\n","const VERSION_SEGMENT_REGEX = /^v\\d+$/;\n\nexport interface ServiceRouteParseSuccess {\n status: 'success';\n normalizedRouteName: string;\n service: string;\n routeName: string;\n}\n\nexport interface ServiceRouteParseError {\n status: 'error';\n reason: string;\n}\n\nexport type ServiceRouteParseResult = ServiceRouteParseSuccess | ServiceRouteParseError;\n\nexport interface TransportRouteParseSuccess {\n status: 'success';\n normalizedFullName: string;\n version: string;\n serviceRoute: ServiceRouteParseSuccess;\n}\n\nexport interface TransportRouteParseError {\n status: 'error';\n reason: string;\n}\n\nexport type TransportRouteParseResult = TransportRouteParseSuccess | TransportRouteParseError;\n\nconst normalizeRawRoute = (value: string): string => {\n return value.trim().replaceAll('\\\\', '/').replaceAll(/^\\/+/g, '').replaceAll(/\\/+$/g, '');\n};\n\nconst hasEmptySegments = (value: string): boolean => {\n return value.split('/').some((segment) => segment.length === 0);\n};\n\nexport const parseServiceRouteName = (value: string): ServiceRouteParseResult => {\n const normalized = normalizeRawRoute(value);\n if (!normalized) {\n return { status: 'error', reason: 'Route name cannot be empty.' };\n }\n\n if (hasEmptySegments(normalized)) {\n return { status: 'error', reason: 'Route name cannot contain empty segments.' };\n }\n\n const segments = normalized.split('/');\n if (segments.length < 2) {\n return { status: 'error', reason: 'Route name must include service and route segments.' };\n }\n\n const [service, ...routeSegments] = segments;\n const routeName = routeSegments.join('/');\n\n if (!service || !routeName) {\n return { status: 'error', reason: 'Route name must include service and route segments.' };\n }\n\n return {\n status: 'success',\n normalizedRouteName: `${service}/${routeName}`,\n service,\n routeName,\n };\n};\n\nexport const parseTransportRouteName = ({\n value,\n prefix,\n}: {\n value: string;\n prefix: 'api' | 'sync';\n}): TransportRouteParseResult => {\n const normalized = normalizeRawRoute(value);\n if (!normalized) {\n return { status: 'error', reason: 'Transport route cannot be empty.' };\n }\n\n if (hasEmptySegments(normalized)) {\n return { status: 'error', reason: 'Transport route cannot contain empty segments.' };\n }\n\n const segments = normalized.split('/');\n const routeSegments = segments[0] === prefix ? segments.slice(1) : segments;\n\n if (routeSegments.length < 3) {\n return {\n status: 'error',\n reason: `Transport route must use ${prefix}/{service}/{name}/{version} format.`,\n };\n }\n\n const version = routeSegments.at(-1);\n if (!version || !VERSION_SEGMENT_REGEX.test(version)) {\n return { status: 'error', reason: 'Route version segment must match v{number}.' };\n }\n\n const serviceRouteToken = routeSegments.slice(0, -1).join('/');\n const serviceRoute = parseServiceRouteName(serviceRouteToken);\n if (serviceRoute.status === 'error') {\n return {\n status: 'error',\n reason: serviceRoute.reason,\n };\n }\n\n return {\n status: 'success',\n normalizedFullName: `${prefix}/${serviceRoute.normalizedRouteName}/${version}`,\n version,\n serviceRoute,\n };\n};\n","const withIndex = (prefix: string, responseIndex: number | string): string => {\n return `${prefix}${String(responseIndex)}`;\n};\n\nexport const socketEventNames = {\n apiRequest: 'apiRequest',\n apiCancel: 'apiCancel',\n sync: 'sync',\n syncCancel: 'syncCancel',\n joinRoom: 'joinRoom',\n leaveRoom: 'leaveRoom',\n getJoinedRooms: 'getJoinedRooms',\n updateLocation: 'updateLocation',\n updateSession: 'updateSession',\n sessionReplaced: 'sessionReplaced',\n logout: 'logout',\n\n intentionalDisconnect: 'intentionalDisconnect',\n intentionalReconnect: 'intentionalReconnect',\n //? Client -> server activity heartbeat (mouse/keyboard/touch), throttled.\n //? Drives the server-side activity sampler that fires registered activity\n //? events (e.g. the built-in AFK detector).\n activity: 'activity',\n userAfk: 'userAfk',\n userBack: 'userBack',\n //? Emitted to a room's remaining members when a peer leaves — hard disconnect\n //? (socket close) or grace-expiry (presence MIS-003). Lets clients prune a\n //? departed peer from their roster without polling. The presence package owns\n //? the dispatch; core owns the wire-event name so client + server can't drift.\n userLeft: 'userLeft',\n\n connect: 'connect',\n disconnect: 'disconnect',\n reconnectAttempt: 'reconnect_attempt',\n connectError: 'connect_error',\n\n apiResponsePrefix: 'apiResponse-',\n apiStreamPrefix: 'apiStream-',\n syncResponsePrefix: 'sync-',\n syncProgressPrefix: 'sync-progress-',\n joinRoomResponsePrefix: 'joinRoom-',\n leaveRoomResponsePrefix: 'leaveRoom-',\n getJoinedRoomsResponsePrefix: 'getJoinedRooms-',\n} as const;\n\nexport const buildApiResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.apiResponsePrefix, responseIndex);\n};\n\nexport const buildApiStreamEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.apiStreamPrefix, responseIndex);\n};\n\nexport const buildSyncResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.syncResponsePrefix, responseIndex);\n};\n\nexport const buildSyncProgressEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.syncProgressPrefix, responseIndex);\n};\n\nexport const buildJoinRoomResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.joinRoomResponsePrefix, responseIndex);\n};\n\nexport const buildLeaveRoomResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.leaveRoomResponsePrefix, responseIndex);\n};\n\nexport const buildGetJoinedRoomsResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.getJoinedRoomsResponsePrefix, responseIndex);\n};\n","//? CSRF configuration registry. The framework's CSRF middleware\n//? (`packages/server/src/httpRoutes/csrfMiddleware.ts`) and the\n//? client-side fetch wrapper (`packages/core/src/csrf.ts`) both consult\n//? `getCsrfConfig()` so consumers can change the header name, cookie\n//? name, token length, or cookie options without forking.\n//?\n//? The default values match the framework's pre-registry hardcodes\n//? (header `x-csrf-token`, 32-byte hex token minted at session create) so\n//? consumers who never call `registerCsrfConfig` get the historical\n//? behaviour unchanged.\n\nimport { createRegistry } from './createRegistry';\n\nexport interface CsrfCookieOptions {\n sameSite?: 'strict' | 'lax' | 'none';\n secure?: boolean;\n /**\n * MUST be false for CSRF cookies — the client needs to read the value\n * with JS to attach it as a request header. The framework reads the\n * value from the session record (Redis-side) rather than the cookie,\n * so this flag is documentation/intent only.\n */\n httpOnly?: boolean;\n path?: string;\n maxAgeMs?: number;\n}\n\nexport interface CsrfConfig {\n /** Cookie name used to deliver the CSRF token to the browser. */\n cookieName: string;\n /** Request-header name the client attaches on state-changing requests. */\n headerName: string;\n /** Length in bytes of the random token minted by the session layer. */\n tokenLength: number;\n cookieOptions: CsrfCookieOptions;\n}\n\nexport const DEFAULT_CSRF_CONFIG: CsrfConfig = {\n cookieName: 'csrf-token',\n headerName: 'x-csrf-token',\n tokenLength: 32,\n cookieOptions: {\n sameSite: 'lax',\n //? `secure` intentionally UNSET so it resolves per-environment at serialize\n //? time (csrfRoute.ts) the SAME way the session cookie does — emit `Secure` in\n //? prod (SECURE=true) but NOT over plain HTTP in dev. A hardcoded `true` made\n //? the browser drop the login-absent double-submit cookie over http://, so\n //? every state-changing request 403'd. Force it on explicitly via\n //? `registerCsrfConfig({ cookieOptions: { secure: true } })` if desired.\n httpOnly: false,\n path: '/',\n maxAgeMs: 24 * 60 * 60 * 1000,\n },\n};\n\nconst registry = createRegistry<CsrfConfig, Partial<CsrfConfig>>(DEFAULT_CSRF_CONFIG, {\n //? Merge shallowly with the CURRENT config; `cookieOptions` is deep-merged\n //? so a partial override of (say) `sameSite` does not clobber `path` /\n //? `maxAgeMs`. Last-write-wins, and successive calls accumulate.\n transform: (input, current) => ({\n ...current,\n ...input,\n cookieOptions: { ...current.cookieOptions, ...input.cookieOptions },\n }),\n});\n\n/**\n * Override one or more CSRF settings. Merges shallowly with the current\n * config; `cookieOptions` is deep-merged so a partial override of (say)\n * `sameSite` does not clobber `path` / `maxAgeMs`. Last-write-wins.\n */\nexport const registerCsrfConfig = (input: Partial<CsrfConfig>): void => {\n registry.register(input);\n};\n\n/** Read the active config at call time (never at module load). */\nexport const getCsrfConfig = (): CsrfConfig => registry.get();\n\n/** Test-only — restore defaults between scenarios. */\nexport const resetCsrfConfigForTests = (): void => {\n registry.reset();\n};\n","import { captureException } from './sentrySetup';\n\nexport default async function tryCatch<T, P = void>(\n func: (values: P) => Promise<T> | T,\n params?: P,\n context?: Record<string, unknown>\n): Promise<[Error | null, T | null]> {\n try {\n const response = await func(params as P);\n return [null, response];\n } catch (error) {\n captureException(error, context);\n return [error instanceof Error ? error : new Error(String(error)), null];\n }\n}\n","//? Client-side CSRF helper. Lazily fetches and caches the session's CSRF\n//? token from `/auth/csrf`, then attaches it as `x-csrf-token` on\n//? subsequent HTTP fetches in cookie mode.\n//?\n//? Token-mode sessions are CSRF-immune because cross-origin POSTs do not\n//? auto-attach the sessionStorage value; the cache simply stays empty in\n//? that mode.\n\nimport { getCsrfConfig } from './csrfConfig';\nimport { getProjectConfig } from './projectConfig';\nimport { socket } from './socketState';\nimport tryCatch from './tryCatch';\nimport tryCatchSync from './tryCatchSync';\n\n//? Tokens are scoped to the HTTP invocation origin. In routed-http mode the\n//? Socket.io connection may terminate on a remote `system` service while the\n//? invocation goes same-origin through a local router; a single socket-derived\n//? cache would fetch the CSRF token from the wrong ingress.\nconst cachedTokens = new Map<string, string>();\nconst inflightFetches = new Map<string, Promise<string | null>>();\n\nconst resolveSocketOrigin = (): string => socket?.io.opts.hostname\n ? `${socket.io.opts.secure ? 'https' : 'http'}://${socket.io.opts.hostname}${socket.io.opts.port ? `:${socket.io.opts.port}` : ''}`\n : '';\n\nconst resolveRequestOrigin = (input?: RequestInfo | URL): string => {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- SSR runtime guard\n const browserOrigin = globalThis.window !== undefined && globalThis.location?.origin\n ? globalThis.location.origin\n : '';\n if (input !== undefined) {\n let raw: string;\n if (typeof input === 'string') raw = input;\n else if (input instanceof URL) raw = input.href;\n else raw = input.url;\n const fallbackOrigin = browserOrigin.length > 0 ? browserOrigin : resolveSocketOrigin();\n const [error, parsed] = tryCatchSync(() => new URL(raw, fallbackOrigin));\n if (!error && parsed) return parsed.origin;\n }\n return browserOrigin.length > 0 ? browserOrigin : resolveSocketOrigin();\n};\n\nconst fetchCsrfToken = async (origin: string): Promise<string | null> => {\n const base = origin;\n const [error, token] = await tryCatch<string | null, undefined>(async () => {\n const response = await fetch(`${base}/auth/csrf`, {\n method: 'GET',\n credentials: 'include',\n });\n if (!response.ok) return null;\n const body = (await response.json()) as { csrfToken?: string | null };\n return body.csrfToken ?? null;\n });\n if (error) return null;\n return token ?? null;\n};\n\n/**\n * Fetch (or return the cached) CSRF token for the active session. Returns\n * null in token mode or when no session exists. Callers should attach the\n * returned value as the `X-CSRF-Token` request header.\n */\nexport const getCsrfToken = async (origin = resolveRequestOrigin()): Promise<string | null> => {\n //? Token-mode sessions: skip entirely. The server doesn't enforce CSRF\n //? in token mode, and there's no value to attach.\n if (getProjectConfig().session.basedToken) {\n return null;\n }\n\n const cached = cachedTokens.get(origin);\n if (cached) return cached;\n const inflight = inflightFetches.get(origin);\n if (inflight) return inflight;\n\n const next = fetchCsrfToken(origin).then((token) => {\n if (token) cachedTokens.set(origin, token);\n inflightFetches.delete(origin);\n return token;\n });\n inflightFetches.set(origin, next);\n return next;\n};\n\n/** Drop the cached token. Call this on logout or when a 403 csrfMismatch is seen. */\nexport const clearCsrfToken = (): void => {\n cachedTokens.clear();\n inflightFetches.clear();\n};\n\nconst STATE_CHANGING = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);\n\nconst isCsrfMismatchResponse = async (response: Response): Promise<boolean> => {\n if (response.status !== 403) return false;\n const [error, isMismatch] = await tryCatch<boolean, undefined>(async () => {\n //? `response.clone()` is used here so the ORIGINAL response body stream is\n //? preserved for the caller (the clone is consumed, the original is not).\n //? This is important: `httpFetch` returns the original `response` reference\n //? to the caller so it can read its body, and cloning before reading is the\n //? correct way to peek at the body without consuming it.\n const cloned = response.clone();\n const body = (await cloned.json()) as { errorCode?: string };\n return body.errorCode === 'auth.csrfMismatch';\n });\n if (error) return false;\n return isMismatch ?? false;\n};\n\n/**\n * Drop-in replacement for `fetch` that automatically attaches the framework's\n * `X-CSRF-Token` and `X-Request-Id` headers to state-changing requests in\n * cookie mode. Use this for any project-side HTTP call that hits a\n * LuckyStack `/api/*`, `/sync/*`, or `/auth/api/*` endpoint.\n *\n * Behavior:\n * - GET / OPTIONS / HEAD: passes through without CSRF (read-only).\n * - Token mode: passes through without CSRF (immune by design).\n * - Cookie mode + state-changing method: lazily fetches `/auth/csrf` once,\n * caches, attaches as `x-csrf-token`. On a 403 `auth.csrfMismatch`\n * response, clears the cache and retries once.\n * - `credentials: 'include'` is forced unless the caller overrode it.\n *\n * @example\n * import { httpFetch } from '@luckystack/core/client';\n * const res = await httpFetch('/api/system/myRoute/v1', {\n * method: 'POST',\n * body: JSON.stringify({ ... }),\n * });\n */\nexport const httpFetch: typeof fetch = async (input, init = {}) => {\n const method = (init.method ?? 'GET').toUpperCase();\n const headers = new Headers(init.headers);\n const config = getProjectConfig();\n if (config.session.basedToken && !headers.has('Authorization')) {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- SSR runtime guard\n const token = globalThis.window === undefined\n ? null\n : tryCatchSync(() => globalThis.sessionStorage.getItem('token'))[1];\n if (token) headers.set('Authorization', `Bearer ${token}`);\n }\n if (!headers.has('Content-Type') && init.body && typeof init.body === 'string') {\n headers.set('Content-Type', 'application/json');\n }\n\n const csrfHeaderName = getCsrfConfig().headerName;\n const send = async (csrfToken: string | null): Promise<Response> => {\n const finalHeaders = new Headers(headers);\n if (csrfToken && STATE_CHANGING.has(method) && !finalHeaders.has(csrfHeaderName)) {\n finalHeaders.set(csrfHeaderName, csrfToken);\n }\n return fetch(input, {\n ...init,\n method,\n headers: finalHeaders,\n credentials: init.credentials ?? 'include',\n });\n };\n\n if (!STATE_CHANGING.has(method) || config.session.basedToken) {\n return send(null);\n }\n\n const requestOrigin = resolveRequestOrigin(input);\n const token = await getCsrfToken(requestOrigin);\n let response = await send(token);\n\n //? On csrfMismatch, the cached token is stale (session rotated). Clear and\n //? retry ONCE with a fresh fetch — covers the case where the user logged\n //? back in while a tab was idle. The retry is intentionally limited to one\n //? attempt: a second csrfMismatch means the session is broken in a way a\n //? fresh token won't fix (e.g. session expired), and retrying would loop.\n if (await isCsrfMismatchResponse(response)) {\n clearCsrfToken();\n const refreshed = await getCsrfToken(requestOrigin);\n if (refreshed) {\n response = await send(refreshed);\n }\n }\n\n return response;\n};\n","export default function sleep(ms: number) {\n return new Promise<void>((resolve) => setTimeout(resolve, ms));\n}\n","//? Pure page-route validator. Lives in @luckystack/core (no Node deps) so\n//? both the client-side router (`src/main.tsx`'s `getRoutes()`) AND the\n//? devkit-side scaffold CLI / hot-reload validator can consume it through\n//? the same single source of truth.\n//?\n//? Implements the invisible-parent folder convention: a folder starting\n//? with `privateFolderPrefix` is stripped from the URL but its CHILDREN\n//? remain routeable. A `page.tsx` placed directly inside such a folder\n//? (no non-private sibling segment) is invalid because it has no URL.\n//?\n//? Reserved framework folders (`_api`, `_sync`, `_components`, …) NEVER\n//? host a page, even nested — placing `page.tsx` anywhere under one of\n//? them is a hard error.\n\nexport interface PageRouteRules {\n /**\n * Single-character prefix that marks a folder as **invisible-parent** for\n * page routing. Default: `'_'`.\n */\n privateFolderPrefix: string;\n /**\n * Folder names that are reserved for framework-internal use and may NEVER\n * host a `page.tsx`, nested or otherwise. Scaffold + page-discovery emit\n * a hard error when a page is placed inside one.\n */\n scaffoldIgnoredFolders: readonly string[];\n}\n\nexport const DEFAULT_PAGE_ROUTE_RULES: PageRouteRules = {\n privateFolderPrefix: '_',\n scaffoldIgnoredFolders: [\n '_api',\n '_sync',\n '_function',\n '_functions',\n '_component',\n '_components',\n '_provider',\n '_providers',\n '_locale',\n '_locales',\n '_socket',\n '_sockets',\n '_shared',\n '_server',\n ],\n};\n\nexport interface PagePathValidationResult {\n /** `true` when the page file produces a routeable URL. */\n valid: boolean;\n /** Computed URL when `valid`. Always begins with `/`. `/` for root page. */\n route?: string;\n /** Human-readable explanation when `!valid`. */\n reason?: string;\n}\n\n/**\n * Validate a `page.tsx` file path relative to `src/`. Accepts both\n * forward and back slashes, with or without a leading `./` or `/`.\n * The filename **must** be `page.tsx` (or `page.jsx`); other filenames\n * return `valid: false`.\n *\n * Examples (defaults):\n * admin/page.tsx -> { valid: true, route: '/admin' }\n * _housing/renting/page.tsx -> { valid: true, route: '/renting' }\n * _housing/page.tsx -> { valid: false, reason: 'no URL segment left after stripping underscore folders' }\n * _api/something/page.tsx -> { valid: false, reason: 'page.tsx cannot live inside reserved folder _api' }\n * page.tsx -> { valid: true, route: '/' }\n * some/route.tsx -> { valid: false, reason: 'not a page file (expected page.tsx)' }\n */\nexport const validatePagePath = (\n srcRelativePath: string,\n rules: PageRouteRules = DEFAULT_PAGE_ROUTE_RULES,\n): PagePathValidationResult => {\n const normalized = srcRelativePath.replaceAll('\\\\', '/').replace(/^\\.\\//, '').replace(/^\\/+/, '');\n const segments = normalized.split('/').filter((segment) => segment.length > 0);\n\n if (segments.length === 0) {\n return { valid: false, reason: 'empty path' };\n }\n\n const filename = segments.at(-1);\n if (filename !== 'page.tsx' && filename !== 'page.jsx') {\n return { valid: false, reason: 'not a page file (expected page.tsx)' };\n }\n\n const folderSegments = segments.slice(0, -1);\n const prefix = rules.privateFolderPrefix;\n const reserved = rules.scaffoldIgnoredFolders;\n\n for (const reservedName of reserved) {\n if (folderSegments.includes(reservedName)) {\n return { valid: false, reason: `page.tsx cannot live inside reserved folder ${reservedName}` };\n }\n }\n\n const visibleSegments = folderSegments.filter((segment) => !segment.startsWith(prefix));\n\n if (folderSegments.length > 0 && visibleSegments.length === 0) {\n return { valid: false, reason: 'no URL segment left after stripping underscore folders' };\n }\n\n const route = visibleSegments.length === 0 ? '/' : `/${visibleSegments.join('/')}`;\n return { valid: true, route };\n};\n"],"mappings":"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","names":["registry","responseIndex","registry","token"]}
|