@luckystack/sync 0.1.0

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/handleSyncRequest.ts","../src/_shared/streamEmitters.ts","../src/handleHttpSyncRequest.ts","../src/streamThrottle.ts"],"sourcesContent":["/* eslint-disable unicorn/no-abusive-eslint-disable */\r\n/* eslint-disable */\r\nimport type { syncMessage, PostSyncFanoutPayload } from \"@luckystack/core\";\r\nimport { Socket } from \"socket.io\";\r\nimport { getSession } from \"@luckystack/login\";\r\nimport type { BaseSessionLayout as SessionLayout } from '@luckystack/login';\r\nimport { getProjectConfig } from '@luckystack/core';\r\nimport type { AuthProps } from '@luckystack/login';\r\nimport { getRuntimeSyncMaps } from '@luckystack/core';\r\nimport {\r\n validateRequest,\r\n extractTokenFromSocket,\r\n getIoInstance,\r\n tryCatch,\r\n parseTransportRouteName,\r\n checkRateLimit,\r\n buildSyncProgressEventName,\r\n buildSyncResponseEventName,\r\n socketEventNames,\r\n dispatchHook,\r\n validateInputByType,\r\n getLogger,\r\n} from \"@luckystack/core\";\r\nimport { extractLanguageFromHeader, normalizeErrorResponse, applyErrorFormatter } from \"@luckystack/core\";\r\nimport type { ErrorFormatter } from \"@luckystack/core\";\r\nimport { buildSyncStreamEmitters, type FlushPressure } from './_shared/streamEmitters';\r\nimport {\r\n registerSyncAbortController,\r\n unregisterSyncAbortController,\r\n} from '@luckystack/core';\r\n\r\ntype SyncStreamPayload = Record<string, unknown>;\r\n\r\ninterface RuntimeErrorResponse {\r\n status: 'error';\r\n errorCode?: string;\r\n errorParams?: { key: string; value: string | number | boolean; }[];\r\n httpStatus?: number;\r\n message?: string;\r\n [key: string]: unknown;\r\n}\r\n\r\ninterface RuntimeSuccessResponse {\r\n status: 'success';\r\n message?: string;\r\n httpStatus?: number;\r\n [key: string]: unknown;\r\n}\r\n\r\ntype RuntimeSyncResponse = RuntimeSuccessResponse | RuntimeErrorResponse;\r\n\r\n//? Stream-emit callbacks passed into the `_server` handler. Three flavors:\r\n//?\r\n//? - `stream(payload)` — unicast back to the originator socket\r\n//? only. Cheapest. Use for per-user progress\r\n//? that nobody else cares about.\r\n//? - `broadcastStream(payload)` — fan-out to every socket in the receiver\r\n//? room, ACROSS all server instances (via the\r\n//? Redis adapter's `io.to(room).emit`). Use\r\n//? for live AI chat tokens, collab-editor\r\n//? diffs, anything the whole room should see\r\n//? in real time.\r\n//? - `streamTo(tokens, payload)` — selective fanout to only the given\r\n//? session tokens (each is its own room\r\n//? because every socket joins a room named\r\n//? after its token at connect time). Use\r\n//? when you want explicit subscribers, not\r\n//? \"everyone in the room\".\r\ntype SyncBroadcastStream = (payload?: SyncStreamPayload) => void;\r\ntype SyncStreamTo = (\r\n tokens: string | string[],\r\n payload?: SyncStreamPayload,\r\n) => void;\r\n\r\ninterface RuntimeSyncServerEntry {\r\n auth: AuthProps;\r\n main: (params: {\r\n clientInput: Record<string, unknown>;\r\n user: SessionLayout | null;\r\n functions: Record<string, unknown>;\r\n roomCode: string;\r\n stream: (payload?: SyncStreamPayload) => void;\r\n broadcastStream: SyncBroadcastStream;\r\n streamTo: SyncStreamTo;\r\n //? B1 — per-request AbortSignal. Aborts on client-side cancel\r\n //? (`syncCancel`) or socket disconnect. Handlers that don't destructure\r\n //? these still work unchanged (extra params are dropped).\r\n abortSignal: AbortSignal;\r\n //? B2 — backpressure helper. Awaitable; resolves when the worst-case\r\n //? Socket.io write-buffer across the affected sockets drops below the\r\n //? configured threshold (default 1 MB).\r\n flushPressure: FlushPressure;\r\n }) => Promise<RuntimeSyncResponse>;\r\n inputType?: string;\r\n inputTypeFilePath?: string;\r\n validation?: 'strict' | 'relaxed' | { input: 'skip' | 'strict' };\r\n /**\r\n * Per-route error response formatter. Falls back to the global formatter\r\n * from `registerErrorFormatter(...)`, then to the framework default\r\n * `normalizeErrorResponse`. Same contract as the API handler — both\r\n * transports honor the same `errorFormatter` export.\r\n */\r\n errorFormatter?: ErrorFormatter;\r\n}\r\n\r\ntype RuntimeSyncClientHandler = (params: {\r\n clientInput: Record<string, unknown>;\r\n token: string | null;\r\n functions: Record<string, unknown>;\r\n serverOutput: unknown;\r\n roomCode: string;\r\n stream: (payload?: SyncStreamPayload) => void;\r\n}) => Promise<RuntimeSyncResponse>;\r\n\r\nconst shouldLogDev = () => getProjectConfig().logging.devLogs;\r\nconst shouldLogStream = () => getProjectConfig().logging.stream;\r\n\r\ninterface SyncErrorBuilder {\r\n (args: {\r\n response: { status: 'error'; errorCode?: string; errorParams?: { key: string; value: string | number | boolean }[]; httpStatus?: number };\r\n preferred?: string | null;\r\n userLanguage?: string | null;\r\n }): RuntimeErrorResponse;\r\n}\r\n\r\n//? Returns true when both buckets passed; false when one rejected and the\r\n//? caller should bail. The caller still owns the socket emit + responseIndex\r\n//? wiring because the existing code structure interleaves emit logic with\r\n//? other guard returns.\r\nconst applySyncRateLimits = async ({\r\n resolvedName,\r\n token,\r\n socket,\r\n user,\r\n responseIndex,\r\n buildSyncError,\r\n preferredLocale,\r\n}: {\r\n resolvedName: string;\r\n token: string | null;\r\n socket: Socket;\r\n user: SessionLayout | null;\r\n responseIndex: number | undefined;\r\n buildSyncError: SyncErrorBuilder;\r\n preferredLocale: string | null | undefined;\r\n}): Promise<boolean> => {\r\n const config = getProjectConfig();\r\n const defaultApiLimit = config.rateLimiting.defaultApiLimit;\r\n if (defaultApiLimit !== false && defaultApiLimit > 0) {\r\n const requesterIdentity = token ?? socket.handshake.address ?? 'unknown';\r\n const keyPrefix = token ? 'token' : 'ip';\r\n const rateLimitKey = `${keyPrefix}:${requesterIdentity}:sync:${resolvedName}`;\r\n const { allowed, resetIn } = await checkRateLimit({\r\n key: rateLimitKey,\r\n limit: defaultApiLimit,\r\n windowMs: config.rateLimiting.windowMs,\r\n });\r\n if (!allowed) {\r\n void dispatchHook('rateLimitExceeded', {\r\n scope: token ? 'user' : 'route',\r\n key: rateLimitKey,\r\n limit: defaultApiLimit,\r\n windowMs: config.rateLimiting.windowMs,\r\n count: defaultApiLimit + 1,\r\n route: resolvedName,\r\n userId: user?.id,\r\n });\r\n if (typeof responseIndex === 'number') {\r\n socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'sync.rateLimitExceeded',\r\n errorParams: [{ key: 'seconds', value: resetIn }],\r\n httpStatus: 429,\r\n },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n }));\r\n }\r\n return false;\r\n }\r\n }\r\n\r\n const defaultIpLimit = config.rateLimiting.defaultIpLimit;\r\n if (defaultIpLimit !== false && defaultIpLimit > 0) {\r\n const requesterIp = socket.handshake.address ?? 'unknown';\r\n const ipKey = `ip:${requesterIp}:sync:all`;\r\n const { allowed, resetIn } = await checkRateLimit({\r\n key: ipKey,\r\n limit: defaultIpLimit,\r\n windowMs: config.rateLimiting.windowMs,\r\n });\r\n if (!allowed) {\r\n void dispatchHook('rateLimitExceeded', {\r\n scope: 'ip',\r\n key: ipKey,\r\n limit: defaultIpLimit,\r\n windowMs: config.rateLimiting.windowMs,\r\n count: defaultIpLimit + 1,\r\n ip: requesterIp,\r\n });\r\n if (typeof responseIndex === 'number') {\r\n socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'sync.rateLimitExceeded',\r\n errorParams: [{ key: 'seconds', value: resetIn }],\r\n httpStatus: 429,\r\n },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n }));\r\n }\r\n return false;\r\n }\r\n }\r\n\r\n return true;\r\n};\r\n\r\n\r\n// export default async function handleSyncRequest({ name, clientData, user, serverOutput, roomCode }: syncMessage) {\r\nexport default async function handleSyncRequest({ msg, socket, token }: {\r\n msg: syncMessage,\r\n socket: Socket,\r\n token: string | null,\r\n}) {\r\n\r\n const ioInstance = getIoInstance();\r\n if (!ioInstance) { return; }\r\n\r\n //? first we validate the data\r\n if (typeof msg != 'object') {\r\n if (shouldLogDev()) {\r\n getLogger().warn('sync: socket message was not a json object');\r\n }\r\n const normalized = normalizeErrorResponse({\r\n response: { status: 'error', errorCode: 'sync.invalidRequest' },\r\n preferredLocale:\r\n extractLanguageFromHeader(socket.handshake.headers['x-language'])\r\n || extractLanguageFromHeader(socket.handshake.headers['accept-language']),\r\n });\r\n return socket.emit(socketEventNames.sync, {\r\n status: normalized.status,\r\n message: normalized.message,\r\n errorCode: normalized.errorCode,\r\n errorParams: normalized.errorParams,\r\n httpStatus: normalized.httpStatus,\r\n });\r\n }\r\n\r\n const { name, data, cb, receiver: rawReceiver, responseIndex, ignoreSelf } = msg;\r\n const receiver = typeof rawReceiver === 'string' ? rawReceiver.trim() : '';\r\n const preferredLocale =\r\n extractLanguageFromHeader(socket.handshake.headers['x-language'])\r\n || extractLanguageFromHeader(socket.handshake.headers['accept-language']);\r\n\r\n //? Per-route formatter ref + resolved-name ref — both undefined until the\r\n //? sync entry is looked up. Pre-lookup errors (invalid message, unknown\r\n //? route) emit with global formatter only because there's no syncEntry yet.\r\n let currentRouteName: string | undefined;\r\n let currentPerRouteFormatter: ErrorFormatter | undefined;\r\n let currentUserId: string | undefined;\r\n\r\n const buildSyncError = ({\r\n response,\r\n preferred,\r\n userLanguage,\r\n }: {\r\n response: { status: 'error'; errorCode?: string; errorParams?: { key: string; value: string | number | boolean; }[]; httpStatus?: number };\r\n preferred?: string | null;\r\n userLanguage?: string | null;\r\n }) => {\r\n const normalized = normalizeErrorResponse({\r\n response,\r\n preferredLocale: preferred,\r\n userLanguage,\r\n });\r\n\r\n const baseEnvelope = {\r\n status: normalized.status,\r\n message: normalized.message,\r\n errorCode: normalized.errorCode,\r\n errorParams: normalized.errorParams,\r\n httpStatus: normalized.httpStatus,\r\n };\r\n\r\n //? Per-route → global → identity formatter chain. The cast is sound\r\n //? because applyErrorFormatter only mutates the shape when status is\r\n //? 'error' — and we always pass an error envelope here.\r\n return applyErrorFormatter({\r\n response: baseEnvelope as unknown as Record<string, unknown> & { status?: string },\r\n routeName: currentRouteName ?? 'sync/unknown',\r\n transport: 'socket',\r\n userId: currentUserId,\r\n perRouteFormatter: currentPerRouteFormatter,\r\n }) as unknown as RuntimeErrorResponse;\r\n };\r\n\r\n const ensureSyncErrorShape = (response: { status: 'error'; errorCode?: string; errorParams?: { key: string; value: string | number | boolean; }[]; httpStatus?: number }) => {\r\n if (typeof response.errorCode === 'string' && response.errorCode.trim().length > 0) {\r\n return response;\r\n }\r\n\r\n return {\r\n ...response,\r\n errorCode: 'sync.clientRejected',\r\n };\r\n };\r\n\r\n if (!name || !data || typeof name != 'string' || typeof data != 'object') {\r\n return typeof responseIndex == 'number' && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.invalidRequest' },\r\n preferred: preferredLocale,\r\n }))\r\n }\r\n\r\n const normalizedData = data as Record<string, unknown>;\r\n\r\n const parsedRoute = parseTransportRouteName({ value: name, prefix: 'sync' });\r\n if (parsedRoute.status === 'error') {\r\n return typeof responseIndex == 'number' && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'routing.invalidServiceRouteName',\r\n errorParams: [{ key: 'name', value: name }],\r\n },\r\n preferred: preferredLocale,\r\n }));\r\n }\r\n\r\n const resolvedName = parsedRoute.normalizedFullName;\r\n currentRouteName = resolvedName;\r\n\r\n if (!cb || typeof cb != 'string') {\r\n return typeof responseIndex == 'number' && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.invalidCallback' },\r\n preferred: preferredLocale,\r\n }));\r\n }\r\n\r\n if (!receiver) {\r\n if (shouldLogDev()) {\r\n getLogger().warn('sync: missing receiver / roomCode', { receiver });\r\n }\r\n return typeof responseIndex == 'number' && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.missingReceiver' },\r\n preferred: preferredLocale,\r\n }));\r\n }\r\n\r\n if (shouldLogDev()) {\r\n getLogger().debug(`sync: ${resolvedName} called`, { sync: resolvedName });\r\n }\r\n\r\n const user = await getSession(token);\r\n currentUserId = user?.id;\r\n //? Identity propagation now flows via the `preSyncAuthorize` hook subscriber\r\n //? registered by `@luckystack/error-tracking`'s `enableErrorTrackingAutoInstrumentation()`.\r\n //? Direct `setSentryUser` removed from this handler — see migration doc.\r\n const { syncObject, functionsObject } = await getRuntimeSyncMaps();\r\n\r\n //? B1 — per-request AbortController. The controller drives three things:\r\n //? 1. Aborts when the client emits `syncCancel { cb }` (registered in cancelRegistry).\r\n //? 2. Aborts when the originator socket disconnects (listener below).\r\n //? 3. Gates further chunk emits via the signal handed to streamEmitters.\r\n //? `cb` is the stable per-request key the client already knows; we register\r\n //? under `${socket.id}:${cb}` so cancel lookups are deterministic.\r\n const abortController = new AbortController();\r\n const abortKey = registerSyncAbortController(socket.id, cb, abortController);\r\n const onSocketDisconnect = () => {\r\n abortController.abort();\r\n };\r\n socket.once(socketEventNames.disconnect, onSocketDisconnect);\r\n let cleanupDone = false;\r\n const cleanupRequest = () => {\r\n if (cleanupDone) return;\r\n cleanupDone = true;\r\n socket.off(socketEventNames.disconnect, onSocketDisconnect);\r\n unregisterSyncAbortController(abortKey);\r\n };\r\n\r\n //? we check if there is a client file or/and a server file, if they both dont exist we abort\r\n if (!syncObject[`${resolvedName}_client`] && !syncObject[`${resolvedName}_server`]) {\r\n if (shouldLogDev()) {\r\n getLogger().warn(`sync: ${name} has no _client or _server file`, { sync: name });\r\n }\r\n cleanupRequest();\r\n return typeof responseIndex == 'number' && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.notFound' },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n }));\r\n }\r\n\r\n const { emitServerSyncStream, emitBroadcastSyncStream, emitStreamToTokens, flushPressure } =\r\n buildSyncStreamEmitters({\r\n cb,\r\n receiver,\r\n resolvedName,\r\n logLabel: 'sync',\r\n signal: abortController.signal,\r\n originatorSocket: socket,\r\n emitOriginatorChunk: (payload) => {\r\n if (typeof responseIndex !== 'number') return;\r\n socket.emit(buildSyncProgressEventName(responseIndex), payload);\r\n },\r\n });\r\n\r\n //? Pipeline order: auth → rate-limit → validate → execute → respond.\r\n //? Auth runs first so unauthenticated probes can't consume rate-limit budget\r\n //? or learn input shape from `inputValidation.message`. Mirrors api handlers.\r\n const serverSyncEntry = syncObject[`${resolvedName}_server`] as RuntimeSyncServerEntry | undefined;\r\n currentPerRouteFormatter = serverSyncEntry?.errorFormatter;\r\n if (serverSyncEntry) {\r\n const { auth } = serverSyncEntry;\r\n if (auth.login && !user?.id) {\r\n if (shouldLogDev()) {\r\n getLogger().warn(`sync: ${resolvedName} requires login`, { sync: resolvedName });\r\n }\r\n cleanupRequest();\r\n return typeof responseIndex == 'number' && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({\r\n response: { status: 'error', errorCode: 'auth.required' },\r\n preferred: preferredLocale,\r\n }));\r\n }\r\n\r\n const validationResult = validateRequest({ auth, user: user! });\r\n if (validationResult.status === 'error') {\r\n if (shouldLogDev()) {\r\n getLogger().warn(`sync: auth failed for ${resolvedName}`, { sync: resolvedName, errorCode: validationResult.errorCode });\r\n }\r\n cleanupRequest();\r\n return typeof responseIndex == 'number' && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({\r\n response: {\r\n status: 'error',\r\n errorCode: validationResult.errorCode || 'auth.forbidden',\r\n errorParams: validationResult.errorParams,\r\n httpStatus: validationResult.httpStatus,\r\n },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n }));\r\n }\r\n }\r\n\r\n //? Custom authorization hook. Fires after basic auth + AuthProps check\r\n //? pass, before rate-limit + input validation. Consumers use this to\r\n //? enforce room-membership rules (\"user X may only emit sync to room\r\n //? Y\"), per-tenant policies, or audit trails. Stop with a specific\r\n //? errorCode to reject without revealing input shape.\r\n const preAuthorizeResult = await dispatchHook('preSyncAuthorize', {\r\n routeName: resolvedName,\r\n data: normalizedData,\r\n user,\r\n receiver,\r\n transport: 'socket',\r\n });\r\n if (preAuthorizeResult.stopped) {\r\n if (shouldLogDev()) {\r\n getLogger().warn(`sync: preSyncAuthorize stopped ${resolvedName}`, { sync: resolvedName, errorCode: preAuthorizeResult.signal.errorCode });\r\n }\r\n cleanupRequest();\r\n return typeof responseIndex == 'number' && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({\r\n response: {\r\n status: 'error',\r\n errorCode: preAuthorizeResult.signal.errorCode,\r\n httpStatus: preAuthorizeResult.signal.httpStatus,\r\n },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n }));\r\n }\r\n\r\n //? Observational mirror of `preSyncAuthorize`. Fires after the request\r\n //? passes auth + custom policy; lets audit-log / metrics subscribers\r\n //? record successful authorizations without forking the dispatch path.\r\n //? Stop signals from handlers are ignored (this is post-decision).\r\n void dispatchHook('postSyncAuthorize', {\r\n routeName: resolvedName,\r\n data: normalizedData,\r\n user,\r\n receiver,\r\n transport: 'socket',\r\n });\r\n\r\n //? Rate limit check: per-sync bucket fallback + global per-IP cap\r\n const rateLimitOk = await applySyncRateLimits({\r\n resolvedName,\r\n token,\r\n socket,\r\n user,\r\n responseIndex,\r\n buildSyncError,\r\n preferredLocale,\r\n });\r\n if (!rateLimitOk) { cleanupRequest(); return; }\r\n\r\n let serverOutput = {};\r\n if (serverSyncEntry) {\r\n const { main: serverMain, inputType, inputTypeFilePath } = serverSyncEntry;\r\n\r\n const inputValidation = await validateInputByType({\r\n typeText: inputType,\r\n value: normalizedData,\r\n rootKey: 'clientInput',\r\n filePath: inputTypeFilePath,\r\n });\r\n if (inputValidation.status === 'error') {\r\n cleanupRequest();\r\n return typeof responseIndex == 'number' && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'sync.invalidInputType',\r\n errorParams: [{ key: 'message', value: inputValidation.message }],\r\n },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n }));\r\n }\r\n\r\n //? if the user has passed all the checks we call the preload sync function and return the result\r\n const [serverSyncError, serverSyncResult] = await tryCatch(\r\n async () => await serverMain({\r\n clientInput: normalizedData,\r\n user,\r\n functions: functionsObject,\r\n roomCode: receiver,\r\n stream: emitServerSyncStream,\r\n broadcastStream: emitBroadcastSyncStream,\r\n streamTo: emitStreamToTokens,\r\n abortSignal: abortController.signal,\r\n flushPressure,\r\n }),\r\n undefined,\r\n {\r\n handler: 'handleSyncRequest',\r\n sync: resolvedName,\r\n stage: 'server',\r\n userId: user?.id,\r\n receiver,\r\n transport: 'socket',\r\n },\r\n );\r\n if (serverSyncError) {\r\n if (shouldLogDev()) {\r\n getLogger().error(`sync: server execution failed for ${resolvedName}`, serverSyncError, { sync: resolvedName });\r\n }\r\n cleanupRequest();\r\n return typeof responseIndex == 'number' && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.serverExecutionFailed' },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n }));\r\n } else if (serverSyncResult?.status == 'error') {\r\n const normalizedServerError = buildSyncError({\r\n response: serverSyncResult,\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n });\r\n if (shouldLogDev()) {\r\n getLogger().warn(`sync: server returned error for ${resolvedName}`, { sync: resolvedName, message: normalizedServerError.message });\r\n }\r\n cleanupRequest();\r\n return typeof responseIndex == 'number' && socket.emit(buildSyncResponseEventName(responseIndex), normalizedServerError);\r\n } else if (serverSyncResult?.status !== 'success') {\r\n //? badReturn means it doesnt include a status key with the value 'success' || 'error'\r\n if (shouldLogDev()) {\r\n getLogger().warn(`sync: ${resolvedName}_server returned invalid response`, { sync: resolvedName });\r\n }\r\n cleanupRequest();\r\n return typeof responseIndex == 'number' && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.invalidServerResponse' },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n }));\r\n } else if (serverSyncResult?.status == 'success') {\r\n serverOutput = serverSyncResult;\r\n }\r\n }\r\n\r\n //? from here on we can assume that we have either called a server sync and got a proper result of we didnt call a server sync\r\n\r\n //? Cross-instance recipient list. `fetchSockets()` (via the Redis adapter)\r\n //? returns RemoteSocket objects spanning EVERY backend sharing the adapter —\r\n //? not just this process's room view — so a normal sync fan-out reaches room\r\n //? members connected to other instances. `remoteSocket.emit()` routes to the\r\n //? owning instance. (Per-sync Redis round-trip; see docs/ARCHITECTURE_MULTI_INSTANCE.md.)\r\n const sockets = receiver === 'all'\r\n ? await ioInstance.fetchSockets()\r\n : await ioInstance.in(receiver).fetchSockets();\r\n\r\n //? now we check if we found any sockets\r\n if (sockets.length === 0) {\r\n if (shouldLogDev()) {\r\n getLogger().warn('sync: no sockets found for receiver', { receiver, sync: resolvedName });\r\n }\r\n cleanupRequest();\r\n return typeof responseIndex == 'number' && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.noReceiversFound' },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n }));\r\n }\r\n\r\n //? Single payload reference reused by pre/post — span pinning in\r\n //? `@luckystack/error-tracking` uses WeakMap on this object. `recipientCount`\r\n //? is mutated in place after fanout completes.\r\n const fanoutPayload: PostSyncFanoutPayload = {\r\n routeName: resolvedName,\r\n data: normalizedData,\r\n user,\r\n receiver,\r\n serverOutput,\r\n transport: 'socket',\r\n recipientCount: 0,\r\n };\r\n const preFanoutResult = await dispatchHook('preSyncFanout', fanoutPayload);\r\n if (preFanoutResult.stopped) {\r\n cleanupRequest();\r\n return typeof responseIndex == 'number' && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({\r\n response: {\r\n status: 'error',\r\n errorCode: preFanoutResult.signal.errorCode,\r\n httpStatus: preFanoutResult.signal.httpStatus,\r\n },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n }));\r\n }\r\n\r\n //? here we loop over all the connected clients\r\n //? Yield to the event loop periodically so a giant `receiver: 'all'` fanout\r\n //? doesn't starve other requests. Tunables live in projectConfig.sync.\r\n const { fanoutYieldEvery, fanoutYieldMs } = getProjectConfig().sync;\r\n let recipientCount = 0;\r\n let tempCount = 1;\r\n for (const tempSocket of sockets) {\r\n tempCount++;\r\n if (tempCount % fanoutYieldEvery === 0) { await new Promise(resolve => setTimeout(resolve, fanoutYieldMs)); }\r\n\r\n //? check if they have a token stored in their cookie or session based on the settings\r\n const tempToken = extractTokenFromSocket(tempSocket);\r\n\r\n if (ignoreSelf && typeof ignoreSelf == 'boolean' && token == tempToken) {\r\n continue;\r\n }\r\n\r\n recipientCount++;\r\n\r\n if (syncObject[`${resolvedName}_client`]) {\r\n const clientSyncHandler = syncObject[`${resolvedName}_client`] as RuntimeSyncClientHandler;\r\n const emitClientSyncStream = (payload: SyncStreamPayload = {}) => {\r\n if (shouldLogStream()) {\r\n getLogger().debug(`sync: ${resolvedName} client stream`, { payload });\r\n }\r\n\r\n tempSocket.emit(socketEventNames.sync, {\r\n ...payload,\r\n cb,\r\n fullName: resolvedName,\r\n status: 'stream',\r\n });\r\n };\r\n\r\n const [clientSyncError, clientSyncResult] = await tryCatch(\r\n async () => await clientSyncHandler({ clientInput: normalizedData, token: tempToken, functions: functionsObject, serverOutput, roomCode: receiver, stream: emitClientSyncStream }),\r\n undefined,\r\n {\r\n handler: 'handleSyncRequest',\r\n sync: resolvedName,\r\n stage: 'client',\r\n sourceUserId: user?.id,\r\n targetToken: tempToken,\r\n receiver,\r\n transport: 'socket',\r\n },\r\n );\r\n if (clientSyncError) {\r\n tempSocket.emit(socketEventNames.sync, {\r\n cb,\r\n fullName: resolvedName,\r\n ...buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.clientExecutionFailed' },\r\n preferred:\r\n extractLanguageFromHeader(tempSocket.handshake.headers['x-language'])\r\n || extractLanguageFromHeader(tempSocket.handshake.headers['accept-language']),\r\n }),\r\n });\r\n continue;\r\n }\r\n if (clientSyncResult?.status == 'error') {\r\n tempSocket.emit(socketEventNames.sync, {\r\n cb,\r\n fullName: resolvedName,\r\n ...buildSyncError({\r\n response: ensureSyncErrorShape(clientSyncResult),\r\n preferred:\r\n extractLanguageFromHeader(tempSocket.handshake.headers['x-language'])\r\n || extractLanguageFromHeader(tempSocket.handshake.headers['accept-language']),\r\n }),\r\n });\r\n continue;\r\n }\r\n if (clientSyncResult?.status !== 'success') {\r\n tempSocket.emit(socketEventNames.sync, {\r\n cb,\r\n fullName: resolvedName,\r\n ...buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.invalidClientResponse' },\r\n preferred:\r\n extractLanguageFromHeader(tempSocket.handshake.headers['x-language'])\r\n || extractLanguageFromHeader(tempSocket.handshake.headers['accept-language']),\r\n }),\r\n });\r\n continue;\r\n }\r\n else if (clientSyncResult?.status == 'success') {\r\n const result = {\r\n cb,\r\n fullName: resolvedName,\r\n serverOutput,\r\n clientOutput: clientSyncResult, // Return from _client file (success only)\r\n message: clientSyncResult.message || `${resolvedName} sync success`,\r\n status: 'success'\r\n };\r\n if (shouldLogDev()) {\r\n getLogger().debug(`sync: ${resolvedName} client success`, { result });\r\n }\r\n tempSocket.emit(socketEventNames.sync, result);\r\n }\r\n } else {\r\n //? if there is no client function we still want to send the server data to the clients\r\n const result = {\r\n cb,\r\n fullName: resolvedName,\r\n serverOutput,\r\n clientOutput: {}, // No client file, so empty output\r\n message: `${resolvedName} sync success`,\r\n status: 'success'\r\n };\r\n if (shouldLogDev()) {\r\n getLogger().debug(`sync: ${resolvedName} server-only success`, { result });\r\n }\r\n tempSocket.emit(socketEventNames.sync, result);\r\n }\r\n }\r\n\r\n fanoutPayload.recipientCount = recipientCount;\r\n await dispatchHook('postSyncFanout', fanoutPayload);\r\n\r\n cleanupRequest();\r\n return typeof responseIndex == 'number' && socket.emit(buildSyncResponseEventName(responseIndex), {\r\n status: 'success',\r\n message: `sync ${resolvedName} success`,\r\n result: serverOutput,\r\n });\r\n}","import {\r\n dispatchHook,\r\n getIoInstance,\r\n getLogger,\r\n getProjectConfig,\r\n socketEventNames,\r\n} from '@luckystack/core';\r\nimport type { Socket } from 'socket.io';\r\n\r\n//? Counter of chunks per (routeName, recipient) pair so postSyncStream\r\n//? consumers can index streams. Cleared on receiver-room teardown.\r\nconst chunkCounters = new Map<string, number>();\r\nconst counterKey = (routeName: string, recipient: string): string => `${routeName}|${recipient}`;\r\nconst bumpChunkIndex = (routeName: string, recipient: string): number => {\r\n const key = counterKey(routeName, recipient);\r\n const next = (chunkCounters.get(key) ?? 0) + 1;\r\n chunkCounters.set(key, next);\r\n return next;\r\n};\r\nconst dispatchStreamHooks = (routeName: string, recipient: string, chunk: unknown): void => {\r\n //? Fire-and-forget — stream emitters are sync and consumer hooks must\r\n //? not block chunk delivery. Errors inside hooks are swallowed by the\r\n //? hook dispatcher's own tryCatch.\r\n void dispatchHook('preSyncStream', { routeName, chunk, recipient });\r\n const chunkIndex = bumpChunkIndex(routeName, recipient);\r\n void dispatchHook('postSyncStream', { routeName, chunk, recipient, chunkIndex });\r\n};\r\n\r\nexport type SyncStreamPayload = Record<string, unknown>;\r\n\r\n//? Optional helper passed to consumer handlers so an LLM/long stream can\r\n//? `await flushPressure()` between chunks. Resolves once the underlying\r\n//? Socket.io transport's pending write buffer drops below `thresholdBytes`\r\n//? (measured in packets, not bytes — engine.io exposes a writeBuffer of\r\n//? packet objects, not byte length, so we approximate via packet count).\r\n//? Default threshold = 1 MB ≈ 1024 packets at ~1KB each. Handlers opt in;\r\n//? omitting the call is fine for handlers that don't stream a lot.\r\nexport interface FlushPressureOptions {\r\n /**\r\n * Drain threshold in bytes. Used as a packet-count approximation —\r\n * we assume an average packet size of ~1024 bytes and resolve once the\r\n * engine.io writeBuffer length is below `thresholdBytes / 1024`.\r\n * Default: 1_048_576 (1 MB).\r\n */\r\n thresholdBytes?: number;\r\n}\r\n\r\nexport type FlushPressure = (options?: FlushPressureOptions) => Promise<void>;\r\n\r\nexport interface SyncStreamEmitters {\r\n emitServerSyncStream: (payload?: SyncStreamPayload) => void;\r\n emitBroadcastSyncStream: (payload?: SyncStreamPayload) => void;\r\n emitStreamToTokens: (tokens: string | string[], payload?: SyncStreamPayload) => void;\r\n buildBroadcastFrame: (payload: SyncStreamPayload) => Record<string, unknown>;\r\n flushPressure: FlushPressure;\r\n}\r\n\r\nconst shouldLogStream = () => getProjectConfig().logging.stream;\r\n\r\n//? Default 1 MB threshold (per spec B2). Packet count = bytes / avg-packet-size.\r\nconst DEFAULT_THRESHOLD_BYTES = 1_048_576;\r\nconst AVG_PACKET_BYTES = 1024;\r\nconst POLL_INTERVAL_MS = 10;\r\n//? Cap sockets considered for worst-case pressure so a `receiver: 'all'`\r\n//? broadcast doesn't degrade to O(n). First 32 — see spec B2.\r\nconst MAX_SOCKETS_FOR_PRESSURE_SAMPLE = 32;\r\n\r\n//? Read the engine.io write-buffer length defensively — the underlying\r\n//? `socket.conn` and `transport` are present at runtime but `writeBuffer`\r\n//? is marked private in the engine.io typings. Narrow the shape via a\r\n//? runtime guard (vs `as unknown as` double-cast) to satisfy the\r\n//? boundary-helper lint rule.\r\ninterface EngineIoConnLike {\r\n writeBuffer?: { length: number };\r\n transport?: { writable?: boolean };\r\n}\r\n\r\nconst isEngineConnLike = (value: unknown): value is EngineIoConnLike =>\r\n typeof value === 'object' && value !== null;\r\n\r\nconst readSocketPressure = (socket: Socket): { packets: number; writable: boolean } => {\r\n const maybeConn: unknown = (socket as { conn?: unknown }).conn;\r\n if (!isEngineConnLike(maybeConn)) return { packets: 0, writable: true };\r\n const packets = maybeConn.writeBuffer?.length ?? 0;\r\n const writable = maybeConn.transport?.writable ?? true;\r\n return { packets, writable };\r\n};\r\n\r\n//? Poll-based drain — engine.io doesn't surface a per-socket `drain` event\r\n//? in the Socket.io public API. Poll every POLL_INTERVAL_MS until either\r\n//? buffer drops below threshold, transport is no longer writable (in\r\n//? which case waiting is pointless), or `isAborted()` returns true.\r\nconst waitUntilSocketDrained = async (\r\n socket: Socket,\r\n packetThreshold: number,\r\n isAborted: () => boolean,\r\n): Promise<void> => {\r\n let { packets, writable } = readSocketPressure(socket);\r\n while (writable && packets >= packetThreshold && !isAborted()) {\r\n await new Promise<void>((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\r\n ({ packets, writable } = readSocketPressure(socket));\r\n }\r\n};\r\n\r\nconst collectRoomSocketsForPressure = (receiver: string): Socket[] => {\r\n const io = getIoInstance();\r\n if (!io) return [];\r\n if (!receiver) return [];\r\n\r\n if (receiver === 'all') {\r\n const out: Socket[] = [];\r\n let i = 0;\r\n for (const [, sock] of io.sockets.sockets) {\r\n if (i >= MAX_SOCKETS_FOR_PRESSURE_SAMPLE) break;\r\n out.push(sock);\r\n i++;\r\n }\r\n return out;\r\n }\r\n\r\n const ids = io.sockets.adapter.rooms.get(receiver);\r\n if (!ids || ids.size === 0) return [];\r\n const out: Socket[] = [];\r\n let i = 0;\r\n for (const id of ids) {\r\n if (i >= MAX_SOCKETS_FOR_PRESSURE_SAMPLE) break;\r\n const sock = io.sockets.sockets.get(id);\r\n if (sock) out.push(sock);\r\n i++;\r\n }\r\n return out;\r\n};\r\n\r\ninterface BuildSyncStreamEmittersArgs {\r\n cb: string | undefined;\r\n receiver: string;\r\n resolvedName: string;\r\n emitOriginatorChunk: (payload: SyncStreamPayload) => void;\r\n logLabel: string;\r\n /**\r\n * AbortSignal sourced from the per-request controller in `handleSyncRequest`\r\n * / `handleHttpSyncRequest`. When aborted (client disconnect, explicit\r\n * `syncCancel`), every emit is short-circuited with a single dev-log line\r\n * and `flushPressure()` resolves immediately.\r\n */\r\n signal?: AbortSignal;\r\n /**\r\n * Originator socket — used to drive `flushPressure` measurement when the\r\n * stream targets only the originator. Optional because the HTTP/SSE\r\n * transport has no originator socket (SSE response writer is the sink).\r\n */\r\n originatorSocket?: Socket;\r\n}\r\n\r\n//? Shared between socket (`handleSyncRequest`) and HTTP/SSE\r\n//? (`handleHttpSyncRequest`) transports. The only divergence is the originator\r\n//? sink: socket transport unicasts a progress event back to the requesting\r\n//? socket; HTTP transport pipes the chunk through the SSE writer. Caller\r\n//? supplies that as `emitOriginatorChunk`. Broadcast/streamTo paths use\r\n//? Socket.io regardless of transport because recipients always live on sockets.\r\nexport const buildSyncStreamEmitters = ({\r\n cb,\r\n receiver,\r\n resolvedName,\r\n emitOriginatorChunk,\r\n logLabel,\r\n signal,\r\n originatorSocket,\r\n}: BuildSyncStreamEmittersArgs): SyncStreamEmitters => {\r\n const buildBroadcastFrame = (payload: SyncStreamPayload) => ({\r\n ...payload,\r\n cb,\r\n fullName: resolvedName,\r\n status: 'stream' as const,\r\n });\r\n\r\n const isAborted = (): boolean => signal?.aborted === true;\r\n const logAbortedDrop = (kind: string): void => {\r\n if (shouldLogStream()) {\r\n getLogger().debug(`${logLabel}: ${resolvedName} ${kind} skipped — request aborted`);\r\n }\r\n };\r\n\r\n const emitServerSyncStream = (payload: SyncStreamPayload = {}) => {\r\n if (isAborted()) { logAbortedDrop('server stream'); return; }\r\n if (shouldLogStream()) {\r\n getLogger().debug(`${logLabel}: ${resolvedName} server stream`, { payload });\r\n }\r\n dispatchStreamHooks(resolvedName, 'originator', payload);\r\n emitOriginatorChunk(payload);\r\n };\r\n\r\n const emitBroadcastSyncStream = (payload: SyncStreamPayload = {}) => {\r\n if (isAborted()) { logAbortedDrop('broadcastStream'); return; }\r\n if (shouldLogStream()) {\r\n getLogger().debug(`${logLabel}: ${resolvedName} broadcastStream`, { payload });\r\n }\r\n if (!receiver) return;\r\n const io = getIoInstance();\r\n if (!io) return;\r\n\r\n dispatchStreamHooks(resolvedName, receiver, payload);\r\n\r\n //? `io.to(room).emit` fans the chunk out across EVERY server instance via\r\n //? the Redis adapter, so room members connected to a different instance get\r\n //? it too. Do NOT gate on `adapter.rooms.get(receiver)` — that is the\r\n //? per-process room view; in a multi-instance cluster it only sees locally\r\n //? connected members, so the previous \"size <= 1 ⇒ unicast to the lone\r\n //? socket\" optimization mis-fired whenever the other room members lived on\r\n //? another instance and collapsed broadcastStream into an originator-only\r\n //? stream. `streamTo` always used `io.to(...).emit` and never had this bug;\r\n //? this aligns broadcastStream with it.\r\n io.to(receiver).emit(socketEventNames.sync, buildBroadcastFrame(payload));\r\n };\r\n\r\n const emitStreamToTokens = (\r\n tokens: string | string[],\r\n payload: SyncStreamPayload = {},\r\n ) => {\r\n if (isAborted()) { logAbortedDrop('streamTo'); return; }\r\n const list = Array.isArray(tokens) ? tokens : [tokens];\r\n const filtered = list.filter((t): t is string => typeof t === 'string' && t.length > 0);\r\n if (filtered.length === 0) return;\r\n if (shouldLogStream()) {\r\n getLogger().debug(`${logLabel}: ${resolvedName} streamTo`, { tokens: filtered, payload });\r\n }\r\n const io = getIoInstance();\r\n if (!io) return;\r\n for (const recipient of filtered) {\r\n dispatchStreamHooks(resolvedName, recipient, payload);\r\n }\r\n const frame = buildBroadcastFrame(payload);\r\n io.to(filtered).emit(socketEventNames.sync, frame);\r\n };\r\n\r\n //? Backpressure helper. Resolves once the worst-case pending write-buffer\r\n //? across the affected sockets drops below the configured threshold. Used\r\n //? opt-in by handlers streaming many small chunks (LLM tokens, telemetry).\r\n //?\r\n //? Sockets considered (in order of preference):\r\n //? 1. Originator socket (if provided) — covers `stream(payload)`.\r\n //? 2. Room sockets for `receiver` (up to 32) — covers `broadcastStream` / `streamTo`.\r\n //? If neither is available we return immediately — there's nothing to\r\n //? measure pressure on (e.g. HTTP/SSE only, empty room).\r\n const flushPressure: FlushPressure = async ({ thresholdBytes } = {}) => {\r\n if (isAborted()) return;\r\n const effectiveThresholdBytes = typeof thresholdBytes === 'number' && thresholdBytes > 0\r\n ? thresholdBytes\r\n : DEFAULT_THRESHOLD_BYTES;\r\n const packetThreshold = Math.max(1, Math.ceil(effectiveThresholdBytes / AVG_PACKET_BYTES));\r\n\r\n const targets: Socket[] = [];\r\n if (originatorSocket) targets.push(originatorSocket);\r\n for (const sock of collectRoomSocketsForPressure(receiver)) {\r\n //? Avoid duplicating the originator if it's also in the receiver room.\r\n if (sock !== originatorSocket) targets.push(sock);\r\n }\r\n if (targets.length === 0) return;\r\n\r\n //? Wait on every target in parallel — worst-case latency wins. Abort\r\n //? mid-wait short-circuits via `isAborted()` check inside the loop body.\r\n await Promise.all(targets.map((sock) => waitUntilSocketDrained(sock, packetThreshold, isAborted)));\r\n };\r\n\r\n return {\r\n emitServerSyncStream,\r\n emitBroadcastSyncStream,\r\n emitStreamToTokens,\r\n buildBroadcastFrame,\r\n flushPressure,\r\n };\r\n};\r\n","/* eslint-disable unicorn/no-abusive-eslint-disable */\r\n/* eslint-disable */\r\nimport { getSession } from \"@luckystack/login\";\r\nimport type { BaseSessionLayout as SessionLayout } from '@luckystack/login';\r\nimport { getProjectConfig } from '@luckystack/core';\r\nimport type { AuthProps } from '@luckystack/login';\r\nimport { getRuntimeSyncMaps as getRuntimeSyncMapsFromSource } from '@luckystack/core';\r\nimport {\r\n validateRequest,\r\n extractTokenFromSocket,\r\n getIoInstance,\r\n tryCatch,\r\n parseTransportRouteName,\r\n checkRateLimit,\r\n socketEventNames,\r\n validateInputByType,\r\n dispatchHook,\r\n getLogger,\r\n} from \"@luckystack/core\";\r\nimport { extractLanguageFromHeader, normalizeErrorResponse, applyErrorFormatter } from \"@luckystack/core\";\r\nimport type { ErrorFormatter } from \"@luckystack/core\";\r\nimport type { PostSyncFanoutPayload } from '@luckystack/core';\r\nimport { buildSyncStreamEmitters, type FlushPressure } from './_shared/streamEmitters';\r\n\r\ninterface HttpSyncRequestParams {\r\n name: string;\r\n cb?: string;\r\n data: Record<string, unknown>;\r\n receiver: string;\r\n ignoreSelf?: boolean;\r\n token: string | null;\r\n requesterIp?: string;\r\n xLanguageHeader?: string | string[];\r\n acceptLanguageHeader?: string | string[];\r\n stream?: (payload: HttpSyncStreamEvent) => void;\r\n /**\r\n * Optional AbortSignal. The HTTP server (`@luckystack/server`) wires this\r\n * to `req.on('close', ...)` so a closed SSE connection aborts in-flight\r\n * stream emits. Sync handlers receive it as `abortSignal` in params.\r\n */\r\n abortSignal?: AbortSignal;\r\n}\r\n\r\ninterface HttpSyncResponse {\r\n status: 'success' | 'error';\r\n message: string;\r\n errorCode?: string;\r\n errorParams?: { key: string; value: string | number | boolean; }[];\r\n httpStatus?: number;\r\n}\r\n\r\ntype SyncStreamPayload = Record<string, unknown>;\r\n\r\ninterface RuntimeErrorResponse {\r\n status: 'error';\r\n errorCode?: string;\r\n errorParams?: { key: string; value: string | number | boolean; }[];\r\n httpStatus?: number;\r\n message?: string;\r\n [key: string]: unknown;\r\n}\r\n\r\ninterface RuntimeSuccessResponse {\r\n status: 'success';\r\n message?: string;\r\n httpStatus?: number;\r\n [key: string]: unknown;\r\n}\r\n\r\ntype RuntimeSyncResponse = RuntimeSuccessResponse | RuntimeErrorResponse;\r\n\r\ntype SyncBroadcastStream = (payload?: SyncStreamPayload) => void;\r\ntype SyncStreamTo = (\r\n tokens: string | string[],\r\n payload?: SyncStreamPayload,\r\n) => void;\r\n\r\ninterface RuntimeSyncServerEntry {\r\n auth: AuthProps;\r\n main: (params: {\r\n clientInput: Record<string, unknown>;\r\n user: SessionLayout | null;\r\n functions: Record<string, unknown>;\r\n roomCode: string;\r\n stream: (payload?: SyncStreamPayload) => void;\r\n broadcastStream: SyncBroadcastStream;\r\n streamTo: SyncStreamTo;\r\n abortSignal: AbortSignal;\r\n flushPressure: FlushPressure;\r\n }) => Promise<RuntimeSyncResponse>;\r\n inputType?: string;\r\n inputTypeFilePath?: string;\r\n validation?: 'strict' | 'relaxed' | { input: 'skip' | 'strict' };\r\n errorFormatter?: ErrorFormatter;\r\n}\r\n\r\ntype RuntimeSyncClientHandler = (params: {\r\n clientInput: Record<string, unknown>;\r\n token: string | null;\r\n functions: Record<string, unknown>;\r\n serverOutput: unknown;\r\n roomCode: string;\r\n stream: (payload?: SyncStreamPayload) => void;\r\n}) => Promise<RuntimeSyncResponse>;\r\n\r\nconst shouldLogDev = () => getProjectConfig().logging.devLogs;\r\nconst shouldLogStream = () => getProjectConfig().logging.stream;\r\n\r\ninterface HttpSyncErrorBuilder {\r\n (args: {\r\n response: { status: 'error'; errorCode?: string; errorParams?: { key: string; value: string | number | boolean }[]; httpStatus?: number };\r\n preferred?: string | null;\r\n userLanguage?: string | null;\r\n }): HttpSyncResponse;\r\n}\r\n\r\n//? Returns an HttpSyncResponse when a rate limit was hit (caller should\r\n//? return it directly), or null when both buckets passed.\r\nconst applyHttpSyncRateLimits = async ({\r\n resolvedName,\r\n token,\r\n requesterIp,\r\n user,\r\n buildSyncError,\r\n preferredLocale,\r\n}: {\r\n resolvedName: string;\r\n token: string | null;\r\n requesterIp: string | undefined;\r\n user: SessionLayout | null;\r\n buildSyncError: HttpSyncErrorBuilder;\r\n preferredLocale: string | null | undefined;\r\n}): Promise<HttpSyncResponse | null> => {\r\n const config = getProjectConfig();\r\n const effectiveSyncLimit = config.rateLimiting.defaultApiLimit;\r\n if (effectiveSyncLimit !== false && effectiveSyncLimit > 0) {\r\n const requesterIdentity = token ?? requesterIp ?? 'anonymous';\r\n const keyPrefix = token ? 'token' : 'ip';\r\n const rateLimitKey = `${keyPrefix}:${requesterIdentity}:sync:${resolvedName}`;\r\n const { allowed, resetIn } = await checkRateLimit({\r\n key: rateLimitKey,\r\n limit: effectiveSyncLimit,\r\n windowMs: config.rateLimiting.windowMs,\r\n });\r\n if (!allowed) {\r\n void dispatchHook('rateLimitExceeded', {\r\n scope: token ? 'user' : 'route',\r\n key: rateLimitKey,\r\n limit: effectiveSyncLimit,\r\n windowMs: config.rateLimiting.windowMs,\r\n count: effectiveSyncLimit + 1,\r\n route: resolvedName,\r\n userId: user?.id,\r\n });\r\n return buildSyncError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'sync.rateLimitExceeded',\r\n errorParams: [{ key: 'seconds', value: resetIn }],\r\n httpStatus: 429,\r\n },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n });\r\n }\r\n }\r\n\r\n const defaultIpLimit = config.rateLimiting.defaultIpLimit;\r\n //? Mirror handleHttpApiRequest: skip the global per-IP abuse limit for\r\n //? loopback traffic in non-production (dev + the scalable test suite).\r\n //? Per-route limits still apply; production is unaffected.\r\n const requesterIsLoopback = process.env.NODE_ENV !== 'production'\r\n && (requesterIp === '127.0.0.1' || requesterIp === '::1' || requesterIp === '::ffff:127.0.0.1'\r\n || (typeof requesterIp === 'string' && requesterIp.startsWith('127.')));\r\n if (!requesterIsLoopback && defaultIpLimit !== false && defaultIpLimit > 0) {\r\n const ipBucket = requesterIp ?? 'unknown';\r\n const ipKey = `ip:${ipBucket}:sync:all`;\r\n const { allowed, resetIn } = await checkRateLimit({\r\n key: ipKey,\r\n limit: defaultIpLimit,\r\n windowMs: config.rateLimiting.windowMs,\r\n });\r\n if (!allowed) {\r\n void dispatchHook('rateLimitExceeded', {\r\n scope: 'ip',\r\n key: ipKey,\r\n limit: defaultIpLimit,\r\n windowMs: config.rateLimiting.windowMs,\r\n count: defaultIpLimit + 1,\r\n ip: ipBucket,\r\n });\r\n return buildSyncError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'sync.rateLimitExceeded',\r\n errorParams: [{ key: 'seconds', value: resetIn }],\r\n httpStatus: 429,\r\n },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n });\r\n }\r\n }\r\n\r\n return null;\r\n};\r\n\r\nexport type HttpSyncStreamEvent = SyncStreamPayload;\r\n\r\nexport default async function handleHttpSyncRequest({\r\n name,\r\n cb,\r\n data,\r\n receiver,\r\n ignoreSelf,\r\n token,\r\n requesterIp,\r\n xLanguageHeader,\r\n acceptLanguageHeader,\r\n stream,\r\n abortSignal,\r\n}: HttpSyncRequestParams): Promise<HttpSyncResponse> {\r\n if (shouldLogDev()) {\r\n getLogger().debug(`http sync: ${name} called`);\r\n }\r\n\r\n //? B1 — HTTP/SSE transport. The caller (typically `@luckystack/server`'s\r\n //? SSE bridge) wires `req.on('close', ...)` to a controller and passes its\r\n //? signal in. If no signal was provided we build a dummy controller so\r\n //? `signal` is always defined for handler param shape. The dummy never\r\n //? aborts on its own, which preserves current behavior for callers that\r\n //? don't opt in.\r\n const effectiveAbortSignal = abortSignal ?? new AbortController().signal;\r\n\r\n const normalizedReceiver = typeof receiver === 'string' ? receiver.trim() : '';\r\n const preferredLocale =\r\n extractLanguageFromHeader(xLanguageHeader)\r\n || extractLanguageFromHeader(acceptLanguageHeader);\r\n const user = await getSession(token);\r\n //? Identity propagation + span lifecycle now flow via the\r\n //? `preSyncAuthorize` / `preSyncFanout` / `postSyncFanout` hook subscribers\r\n //? registered by `@luckystack/error-tracking`'s\r\n //? `enableErrorTrackingAutoInstrumentation()`. Direct `setSentryUser` +\r\n //? `startSpan` removed from this handler — see migration doc.\r\n\r\n //? Per-route formatter ref. Mirrors the socket-sync + API handler pattern —\r\n //? set after the syncEntry lookup; pre-lookup errors fall through to global\r\n //? formatter only because there's no entry yet to read from.\r\n let currentRouteName: string | undefined;\r\n let currentPerRouteFormatter: ErrorFormatter | undefined;\r\n\r\n const buildSyncError = ({\r\n response,\r\n preferred,\r\n userLanguage,\r\n }: {\r\n response: { status: 'error'; errorCode?: string; errorParams?: { key: string; value: string | number | boolean; }[]; httpStatus?: number };\r\n preferred?: string | null;\r\n userLanguage?: string | null;\r\n }): HttpSyncResponse => {\r\n const normalized = normalizeErrorResponse({\r\n response,\r\n preferredLocale: preferred,\r\n userLanguage,\r\n });\r\n\r\n const baseEnvelope = {\r\n status: normalized.status,\r\n message: normalized.message,\r\n errorCode: normalized.errorCode,\r\n errorParams: normalized.errorParams,\r\n httpStatus: normalized.httpStatus,\r\n };\r\n\r\n return applyErrorFormatter({\r\n response: baseEnvelope as unknown as Record<string, unknown> & { status?: string },\r\n routeName: currentRouteName ?? 'sync/unknown',\r\n transport: 'http',\r\n userId: user?.id,\r\n perRouteFormatter: currentPerRouteFormatter,\r\n }) as unknown as HttpSyncResponse;\r\n };\r\n\r\n const ensureSyncErrorShape = (response: { status: 'error'; errorCode?: string; errorParams?: { key: string; value: string | number | boolean; }[]; httpStatus?: number }) => {\r\n if (typeof response.errorCode === 'string' && response.errorCode.trim().length > 0) {\r\n return response;\r\n }\r\n\r\n return {\r\n ...response,\r\n errorCode: 'sync.clientRejected',\r\n };\r\n };\r\n\r\n const ioInstance = getIoInstance();\r\n\r\n //? Wrap the body so the span always closes — including on an unexpected\r\n //? throw. tryCatch returns `[error, value]` so we can call `span.end()`\r\n //? in one place after the inner work resolves.\r\n const [bodyError, bodyResult] = await tryCatch(async () => {\r\n if (!ioInstance) {\r\n return buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.ioUnavailable' },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n });\r\n }\r\n\r\n if (!name || typeof name !== 'string') {\r\n return buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.invalidRequest' },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n });\r\n }\r\n\r\n const parsedRoute = parseTransportRouteName({ value: name, prefix: 'sync' });\r\n if (parsedRoute.status === 'error') {\r\n return buildSyncError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'routing.invalidServiceRouteName',\r\n errorParams: [{ key: 'name', value: name }],\r\n },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n });\r\n }\r\n\r\n const resolvedName = parsedRoute.normalizedFullName;\r\n currentRouteName = resolvedName;\r\n const callbackName = typeof cb === 'string' && cb.trim().length > 0\r\n ? cb.trim()\r\n : `${parsedRoute.serviceRoute.normalizedRouteName}/${parsedRoute.version}`;\r\n\r\n if (!normalizedReceiver) {\r\n return buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.missingReceiver' },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n });\r\n }\r\n\r\n const { syncObject, functionsObject } = await getRuntimeSyncMapsFromSource();\r\n\r\n if (!syncObject[`${resolvedName}_client`] && !syncObject[`${resolvedName}_server`]) {\r\n return buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.notFound' },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n });\r\n }\r\n\r\n //? Pipeline order: auth → rate-limit → validate → execute → respond.\r\n //? Auth runs first so unauthenticated probes can't consume rate-limit budget\r\n //? or learn input shape from `inputValidation.message`. Mirrors api handlers.\r\n const serverSyncEntry = syncObject[`${resolvedName}_server`] as RuntimeSyncServerEntry | undefined;\r\n currentPerRouteFormatter = serverSyncEntry?.errorFormatter;\r\n if (serverSyncEntry) {\r\n const { auth } = serverSyncEntry;\r\n if (auth.login && !user?.id) {\r\n return buildSyncError({\r\n response: { status: 'error', errorCode: 'auth.required' },\r\n preferred: preferredLocale,\r\n });\r\n }\r\n\r\n const validationResult = validateRequest({ auth, user: user! });\r\n if (validationResult.status === 'error') {\r\n return buildSyncError({\r\n response: {\r\n status: 'error',\r\n errorCode: validationResult.errorCode || 'auth.forbidden',\r\n errorParams: validationResult.errorParams,\r\n httpStatus: validationResult.httpStatus,\r\n },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n });\r\n }\r\n }\r\n\r\n //? Identity propagation hook — runs after basic auth + AuthProps check,\r\n //? before rate-limit + input validation. `@luckystack/error-tracking`'s\r\n //? auto-instrumentation subscribes here to call `setSentryUser(user)`.\r\n const preAuthorizeResult = await dispatchHook('preSyncAuthorize', {\r\n routeName: resolvedName,\r\n data,\r\n user,\r\n receiver: normalizedReceiver,\r\n transport: 'http',\r\n });\r\n if (preAuthorizeResult.stopped) {\r\n return buildSyncError({\r\n response: {\r\n status: 'error',\r\n errorCode: preAuthorizeResult.signal.errorCode,\r\n httpStatus: preAuthorizeResult.signal.httpStatus,\r\n },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n });\r\n }\r\n\r\n // Rate limiting for HTTP sync requests (per-route + global IP buckets)\r\n const rateLimitResult = await applyHttpSyncRateLimits({\r\n resolvedName,\r\n token,\r\n requesterIp,\r\n user,\r\n buildSyncError,\r\n preferredLocale,\r\n });\r\n if (rateLimitResult) return rateLimitResult;\r\n\r\n let serverOutput = {};\r\n if (serverSyncEntry) {\r\n const { main: serverMain, inputType, inputTypeFilePath } = serverSyncEntry;\r\n const { emitServerSyncStream, emitBroadcastSyncStream, emitStreamToTokens, flushPressure } =\r\n buildSyncStreamEmitters({\r\n cb,\r\n receiver: normalizedReceiver,\r\n resolvedName,\r\n logLabel: 'http sync',\r\n signal: effectiveAbortSignal,\r\n //? No originatorSocket for HTTP/SSE — `flushPressure` falls back\r\n //? to room-socket measurement only. SSE backpressure is the\r\n //? caller's responsibility (Node's `res.write` returns a bool).\r\n //? Originator chunks travel back via SSE; broadcast / targeted\r\n //? chunks still flow over Socket.io to recipients in the receiver room.\r\n emitOriginatorChunk: (payload) => {\r\n stream?.(payload);\r\n },\r\n });\r\n\r\n const inputValidation = await validateInputByType({\r\n typeText: inputType,\r\n value: data,\r\n rootKey: 'clientInput',\r\n filePath: inputTypeFilePath,\r\n });\r\n if (inputValidation.status === 'error') {\r\n return buildSyncError({\r\n response: {\r\n status: 'error',\r\n errorCode: 'sync.invalidInputType',\r\n errorParams: [{ key: 'message', value: inputValidation.message }],\r\n },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n });\r\n }\r\n\r\n const [serverSyncError, serverSyncResult] = await tryCatch(\r\n async () => await serverMain({\r\n clientInput: data,\r\n user,\r\n functions: functionsObject,\r\n roomCode: normalizedReceiver,\r\n stream: emitServerSyncStream,\r\n broadcastStream: emitBroadcastSyncStream,\r\n streamTo: emitStreamToTokens,\r\n abortSignal: effectiveAbortSignal,\r\n flushPressure,\r\n }),\r\n undefined,\r\n {\r\n handler: 'handleHttpSyncRequest',\r\n sync: resolvedName,\r\n stage: 'server',\r\n userId: user?.id,\r\n receiver,\r\n transport: 'http',\r\n },\r\n );\r\n if (serverSyncError) {\r\n return buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.serverExecutionFailed' },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n });\r\n }\r\n\r\n if (serverSyncResult?.status == 'error') {\r\n return buildSyncError({\r\n response: serverSyncResult,\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n });\r\n }\r\n\r\n if (serverSyncResult?.status !== 'success') {\r\n return buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.invalidServerResponse' },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n });\r\n }\r\n\r\n serverOutput = serverSyncResult;\r\n }\r\n\r\n //? Single payload reference reused by pre/post — span pinning in\r\n //? `@luckystack/error-tracking` uses WeakMap on this object.\r\n const fanoutPayload: PostSyncFanoutPayload = {\r\n routeName: resolvedName,\r\n data,\r\n user,\r\n receiver: normalizedReceiver,\r\n serverOutput,\r\n transport: 'http',\r\n recipientCount: 0,\r\n };\r\n await dispatchHook('preSyncFanout', fanoutPayload);\r\n\r\n //? Over the HTTP/SSE fallback the caller IS the originator, so a receiver\r\n //? room with no connected sockets (no peers online, or the originator used\r\n //? HTTP instead of a websocket) is normal — NOT an error. Fall back to an\r\n //? empty set so the fanout loop simply runs zero times; the server handler\r\n //? already ran and its `serverOutput` is the meaningful result returned below.\r\n //? Cross-instance recipient list (RemoteSocket[]) spanning every backend on\r\n //? the shared Redis adapter, so an HTTP-triggered sync still fans out to room\r\n //? members on other instances. Empty array = no peers online, which is normal\r\n //? over the HTTP fallback (the loop just runs zero times).\r\n const sockets = receiver === 'all'\r\n ? await ioInstance.fetchSockets()\r\n : await ioInstance.in(normalizedReceiver).fetchSockets();\r\n\r\n let recipientCount = 0;\r\n for (const tempSocket of sockets) {\r\n const tempToken = extractTokenFromSocket(tempSocket);\r\n\r\n if (ignoreSelf && token && token === tempToken) {\r\n continue;\r\n }\r\n\r\n if (syncObject[`${resolvedName}_client`]) {\r\n const clientSyncHandler = syncObject[`${resolvedName}_client`] as RuntimeSyncClientHandler;\r\n const emitClientSyncStream = (payload: SyncStreamPayload = {}) => {\r\n if (shouldLogStream()) {\r\n getLogger().debug(`http sync: ${resolvedName} client stream`, { payload });\r\n }\r\n\r\n tempSocket.emit(socketEventNames.sync, {\r\n ...payload,\r\n cb: callbackName,\r\n fullName: resolvedName,\r\n status: 'stream',\r\n });\r\n };\r\n\r\n const [clientSyncError, clientSyncResult] = await tryCatch(\r\n async () => await clientSyncHandler({ clientInput: data, token: tempToken, functions: functionsObject, serverOutput, roomCode: normalizedReceiver, stream: emitClientSyncStream }),\r\n undefined,\r\n {\r\n handler: 'handleHttpSyncRequest',\r\n sync: resolvedName,\r\n stage: 'client',\r\n sourceUserId: user?.id,\r\n targetToken: tempToken,\r\n receiver,\r\n transport: 'http',\r\n },\r\n );\r\n if (clientSyncError) {\r\n tempSocket.emit(socketEventNames.sync, {\r\n cb: callbackName,\r\n fullName: resolvedName,\r\n ...buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.clientExecutionFailed' },\r\n preferred: extractLanguageFromHeader(tempSocket.handshake.headers['accept-language'] || tempSocket.handshake.headers['x-language']),\r\n }),\r\n });\r\n continue;\r\n }\r\n\r\n if (clientSyncResult?.status === 'error') {\r\n tempSocket.emit(socketEventNames.sync, {\r\n cb: callbackName,\r\n fullName: resolvedName,\r\n ...buildSyncError({\r\n response: ensureSyncErrorShape(clientSyncResult),\r\n preferred: extractLanguageFromHeader(tempSocket.handshake.headers['accept-language'] || tempSocket.handshake.headers['x-language']),\r\n }),\r\n });\r\n continue;\r\n }\r\n\r\n if (clientSyncResult?.status !== 'success') {\r\n tempSocket.emit(socketEventNames.sync, {\r\n cb: callbackName,\r\n fullName: resolvedName,\r\n ...buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.invalidClientResponse' },\r\n preferred: extractLanguageFromHeader(tempSocket.handshake.headers['accept-language'] || tempSocket.handshake.headers['x-language']),\r\n }),\r\n });\r\n continue;\r\n }\r\n\r\n tempSocket.emit(socketEventNames.sync, {\r\n cb: callbackName,\r\n fullName: resolvedName,\r\n serverOutput,\r\n clientOutput: clientSyncResult,\r\n message: clientSyncResult.message || `${resolvedName} sync success`,\r\n status: 'success',\r\n });\r\n recipientCount++;\r\n continue;\r\n }\r\n\r\n tempSocket.emit(socketEventNames.sync, {\r\n cb: callbackName,\r\n fullName: resolvedName,\r\n serverOutput,\r\n clientOutput: {},\r\n message: `${resolvedName} sync success`,\r\n status: 'success',\r\n });\r\n recipientCount++;\r\n }\r\n\r\n fanoutPayload.recipientCount = recipientCount;\r\n await dispatchHook('postSyncFanout', fanoutPayload);\r\n\r\n if (shouldLogDev()) {\r\n getLogger().debug(`http sync: ${resolvedName} completed`);\r\n }\r\n\r\n //? Flatten the server handler's `serverOutput` into the HTTP success\r\n //? envelope (mirroring how `handleHttpApiRequest` spreads `result`), so\r\n //? callers over HTTP/SSE receive the route's own fields (tokenCount,\r\n //? completedSteps, message, …) — not just a generic success line.\r\n //? `serverOutput` is statically `{}` here (its real shape is route-defined),\r\n //? so guarantee HttpSyncResponse's required `message` while still preferring\r\n //? the route's own message when it supplied one.\r\n const serverMessage = (serverOutput as { message?: unknown }).message;\r\n return {\r\n ...serverOutput,\r\n status: 'success' as const,\r\n message: typeof serverMessage === 'string' ? serverMessage : `${resolvedName} sync success`,\r\n };\r\n });\r\n if (bodyError) {\r\n getLogger().error(`http sync: ${name} threw`, bodyError, { sync: name });\r\n return buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.serverExecutionFailed' },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n });\r\n }\r\n return bodyResult ?? buildSyncError({\r\n response: { status: 'error', errorCode: 'sync.serverExecutionFailed' },\r\n preferred: preferredLocale,\r\n userLanguage: user?.language,\r\n });\r\n}\r\n","import { getProjectConfig } from '@luckystack/core';\r\n\r\n//? Stream-chunk throttle. Coalesces tiny pieces of data (e.g. AI-provider\r\n//? tokens of 3-10 characters each) into bigger chunks before they're sent\r\n//? over the wire, cutting message count by 10-100x with no perceptible\r\n//? latency hit.\r\n//?\r\n//? Two flush triggers:\r\n//? - `flushAtChars` — buffered length crossed the byte threshold.\r\n//? - `flushEveryMs` — the throttle's internal timer fired.\r\n//?\r\n//? Whichever happens first flushes the buffer. The author calls `push(text, emit)`\r\n//? in the AI loop and `flush(emit)` once after the loop finishes. The `emit`\r\n//? argument is whichever stream callback the route is using (`stream`,\r\n//? `broadcastStream`, `streamTo` partial, etc.) — the throttle stays\r\n//? agnostic so it works with any of the three primitives.\r\n\r\nexport interface CreateStreamThrottleOptions {\r\n /**\r\n * Flush buffered text once it crosses this many characters. Default: 32.\r\n * Lower = more updates, more network traffic.\r\n * Higher = fewer updates, choppier UI.\r\n */\r\n flushAtChars?: number;\r\n /**\r\n * Flush buffered text after this many milliseconds even if the char\r\n * threshold hasn't been hit. Default: 50ms — fast enough that the user\r\n * perceives \"live typing\", slow enough that 50–100 tokens batch into a\r\n * single message on a fast LLM stream.\r\n *\r\n * Set to `false` to disable the timer (only flush at char threshold or\r\n * on explicit `flush()`).\r\n */\r\n flushEveryMs?: number | false;\r\n /**\r\n * Field name on the emitted payload that carries the buffered text.\r\n * Default: `'chunk'`. Override if your stream payload uses a different\r\n * key (e.g. `'text'`, `'delta'`).\r\n */\r\n field?: string;\r\n}\r\n\r\nexport interface StreamThrottle {\r\n /** Append text to the buffer. May trigger a flush. */\r\n push: (text: string, emit: (payload: Record<string, unknown>) => void) => void;\r\n /** Force-flush whatever is in the buffer right now. Call after the source loop ends. */\r\n flush: (emit: (payload: Record<string, unknown>) => void) => void;\r\n /** Drop the buffered text without emitting. Useful on abort. */\r\n reset: () => void;\r\n}\r\n\r\n/**\r\n * Build a chunk throttle for streaming use cases. Designed for LLM token\r\n * streams where the provider yields very small pieces (3–10 chars) and you\r\n * don't want to send a separate socket message for each one.\r\n *\r\n * @example\r\n * const throttle = createStreamThrottle({ flushEveryMs: 50, flushAtChars: 32 });\r\n * for await (const chunk of openaiStream) {\r\n * throttle.push(chunk.text, broadcastStream);\r\n * }\r\n * throttle.flush(broadcastStream);\r\n */\r\nexport const createStreamThrottle = (\r\n options: CreateStreamThrottleOptions = {},\r\n): StreamThrottle => {\r\n const defaults = getProjectConfig().sync.streamThrottle;\r\n const flushAtChars = options.flushAtChars ?? defaults.flushAtChars;\r\n const flushEveryMs = options.flushEveryMs ?? defaults.flushEveryMs;\r\n const field = options.field ?? defaults.field;\r\n\r\n let buffer = '';\r\n let timer: ReturnType<typeof setTimeout> | null = null;\r\n\r\n const clearTimer = () => {\r\n if (timer !== null) {\r\n clearTimeout(timer);\r\n timer = null;\r\n }\r\n };\r\n\r\n const flushNow = (emit: (payload: Record<string, unknown>) => void) => {\r\n if (buffer.length === 0) {\r\n clearTimer();\r\n return;\r\n }\r\n const payload: Record<string, unknown> = { [field]: buffer };\r\n buffer = '';\r\n clearTimer();\r\n emit(payload);\r\n };\r\n\r\n return {\r\n push: (text, emit) => {\r\n if (!text) return;\r\n buffer += text;\r\n\r\n if (buffer.length >= flushAtChars) {\r\n flushNow(emit);\r\n return;\r\n }\r\n\r\n if (flushEveryMs !== false && timer === null) {\r\n timer = setTimeout(() => {\r\n flushNow(emit);\r\n }, flushEveryMs);\r\n //? Allow process exit even if the throttle has a pending flush.\r\n //? Prevents tests + short scripts from hanging waiting on a timer.\r\n if (typeof timer === 'object' && 'unref' in timer) {\r\n (timer as { unref: () => void }).unref();\r\n }\r\n }\r\n },\r\n flush: (emit) => {\r\n flushNow(emit);\r\n },\r\n reset: () => {\r\n buffer = '';\r\n clearTimer();\r\n },\r\n };\r\n};\r\n"],"mappings":"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","names":["getProjectConfig","getIoInstance","socketEventNames","dispatchHook","getLogger","out","i","getProjectConfig","shouldLogStream","dispatchHook","getIoInstance","getLogger","socketEventNames","getSession","getProjectConfig","validateRequest","extractTokenFromSocket","getIoInstance","tryCatch","parseTransportRouteName","checkRateLimit","socketEventNames","validateInputByType","dispatchHook","getLogger","extractLanguageFromHeader","normalizeErrorResponse","applyErrorFormatter","shouldLogDev","getProjectConfig","shouldLogStream","checkRateLimit","dispatchHook","getLogger","extractLanguageFromHeader","getSession","normalizeErrorResponse","applyErrorFormatter","getIoInstance","tryCatch","parseTransportRouteName","validateRequest","validateInputByType","extractTokenFromSocket","socketEventNames","getProjectConfig"]}
@@ -0,0 +1,257 @@
1
+ # callback-registration
2
+
3
+ > How recipients subscribe to sync events on the client. Covers `useSyncEvents` (the React hook for component-scoped callbacks), the `upsertSyncEventCallback` / `upsertSyncEventStreamCallback` registration APIs, `useSyncEventTrigger` (manual local fire for testing), and `initSyncRequest` (one-time wiring of socket lifecycle handlers to the socket-status provider).
4
+
5
+ For the originator side see [`./sync-request.md`](./sync-request.md). For streaming payload shapes see [`./streaming.md`](./streaming.md).
6
+
7
+ ---
8
+
9
+ ## 1. `useSyncEvents()` — the component-scoped hook
10
+
11
+ ```ts
12
+ import { useSyncEvents } from '@luckystack/sync/client';
13
+
14
+ const { upsertSyncEventCallback, upsertSyncEventStreamCallback } = useSyncEvents();
15
+ ```
16
+
17
+ Returns two stable (`useCallback`-wrapped) registrar functions. The hook also installs a cleanup `useEffect` that removes every callback registered through this hook instance on unmount.
18
+
19
+ Key properties:
20
+
21
+ - **Component-scoped local registry.** Each `useSyncEvents()` call holds its own `Map<fullName, Callback>` so the cleanup effect only removes callbacks added by that component, not callbacks other components registered for the same route.
22
+ - **Automatic deduplication of same-key re-registration.** Calling `upsertSyncEventCallback` twice with the same name+version from the same component replaces the previous callback (idempotent). Calling it from different components adds independent entries.
23
+ - **`useCallback`-stable identity.** The registrar functions don't change identity between renders, so dependency arrays (`useEffect(..., [upsertSyncEventCallback])`) don't churn.
24
+
25
+ ### Typical usage
26
+
27
+ ```tsx
28
+ import { useEffect } from 'react';
29
+ import { useSyncEvents } from '@luckystack/sync/client';
30
+
31
+ export const BoardSubscriber = () => {
32
+ const { upsertSyncEventCallback } = useSyncEvents();
33
+
34
+ useEffect(() => {
35
+ const unsubscribe = upsertSyncEventCallback({
36
+ name: 'board/moveCard',
37
+ version: 'v1',
38
+ callback: ({ serverOutput, clientOutput }) => {
39
+ if (serverOutput.status !== 'success') return;
40
+ setCards(prev => moveCardLocally(prev, serverOutput.cardId));
41
+ },
42
+ });
43
+ return unsubscribe;
44
+ }, [upsertSyncEventCallback]);
45
+
46
+ return null;
47
+ };
48
+ ```
49
+
50
+ The returned `unsubscribe` lets you cancel before the component unmounts — useful when the subscription is conditional on props or routing.
51
+
52
+ ---
53
+
54
+ ## 2. `upsertSyncEventCallback({ name, version, callback })`
55
+
56
+ Subscribes to **non-stream** sync payloads — the `{ serverOutput, clientOutput, status: 'success' }` frames that `handleSyncRequest` emits at the end of the per-recipient loop.
57
+
58
+ ```ts
59
+ upsertSyncEventCallback<F, V>(params: {
60
+ name: F; // SyncFullName literal, typed
61
+ version: V; // VersionsForFullName<F> literal
62
+ callback: ({
63
+ clientOutput: ClientOutputForFullName<F, V>,
64
+ serverOutput: ServerOutputForFullName<F, V>,
65
+ }) => void;
66
+ }): () => void // returns unsubscribe function
67
+ ```
68
+
69
+ ### Typed payloads (rule 16, no casts)
70
+
71
+ Both `clientOutput` and `serverOutput` are typed via the generated `SyncTypeMap`. The framework's discriminated unions surface every possible status:
72
+
73
+ ```ts
74
+ upsertSyncEventCallback({
75
+ name: 'board/moveCard',
76
+ version: 'v1',
77
+ callback: ({ serverOutput, clientOutput }) => {
78
+ // serverOutput is the typed union (success | error variants) from the generated map.
79
+ if (serverOutput.status !== 'success') return;
80
+ // After the narrowing, TS knows the success-shape fields are available.
81
+ console.log(serverOutput.cardId, serverOutput.movedBy);
82
+ },
83
+ });
84
+ ```
85
+
86
+ Never cast these to `unknown` or `any` (root `CLAUDE.md` rule 16). If inference fails, regenerate the type map (`npm run ai:index`) — that's the source of truth.
87
+
88
+ ### Validation rules
89
+
90
+ `upsertSyncEventCallback` rejects (returns the `noop` unsubscriber, logs in dev mode) when:
91
+
92
+ - `params.version` isn't a string -> `sync.invalidVersion`.
93
+ - `params.callback` isn't a function -> `sync.invalidCallback`.
94
+ - `params.name` isn't a valid service route name -> `routing.invalidServiceRouteName`.
95
+
96
+ These are dev-time guards. In production the runtime errors surface only if generated types are mis-applied at the call site.
97
+
98
+ ### Multiple components subscribing to the same route
99
+
100
+ Supported and intentional. Each component's registrar pushes a separate callback into the route's array; `triggerSyncCallbacks` iterates and fires each. Cleanup is per-callback — unmounting Component A doesn't affect Component B's subscription.
101
+
102
+ ### Duplicate callback handling (same reference)
103
+
104
+ If the **exact same callback function reference** is registered twice (rare — usually a bug where `useEffect` runs without dependency-checking), the registrar warns in dev mode and ignores the second add. The local registry still tracks the callback so the cleanup works. This guards against runaway callback arrays without breaking legitimate multi-component subscriptions.
105
+
106
+ ---
107
+
108
+ ## 3. `upsertSyncEventStreamCallback({ name, version, callback })`
109
+
110
+ Subscribes to **stream chunks** — the `{ status: 'stream', cb, fullName, ...payload }` frames emitted via `broadcastStream`, `streamTo`, or `_client.stream(...)`.
111
+
112
+ ```ts
113
+ upsertSyncEventStreamCallback<F, V>(params: {
114
+ name: F;
115
+ version: V;
116
+ callback: ({ stream }: { stream: SyncRouteStreamEvent<...> }) => void;
117
+ }): () => void
118
+ ```
119
+
120
+ The `stream` parameter type is the union of:
121
+
122
+ - The route's `serverStream` type (from `broadcastStream` and `streamTo` payloads).
123
+ - The route's `clientStream` type (from per-recipient `_client.stream(payload)`).
124
+
125
+ If neither side ever streams, the callback type collapses to `never` and TS rejects the registration call at compile time — a compile-time guarantee that you can't subscribe to a non-streaming route.
126
+
127
+ ```ts
128
+ upsertSyncEventStreamCallback({
129
+ name: 'chat/sendMessage',
130
+ version: 'v1',
131
+ callback: ({ stream }) => {
132
+ if ('chunk' in stream) appendToken(stream.chunk);
133
+ else if (stream.status === 'started') showTypingIndicator(stream.author);
134
+ else if (stream.status === 'done') hideTypingIndicator();
135
+ },
136
+ });
137
+ ```
138
+
139
+ Stream callbacks run for **every chunk** of every fanout to this recipient — there is no per-request scoping. Use `onStream` on `syncRequest` instead when you want per-request scoping for originator-targeted chunks.
140
+
141
+ The same dedup / multi-subscriber / unsubscribe semantics from `upsertSyncEventCallback` apply.
142
+
143
+ ---
144
+
145
+ ## 4. `useSyncEventTrigger()` — manual local fire
146
+
147
+ ```ts
148
+ const { triggerSyncEvent, triggerSyncStreamEvent } = useSyncEventTrigger();
149
+
150
+ triggerSyncEvent('sync/board/moveCard/v1', { /* clientOutput */ }, { /* serverOutput */ });
151
+ triggerSyncStreamEvent('sync/board/moveCard/v1', { chunk: 'partial' });
152
+ ```
153
+
154
+ Bypasses the wire entirely. The framework's internal `triggerSyncCallbacks` / `triggerSyncStreamCallbacks` are called directly, firing every registered subscriber as if the payload had arrived from the server.
155
+
156
+ Use cases:
157
+
158
+ - **Local testing** — fire a fake payload to verify your UI subscriber works without spinning up the server pipeline.
159
+ - **Optimistic local echo** — apply a payload to your own UI without sending it through the network (rare — usually optimistic UI just mutates local state directly, no need to round-trip the sync system).
160
+ - **Storybook / fixtures** — drive subscriber components with deterministic payloads.
161
+
162
+ The argument names are different from the wire format:
163
+
164
+ - First argument: the **full route name** (`sync/<page>/<name>/v<N>`). The `sync/` prefix is part of the internal routing key.
165
+ - Second argument: `clientOutput`.
166
+ - Third argument: `serverOutput`.
167
+
168
+ Note: the parameter order is `(name, clientOutput, serverOutput)` — opposite of what you might expect from reading the wire frame, which lists `serverOutput` first. The internal callback signature is `{ clientOutput, serverOutput }` (object) but the trigger helper passes positionally for ergonomics.
169
+
170
+ ---
171
+
172
+ ## 5. `initSyncRequest({ setSocketStatus, sessionRef })`
173
+
174
+ One-time wiring of socket lifecycle events to a status setter. Called at app boot from `SocketStatusProvider`. Not consumed by app code directly.
175
+
176
+ ```ts
177
+ initSyncRequest({
178
+ setSocketStatus, // Dispatch from useState<{ self, [userId]: statusContent }>
179
+ sessionRef, // RefObject<SessionLayout | null>
180
+ });
181
+ ```
182
+
183
+ Wires the following Socket.io events:
184
+
185
+ | Socket event | Action |
186
+ |---|---|
187
+ | `connect` | `setSocketStatus(prev => ({ ...prev, self: { ...self, status: 'CONNECTED' } }))` |
188
+ | `disconnect` | `setSocketStatus(prev => ({ ...prev, self: { ...self, status: 'DISCONNECTED' } }))` |
189
+ | `reconnectAttempt(attempt)` | Status `'RECONNECTING'` with `reconnectAttempt` set to the attempt number. |
190
+ | `userAfk({ userId, endTime })` | If `userId === session.id`, mark `self` disconnected with `endTime`. Otherwise mark the user as disconnected in the per-user map. |
191
+ | `userBack({ userId })` | Mark the user as `'CONNECTED'`, clear `endTime`. |
192
+ | `connect_error(err)` | Mark `self` disconnected, log + notify in dev mode. |
193
+
194
+ Idempotent: if `initSyncRequest` is called again later, the previous handlers are removed before new ones are attached (the module holds `activeLifecycleHandlers` for this). Safe to call across hot-reloads.
195
+
196
+ `sessionRef` is a `RefObject` (not a value) because the handlers must always read the **current** session at event-firing time, not the session at handler-construction time. Without the ref, a logout followed by a `userAfk` event would still treat the AFK as if it belonged to the old user.
197
+
198
+ ---
199
+
200
+ ## 6. Why callbacks must never cast `serverOutput` / `clientOutput`
201
+
202
+ Rule 16 in the repo root `CLAUDE.md`: generated types are mandatory; no `unknown` / `any` casts on typed transports.
203
+
204
+ Both `upsertSyncEventCallback` and `upsertSyncEventStreamCallback` give you fully typed payloads via the generated `SyncTypeMap`. The framework's discriminated unions handle the status branching cleanly:
205
+
206
+ ```ts
207
+ // GOOD — uses the generated discriminated union
208
+ callback: ({ serverOutput }) => {
209
+ if (serverOutput.status !== 'success') return;
210
+ console.log(serverOutput.cardId); // typed
211
+ }
212
+
213
+ // BAD — defeats inference, breaks contract evolution
214
+ callback: ({ serverOutput }: any) => {
215
+ console.log((serverOutput as { cardId: string }).cardId);
216
+ }
217
+
218
+ // BAD — same problem, different syntax
219
+ const data = serverOutput as unknown as { cardId: string };
220
+ ```
221
+
222
+ When inference fails:
223
+
224
+ 1. Regenerate the type map: `npm run ai:index`.
225
+ 2. Verify the `_server_v{N}.ts` return type is well-typed (no `any` in the return path).
226
+ 3. If you still hit a wall, fix the typing source — don't paper over with casts at the call site.
227
+
228
+ ---
229
+
230
+ ## 7. Subscription cleanup checklist
231
+
232
+ - `useSyncEvents()` auto-cleans on component unmount (via `useEffect` cleanup). For most cases, just call the registrars in a `useEffect` and return their unsubscribers. That's belt-and-suspenders but cheap.
233
+ - Calling `upsertSyncEventCallback` outside React (e.g. from a `_provider` module-level subscribe) means you own the cleanup. Save the returned function and call it on teardown.
234
+ - Re-registering with the same name+version from the same component (same `useSyncEvents()` instance) replaces the previous callback — no leak.
235
+ - Re-registering from a **different** component adds an independent entry — both fire.
236
+
237
+ ---
238
+
239
+ ## 8. Quick reference
240
+
241
+ | API | Purpose | Scope |
242
+ |---|---|---|
243
+ | `useSyncEvents()` | React hook returning `{ upsertSyncEventCallback, upsertSyncEventStreamCallback }` | Component lifetime — auto-clean on unmount |
244
+ | `upsertSyncEventCallback({ name, version, callback })` | Subscribe to `{ serverOutput, clientOutput }` frames | Returns unsubscribe |
245
+ | `upsertSyncEventStreamCallback({ name, version, callback })` | Subscribe to stream chunks (`broadcastStream`, `streamTo`, `_client.stream`) | Returns unsubscribe |
246
+ | `useSyncEventTrigger()` | React hook returning `{ triggerSyncEvent, triggerSyncStreamEvent }` for local fire | Component-stable identity |
247
+ | `initSyncRequest({ setSocketStatus, sessionRef })` | One-time wire-up of socket lifecycle to status provider | App boot |
248
+
249
+ ---
250
+
251
+ ## 9. Related
252
+
253
+ - Originator API: [`./sync-request.md`](./sync-request.md)
254
+ - Stream payload shapes: [`./streaming.md`](./streaming.md)
255
+ - Server / client handler contracts (what produces the payloads): [`./server-vs-client-handlers.md`](./server-vs-client-handlers.md)
256
+ - Type-generation contract (rule 16): repo root [`.claude/CLAUDE.md`](../../../.claude/CLAUDE.md)
257
+ - `SocketStatusProvider` wiring: `src/_providers/SocketStatusProvider.tsx` (installer side)