@luckystack/sync 0.2.7 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -1
- package/dist/index.js +63 -28
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/client.js
CHANGED
|
@@ -281,6 +281,7 @@ var syncRequestInternal = (params) => {
|
|
|
281
281
|
if (typeof effectiveTimeoutMs === "number" && effectiveTimeoutMs > 0) {
|
|
282
282
|
responseTimeout = setTimeout(() => {
|
|
283
283
|
responseTimeout = null;
|
|
284
|
+
socketInstance.emit(socketEventNames.syncCancel, { cb: serverCancelId ?? syncCb });
|
|
284
285
|
cleanupProgressListener?.();
|
|
285
286
|
cleanupProgressListener = null;
|
|
286
287
|
cleanupResponseListener?.();
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/syncRequest.ts"],"sourcesContent":["//? All cross-package imports go through `@luckystack/core/client` (the\n//? browser-safe subpath) — never the main `@luckystack/core` barrel which\n//? re-exports server-only modules (paths.ts, db.ts, redis.ts) that must\n//? not enter a Vite client bundle.\nimport type {\n BaseSessionLayout as SessionLayout,\n StreamPayload,\n SyncTypeMap,\n statusContent,\n} from \"@luckystack/core/client\";\nimport {\n getLogger,\n getProjectConfig,\n notify,\n incrementResponseIndex,\n socket,\n waitForSocket,\n enqueueSyncRequest,\n isOnline,\n normalizeErrorResponseCore,\n parseServiceRouteName,\n buildSyncProgressEventName,\n buildSyncResponseEventName,\n socketEventNames,\n} from \"@luckystack/core/client\";\nimport { Dispatch, RefObject, SetStateAction, useCallback, useEffect, useRef } from \"react\";\nimport { Socket } from \"socket.io-client\";\n\nexport type SyncRequestStreamEvent<T extends StreamPayload = StreamPayload> = T;\n\nexport type SyncRouteStreamEvent<T extends StreamPayload = StreamPayload> = T;\n\ntype SyncRequestStreamCallback = (event: SyncRequestStreamEvent) => void;\ntype SyncEventStreamCallback = (params: { stream: SyncRouteStreamEvent }) => void;\n\nconst shouldLogDev = () => getProjectConfig().logging.devLogs;\nconst shouldNotifyDev = () => getProjectConfig().logging.devNotifications;\nconst shouldLogSocketStatus = () => getProjectConfig().logging.socketStatus;\nconst shouldLogStream = () => getProjectConfig().logging.stream;\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Type Helpers for Sync Requests\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\ntype Prettify<T> = { [K in keyof T]: T[K] } & {};\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Global Sync Params\n// ═══════════════════════════════════════════════════════════════════════════════\n\n// All possible sync names across all pages\ntype SyncRouteRecord = UnionToIntersection<{\n [P in keyof SyncTypeMap]: {\n [N in keyof SyncTypeMap[P] as P extends 'root'\n ? `system/${Extract<N, string>}`\n : `${Extract<P, string>}/${Extract<N, string>}`]: SyncTypeMap[P][N]\n }\n}[keyof SyncTypeMap]>;\n\ntype SyncFullName = Extract<keyof SyncRouteRecord, string>;\ntype VersionsForFullName<F extends SyncFullName> = Extract<keyof SyncRouteRecord[F], string>;\n\ntype ClientInputForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = SyncRouteRecord[F][V] extends { clientInput: infer I }\n ? I\n : never;\n\ntype ServerOutputForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = SyncRouteRecord[F][V] extends { serverOutput: infer O }\n ? O\n : never;\n\ntype ClientOutputForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = SyncRouteRecord[F][V] extends { clientOutput: infer O }\n ? O\n : never;\n\ntype ServerStreamForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = SyncRouteRecord[F][V] extends { serverStream: infer O }\n ? O\n : never;\n\ntype ClientStreamForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = SyncRouteRecord[F][V] extends { clientStream: infer O }\n ? O\n : never;\n\ntype SyncRequestStreamCallbackForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> =\n [ServerStreamForFullName<F, V>] extends [never]\n ? never\n : (event: SyncRequestStreamEvent<Prettify<ServerStreamForFullName<F, V> extends StreamPayload ? ServerStreamForFullName<F, V> : StreamPayload>>) => void;\n\n//? Recipients see chunks from THREE sources, all on the same wire channel:\n//? 1. `_client_v{n}.ts`'s `stream(...)` (per-recipient, runs after _server)\n//? 2. `_server_v{n}.ts`'s `broadcastStream(...)` (room-wide fan-out)\n//? 3. `_server_v{n}.ts`'s `streamTo(tokens, ...)` (selective fan-out)\n//? All three flow into `upsertSyncEventCallback`'s `stream` argument. Both\n//? serverStream and clientStream are folded into the union so the callback\n//? sees every shape the route can emit. If neither side ever streams, the\n//? callback type collapses to never (compile error to register one).\ntype CombinedRouteStream<F extends SyncFullName, V extends VersionsForFullName<F>> =\n | (ClientStreamForFullName<F, V> extends never ? never : ClientStreamForFullName<F, V>)\n | (ServerStreamForFullName<F, V> extends never ? never : ServerStreamForFullName<F, V>);\n\ntype SyncRouteStreamCallbackForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> =\n [CombinedRouteStream<F, V>] extends [never]\n ? never\n : (params: { stream: SyncRouteStreamEvent<Prettify<CombinedRouteStream<F, V> extends StreamPayload ? CombinedRouteStream<F, V> : StreamPayload>> }) => void;\n\ntype SyncParamsForFullName<\n F extends SyncFullName,\n V extends VersionsForFullName<F>\n> = DataRequired<ClientInputForFullName<F, V>> extends true\n ? {\n name: F;\n version: V;\n data: ClientInputForFullName<F, V>;\n receiver: string;\n ignoreSelf?: boolean;\n onStream?: SyncRequestStreamCallbackForFullName<F, V>;\n /**\n * Per-request override of `projectConfig.offlineQueue.dropPolicy`. Lets a\n * specific sync (\"editor cursor move\") pick `'drop-oldest'` while the\n * app default stays `'reject'` for safer sends. When omitted, falls back\n * to the global config.\n */\n offlineDropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';\n /**\n * Optional AbortSignal. When aborted the client emits `syncCancel { cb }`\n * to the server and resolves locally with\n * `{ status: 'error', errorCode: 'request.aborted' }`.\n */\n signal?: AbortSignal;\n }\n : {\n name: F;\n version: V;\n data?: ClientInputForFullName<F, V>;\n receiver: string;\n ignoreSelf?: boolean;\n onStream?: SyncRequestStreamCallbackForFullName<F, V>;\n /** Per-request override (see typed branch). */\n offlineDropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';\n /** Optional AbortSignal — see typed branch. */\n signal?: AbortSignal;\n };\n\ninterface RuntimeSyncParams {\n name?: string;\n version?: string;\n data?: unknown;\n receiver?: string;\n ignoreSelf?: boolean;\n onStream?: SyncRequestStreamCallback;\n offlineDropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';\n signal?: AbortSignal;\n}\n\ninterface SyncErrorParam { key: string; value: string | number | boolean };\n\ninterface SyncResponseError {\n status: 'error';\n message: string;\n errorCode: string;\n errorParams?: SyncErrorParam[];\n httpStatus?: number;\n}\n\ninterface SyncAckResponse {\n status?: 'success' | 'error';\n message?: string;\n result?: unknown;\n errorCode?: string;\n errorParams?: SyncErrorParam[];\n httpStatus?: number;\n}\n\ntype SyncResultForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> =\n [ServerOutputForFullName<F, V>] extends [never]\n ? Record<string, never>\n : ServerOutputForFullName<F, V>;\n\ntype SyncRequestResponseForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> =\n | SyncResponseError\n | {\n status: 'success';\n message: string;\n result: SyncResultForFullName<F, V>;\n };\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Sync Event Callbacks Registry\n// ═══════════════════════════════════════════════════════════════════════════════\n\ntype SyncEventCallback = (params: { clientOutput: unknown; serverOutput: unknown }) => void;\nconst syncEvents: Partial<Record<string, SyncEventCallback[]>> = {};\nconst syncStreamEvents: Partial<Record<string, SyncEventStreamCallback[]>> = {};\nconst noop = () => null;\n\ninterface SyncLifecycleHandlers {\n connect: () => void;\n disconnect: () => void;\n reconnectAttempt: (attempt: number) => void;\n userAfk: (payload: { userId: string; endTime?: number }) => void;\n userBack: (payload: { userId: string }) => void;\n connectError: (err: { message: string }) => void;\n}\n\nlet activeLifecycleHandlers: SyncLifecycleHandlers | null = null;\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\nconst getCallbacksForRoute = (route: string): SyncEventCallback[] => {\n syncEvents[route] ??= [];\n return syncEvents[route];\n};\n\nconst getStreamCallbacksForRoute = (route: string): SyncEventStreamCallback[] => {\n syncStreamEvents[route] ??= [];\n return syncStreamEvents[route];\n};\n\nconst triggerSyncCallbacks = (name: string, clientOutput: unknown, serverOutput: unknown) => {\n const callbacks = syncEvents[name] ?? [];\n if (callbacks.length === 0) {\n //? Streaming-only routes legitimately register ONLY a stream callback —\n //? the final success envelope is implicit in the stream completing. Only\n //? warn when NEITHER callback type is registered for the route.\n const streamCallbacks = syncStreamEvents[name] ?? [];\n if (streamCallbacks.length === 0 && shouldLogDev()) {\n getLogger().warn(`Sync event ${name} has no registered callback on this page`);\n }\n return;\n }\n\n for (const callback of callbacks) {\n callback({ clientOutput, serverOutput });\n }\n};\n\nconst triggerSyncStreamCallbacks = (name: string, stream: SyncRouteStreamEvent) => {\n const callbacks = syncStreamEvents[name] ?? [];\n if (callbacks.length === 0) {\n return;\n }\n\n for (const callback of callbacks) {\n callback({ stream });\n }\n};\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// syncRequest Function Overloads\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst normalizeSyncError = ({\n response,\n fallbackErrorCode,\n}: {\n response: SyncAckResponse;\n fallbackErrorCode: string;\n}): SyncResponseError => {\n const normalized = normalizeErrorResponseCore({\n response,\n fallbackErrorCode,\n resolveMessage: ({ errorCode }) => {\n if (typeof response.message === 'string' && response.message.trim().length > 0) {\n return response.message;\n }\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,\n };\n};\n\ntype SyncRequestParamsWithOptions<F extends SyncFullName, V extends VersionsForFullName<F>> =\n SyncParamsForFullName<F, V>;\n\n//? Outcome of the pre-flight validation phase of a sync send. Either a\n//? normalized error envelope to resolve the request with, or the sanitized\n//? values the send phase needs. Extracted from `syncRequestInternal` so the\n//? \"validate first, execute second\" split is explicit (the orchestrator stays\n//? a thin promise wrapper around prepare → run).\ntype PreparedSyncRequest =\n | { ok: false; error: SyncResponseError }\n | { ok: true; sanitizedName: string; version: string; data: object; normalizedReceiver: string };\n\n//? Pure-ish validation chain (logs + dev-notifies on failure, same as the\n//? previous inlined block). Async because the socket-readiness gate\n//? (`waitForSocket`) is awaited last, after the cheap structural checks. Each\n//? failure path returns the SAME normalized error envelope the inlined code\n//? resolved with — behaviour-identical.\nconst prepareSyncRequest = async ({\n name,\n version,\n receiver,\n payloadData,\n}: {\n name: unknown;\n version: unknown;\n receiver: unknown;\n payloadData: unknown;\n}): Promise<PreparedSyncRequest> => {\n if (!name || typeof name !== \"string\") {\n if (shouldLogDev()) {\n getLogger().error(\"Invalid name for syncRequest\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'sync.invalidName' });\n }\n return { ok: false, error: normalizeSyncError({\n response: { status: 'error', errorCode: 'sync.invalidName' },\n fallbackErrorCode: 'sync.invalidName',\n }) };\n }\n\n const parsedRoute = parseServiceRouteName(name);\n if (parsedRoute.status === 'error') {\n if (shouldLogDev()) {\n getLogger().error(`[syncRequest] Invalid service route name '${name}'`, undefined, { reason: parsedRoute.reason });\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'routing.invalidServiceRouteName' });\n }\n return { ok: false, error: normalizeSyncError({\n response: {\n status: 'error',\n errorCode: 'routing.invalidServiceRouteName',\n errorParams: [{ key: 'name', value: name }],\n },\n fallbackErrorCode: 'routing.invalidServiceRouteName',\n }) };\n }\n\n const sanitizedName = parsedRoute.normalizedRouteName;\n\n const data = payloadData && typeof payloadData === \"object\" ? payloadData : {};\n\n if (!version || typeof version !== 'string') {\n if (shouldLogDev()) {\n getLogger().error(\"Invalid version for syncRequest\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'sync.invalidVersion' });\n }\n return { ok: false, error: normalizeSyncError({\n response: { status: 'error', errorCode: 'sync.invalidVersion' },\n fallbackErrorCode: 'sync.invalidVersion',\n }) };\n }\n\n const normalizedReceiver = typeof receiver === 'string' ? receiver.trim() : '';\n\n if (!normalizedReceiver) {\n if (shouldLogDev()) {\n getLogger().error(\"You need to provide a receiver for syncRequest, this can be either 'all' to trigger all sockets which we do not recommend or it can be any value such as a code e.g 'Ag2cg4'. this works together with the joinRoom and leaveRoom function\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'sync.missingReceiver' });\n }\n return { ok: false, error: normalizeSyncError({\n response: { status: 'error', errorCode: 'sync.missingReceiver' },\n fallbackErrorCode: 'sync.missingReceiver',\n }) };\n }\n\n if (!await waitForSocket()) {\n return { ok: false, error: normalizeSyncError({\n response: { status: 'error', errorCode: 'sync.ioUnavailable' },\n fallbackErrorCode: 'sync.ioUnavailable',\n }) };\n }\n if (!socket) {\n return { ok: false, error: normalizeSyncError({\n response: { status: 'error', errorCode: 'sync.ioUnavailable' },\n fallbackErrorCode: 'sync.ioUnavailable',\n }) };\n }\n\n return { ok: true, sanitizedName, version, data, normalizedReceiver };\n};\n\nconst syncRequestInternal = <F extends SyncFullName, V extends VersionsForFullName<F>>(\n params: SyncRequestParamsWithOptions<F, V>\n): Promise<Prettify<SyncRequestResponseForFullName<F, V>>> => {\n const runtimeParams = params as RuntimeSyncParams;\n const { name, version, receiver, ignoreSelf, onStream, offlineDropPolicy, signal: externalSignal } = runtimeParams;\n const payloadData = runtimeParams.data;\n\n type RequestOutput = Prettify<SyncRequestResponseForFullName<F, V>>;\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.\n if (externalSignal?.aborted) {\n resolve(normalizeSyncError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }));\n return;\n }\n\n const prepared = await prepareSyncRequest({ name, version, receiver, payloadData });\n if (!prepared.ok) {\n resolve(prepared.error);\n return;\n }\n const { sanitizedName, version: resolvedVersion, data, normalizedReceiver } = prepared;\n\n //? Re-narrow `socket` for the closures below. `prepareSyncRequest`\n //? already guaranteed it is non-null (returns an `ioUnavailable` error\n //? otherwise), but that guard lives in another function so TS can't\n //? carry the narrowing across the call boundary.\n const activeSocket = socket;\n if (!activeSocket) {\n resolve(normalizeSyncError({\n response: { status: 'error', errorCode: 'sync.ioUnavailable' },\n fallbackErrorCode: 'sync.ioUnavailable',\n }));\n return;\n }\n\n const fullName = `sync/${sanitizedName}/${resolvedVersion}`;\n let queueId: string | null = null;\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 //? `enqueueSyncRequest`. 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 //? `enqueueSyncRequest` returns `false` when the offline queue is\n //? full and `dropPolicy: 'reject'` is active. Resolve with a\n //? normalized error envelope so the caller can branch instead of\n //? awaiting forever.\n const enqueued = enqueueSyncRequest({\n id: queueId,\n key: fullName,\n run: (s) => {\n runRequest(s);\n },\n createdAt: Date.now(),\n dropPolicy: offlineDropPolicy,\n //? SYNC-09 — settle the awaiting caller when this QUEUED request is\n //? later EVICTED (drop-oldest by a future enqueue / age expiry)\n //? instead of ever running. Without it the promise would hang\n //? forever, since `run` is the only thing that could otherwise\n //? resolve it. The default `offlineQueue.dropPolicy` is\n //? `'drop-oldest'`, so async eviction is the NORMAL overflow path\n //? for high-churn syncs (cursor moves), not an edge case.\n onDrop: (reason) => {\n if (suppressOnDrop) return;\n resolve(normalizeSyncError({\n response: { status: 'error', errorCode: 'offline.dropped', errorParams: [{ key: 'reason', value: reason }] },\n fallbackErrorCode: 'offline.dropped',\n }));\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(normalizeSyncError({\n response: { status: 'error', errorCode: 'offline.queueFull' },\n fallbackErrorCode: 'offline.queueFull',\n }));\n }\n return;\n }\n\n const tempIndex = incrementResponseIndex();\n\n let cleanupProgressListener: (() => void) | null = null;\n //? CORE-06 — ack-timeout. Armed right after the request is emitted so a\n //? lost acknowledgement (server restart/crash between emit and reply)\n //? settles the promise with `sync.requestTimeout` (504) instead of\n //? hanging the awaiting caller forever. Mirrors `apiRequest`'s\n //? `api.requestTimeoutMs` wiring.\n let responseTimeout: ReturnType<typeof setTimeout> | null = null;\n const clearResponseTimeout = () => {\n if (responseTimeout) {\n clearTimeout(responseTimeout);\n responseTimeout = null;\n }\n };\n\n if (shouldLogDev()) {\n getLogger().debug(`Client Sync Request(${String(tempIndex)})`, { syncName: sanitizedName, data, receiver: normalizedReceiver, ignoreSelf });\n }\n\n //? S13 — server-issued cancel id. The server emits a `{ __cancelId }`\n //? handshake frame on the progress channel before any work runs; we\n //? capture it so an abort can target THIS exact request (not a callback\n //? name reused across concurrent same-route requests, which corrupted\n //? the server-side abort registry). The progress listener is ALWAYS\n //? registered now — even without `onStream` — so the handshake is caught\n //? on non-streaming routes too. The reserved `__cancelId` frame is\n //? filtered out of `onStream` delivery.\n let serverCancelId: string | null = null;\n const progressEventName = buildSyncProgressEventName(tempIndex);\n const progressListener = (streamPayload: SyncRequestStreamEvent & { __cancelId?: string }) => {\n if (typeof streamPayload.__cancelId === 'string') {\n serverCancelId = streamPayload.__cancelId;\n return;\n }\n\n if (typeof onStream !== 'function') return;\n\n if (shouldLogStream()) {\n getLogger().debug(`Server Sync Stream(${String(tempIndex)})`, { syncName: sanitizedName, streamPayload });\n }\n\n onStream(streamPayload);\n };\n\n socketInstance.on(progressEventName, progressListener);\n cleanupProgressListener = () => {\n socketInstance.off(progressEventName, progressListener);\n };\n\n //? Fallback cancel key for the narrow window before the `__cancelId`\n //? handshake frame arrives (abort fired immediately on a slow link).\n //? Best-effort only — once the handshake lands `serverCancelId` is the\n //? authoritative, per-request-unique key the server registered under.\n const syncCb = `${sanitizedName}/${resolvedVersion}`;\n socketInstance.emit(socketEventNames.sync, { name: fullName, data, cb: syncCb, receiver: normalizedReceiver, responseIndex: tempIndex, ignoreSelf });\n\n //? B1 / S13 — bridge the consumer's AbortSignal to the server. When the\n //? signal fires we emit `syncCancel { cb: <serverCancelId> }` so the\n //? server-side handler stops emitting new chunks. We also resolve\n //? locally with `request.aborted` so the awaiting caller settles.\n const responseEventName = buildSyncResponseEventName(tempIndex);\n //? Forward-declared so the abort/timeout paths can proactively `off()`\n //? the lingering ack listener instead of waiting for a reply that was\n //? cancelled / will never arrive.\n let cleanupResponseListener: (() => void) | null = null;\n\n let cleanupExternalAbort: (() => void) | null = null;\n if (externalSignal) {\n const externalAbortHandler = () => {\n clearResponseTimeout();\n socketInstance.emit(socketEventNames.syncCancel, { cb: serverCancelId ?? syncCb });\n cleanupProgressListener?.();\n cleanupProgressListener = null;\n cleanupResponseListener?.();\n cleanupResponseListener = null;\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n resolve(normalizeSyncError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }));\n };\n externalSignal.addEventListener('abort', externalAbortHandler);\n cleanupExternalAbort = () => {\n externalSignal.removeEventListener('abort', externalAbortHandler);\n };\n }\n\n //? Arm the ack-timeout. `sync.requestTimeoutMs` (default 30000) may be\n //? `false` to disable. On expiry we tear down every listener and settle\n //? with a 504 envelope so the awaiting caller never hangs.\n const effectiveTimeoutMs = getProjectConfig().sync.requestTimeoutMs;\n if (typeof effectiveTimeoutMs === 'number' && effectiveTimeoutMs > 0) {\n responseTimeout = setTimeout(() => {\n responseTimeout = null;\n cleanupProgressListener?.();\n cleanupProgressListener = null;\n cleanupResponseListener?.();\n cleanupResponseListener = null;\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n if (shouldLogDev()) {\n getLogger().error(`Sync ${sanitizedName} timed out`, undefined, { responseIndex: tempIndex });\n }\n resolve(normalizeSyncError({\n response: { status: 'error', errorCode: 'sync.requestTimeout', httpStatus: 504 },\n fallbackErrorCode: 'sync.requestTimeout',\n }));\n }, effectiveTimeoutMs);\n }\n\n const responseListener = (responseData: SyncAckResponse) => {\n clearResponseTimeout();\n cleanupProgressListener?.();\n cleanupProgressListener = null;\n cleanupResponseListener = null;\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n\n if (responseData.status === \"error\") {\n const normalizedError = normalizeSyncError({\n response: responseData,\n fallbackErrorCode: 'sync.failedRequest',\n });\n\n if (shouldLogDev()) {\n getLogger().error(`Sync ${sanitizedName} failed`, undefined, { message: normalizedError.message });\n }\n if (shouldNotifyDev()) {\n notify.error({\n key: 'sync.failedRequest',\n params: [\n { key: 'name', value: sanitizedName },\n { key: 'message', value: normalizedError.message },\n ],\n });\n }\n resolve(normalizedError);\n return;\n }\n\n if (responseData.status !== 'success') {\n resolve(normalizeSyncError({\n response: responseData,\n fallbackErrorCode: 'sync.invalidServerResponse',\n }));\n return;\n }\n\n const result = responseData.result && typeof responseData.result === 'object'\n ? responseData.result\n : {};\n\n //? `RequestOutput.result` is a generic `Record<string, never>` shape\n //? that the runtime response can't structurally satisfy from the\n //? untyped `responseData.result` (server-side typing lives on the\n //? other side of the socket). The cast is the documented socket\n //? response boundary.\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- socket response boundary\n resolve({\n status: 'success',\n message: typeof responseData.message === 'string' && responseData.message.trim().length > 0\n ? responseData.message\n : `sync ${sanitizedName} success`,\n result,\n } as RequestOutput);\n };\n\n socketInstance.once(responseEventName, responseListener);\n cleanupResponseListener = () => {\n socketInstance.off(responseEventName, responseListener);\n };\n };\n\n runRequest(activeSocket);\n })();\n });\n};\n\nexport function syncRequest<F extends SyncFullName, V extends VersionsForFullName<F>>(\n params: SyncRequestParamsWithOptions<F, V>\n): Promise<Prettify<SyncRequestResponseForFullName<F, V>>> {\n return syncRequestInternal(params);\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// useSyncEvents Hook - Type-Safe Event Registration\n// ═══════════════════════════════════════════════════════════════════════════════\n\n//? Hoisted to module scope so the emitted `.d.ts` for `useSyncEvents` can\n//? reference them. Local interface declarations inside an exported function\n//? trigger TS4025 (\"has or is using private name\") under `declaration: true`.\ninterface TypedCallbackParams<F extends SyncFullName, V extends VersionsForFullName<F>> {\n clientOutput: ClientOutputForFullName<F, V>;\n serverOutput: ServerOutputForFullName<F, V>;\n}\n\ninterface UpsertParams<F extends SyncFullName, V extends VersionsForFullName<F>> {\n name: F;\n version: V;\n callback: (params: TypedCallbackParams<F, V>) => void;\n}\n\ninterface UpsertStreamParams<F extends SyncFullName, V extends VersionsForFullName<F>> {\n name: F;\n version: V;\n callback: SyncRouteStreamCallbackForFullName<F, V>;\n}\n\nexport const useSyncEvents = () => {\n const localRegistryRef = useRef<Map<string, SyncEventCallback>>(new Map());\n const localStreamRegistryRef = useRef<Map<string, SyncEventStreamCallback>>(new Map());\n\n const upsertSyncEventCallback = useCallback(<F extends SyncFullName, V extends VersionsForFullName<F>>(\n params: UpsertParams<F, V>\n ): (() => void) => {\n\n if (typeof params.version !== 'string') {\n if (shouldLogDev()) {\n getLogger().error(\"Invalid version for upsertSyncEventCallback\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'sync.invalidVersion' });\n }\n return noop;\n }\n\n if (typeof params.callback !== 'function') {\n if (shouldLogDev()) {\n getLogger().error(\"Invalid callback for upsertSyncEventCallback\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'sync.invalidCallback' });\n }\n return noop;\n }\n\n const routeName = params.name;\n const parsedRoute = parseServiceRouteName(routeName);\n if (parsedRoute.status === 'error') {\n if (shouldLogDev()) {\n getLogger().error(`Invalid name for upsertSyncEventCallback`, undefined, { routeName, reason: parsedRoute.reason });\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'routing.invalidServiceRouteName' });\n }\n return noop;\n }\n const sanitizedName = parsedRoute.normalizedRouteName;\n\n const routeVersion = params.version;\n const fullName = `sync/${sanitizedName}/${routeVersion}`;\n const callback: SyncEventCallback = ({ clientOutput, serverOutput }) => {\n params.callback({\n clientOutput: clientOutput as ClientOutputForFullName<F, V>,\n serverOutput: serverOutput as ServerOutputForFullName<F, V>,\n });\n };\n const callbacks = getCallbacksForRoute(fullName);\n\n const previousForRoute = localRegistryRef.current.get(fullName);\n if (previousForRoute) {\n syncEvents[fullName] = callbacks.filter((cb) => cb !== previousForRoute);\n }\n\n const nextCallbacks = getCallbacksForRoute(fullName);\n // Multiple components can intentionally subscribe to the same sync event.\n // Only warn when the exact same callback is registered twice.\n if (nextCallbacks.includes(callback)) {\n if (shouldLogDev()) {\n getLogger().warn(`[SyncEvents] Duplicate callback registration was ignored`, { fullName });\n }\n\n localRegistryRef.current.set(fullName, callback);\n\n return () => {\n const current = getCallbacksForRoute(fullName);\n syncEvents[fullName] = current.filter((cb) => cb !== callback);\n\n if (localRegistryRef.current.get(fullName) === callback) {\n localRegistryRef.current.delete(fullName);\n }\n };\n }\n\n nextCallbacks.push(callback);\n syncEvents[fullName] = nextCallbacks;\n localRegistryRef.current.set(fullName, callback);\n\n return () => {\n const current = getCallbacksForRoute(fullName);\n syncEvents[fullName] = current.filter((cb) => cb !== callback);\n\n if (localRegistryRef.current.get(fullName) === callback) {\n localRegistryRef.current.delete(fullName);\n }\n };\n }, []);\n\n const upsertSyncEventStreamCallback = useCallback(<F extends SyncFullName, V extends VersionsForFullName<F>>(\n params: UpsertStreamParams<F, V>\n ): (() => void) => {\n\n if (typeof params.version !== 'string') {\n if (shouldLogDev()) {\n getLogger().error(\"Invalid version for upsertSyncEventStreamCallback\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'sync.invalidVersion' });\n }\n return noop;\n }\n\n if (typeof params.callback !== 'function') {\n if (shouldLogDev()) {\n getLogger().error(\"Invalid callback for upsertSyncEventStreamCallback\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'sync.invalidCallback' });\n }\n return noop;\n }\n\n const routeName = params.name;\n const parsedRoute = parseServiceRouteName(routeName);\n if (parsedRoute.status === 'error') {\n if (shouldLogDev()) {\n getLogger().error(`Invalid name for upsertSyncEventStreamCallback`, undefined, { routeName, reason: parsedRoute.reason });\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'routing.invalidServiceRouteName' });\n }\n return noop;\n }\n const sanitizedName = parsedRoute.normalizedRouteName;\n\n const routeVersion = params.version;\n const fullName = `sync/${sanitizedName}/${routeVersion}`;\n //? Combined union: both `serverStream` (from `broadcastStream` /\n //? `streamTo`) and `clientStream` (from `_client_v{n}.ts`) flow through\n //? this callback. The type matches `SyncRouteStreamCallbackForFullName`.\n const typedCallback = params.callback as (params: {\n stream: SyncRouteStreamEvent<Prettify<CombinedRouteStream<F, V> extends StreamPayload ? CombinedRouteStream<F, V> : StreamPayload>>;\n }) => void;\n const callback: SyncEventStreamCallback = ({ stream }) => {\n typedCallback({\n stream: stream as SyncRouteStreamEvent<Prettify<CombinedRouteStream<F, V> extends StreamPayload ? CombinedRouteStream<F, V> : StreamPayload>>,\n });\n };\n const callbacks = getStreamCallbacksForRoute(fullName);\n\n const previousForRoute = localStreamRegistryRef.current.get(fullName);\n if (previousForRoute) {\n syncStreamEvents[fullName] = callbacks.filter((cb) => cb !== previousForRoute);\n }\n\n const nextCallbacks = getStreamCallbacksForRoute(fullName);\n if (nextCallbacks.includes(callback)) {\n if (shouldLogDev()) {\n getLogger().warn(`[SyncEvents] Duplicate stream callback registration was ignored`, { fullName });\n }\n\n localStreamRegistryRef.current.set(fullName, callback);\n\n return () => {\n const current = getStreamCallbacksForRoute(fullName);\n syncStreamEvents[fullName] = current.filter((cb) => cb !== callback);\n\n if (localStreamRegistryRef.current.get(fullName) === callback) {\n localStreamRegistryRef.current.delete(fullName);\n }\n };\n }\n\n nextCallbacks.push(callback);\n syncStreamEvents[fullName] = nextCallbacks;\n localStreamRegistryRef.current.set(fullName, callback);\n\n return () => {\n const current = getStreamCallbacksForRoute(fullName);\n syncStreamEvents[fullName] = current.filter((cb) => cb !== callback);\n\n if (localStreamRegistryRef.current.get(fullName) === callback) {\n localStreamRegistryRef.current.delete(fullName);\n }\n };\n }, []);\n\n useEffect(() => {\n const localRegistry = localRegistryRef.current;\n const localStreamRegistry = localStreamRegistryRef.current;\n\n return () => {\n for (const [fullName, callback] of localRegistry.entries()) {\n const current = getCallbacksForRoute(fullName);\n syncEvents[fullName] = current.filter((cb) => cb !== callback);\n }\n\n for (const [fullName, callback] of localStreamRegistry.entries()) {\n const current = getStreamCallbacksForRoute(fullName);\n syncStreamEvents[fullName] = current.filter((cb) => cb !== callback);\n }\n\n localRegistry.clear();\n localStreamRegistry.clear();\n };\n }, []);\n\n return { upsertSyncEventCallback, upsertSyncEventStreamCallback };\n}\n\nexport const useSyncEventTrigger = () => {\n const triggerSyncEvent = useCallback((name: string, clientOutput: unknown = {}, serverOutput: unknown = {}) => {\n triggerSyncCallbacks(name, clientOutput, serverOutput);\n }, []);\n\n const triggerSyncStreamEvent = useCallback((name: string, stream: SyncRouteStreamEvent) => {\n triggerSyncStreamCallbacks(name, stream);\n }, []);\n\n return { triggerSyncEvent, triggerSyncStreamEvent }\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Sync Receive Bridge — attachSyncReceiver\n// ═══════════════════════════════════════════════════════════════════════════════\n\n//? Shape of the `sync` socket event the server emits to clients. Mirrors the\n//? server's emit payload; extra keys are stream chunk fields.\ninterface SyncEventPayload {\n cb?: string;\n clientOutput?: unknown;\n serverOutput?: unknown;\n message?: string;\n status?: 'success' | 'error' | 'stream';\n fullName?: string;\n errorCode?: string;\n errorParams?: { key: string; value: string | number | boolean }[];\n httpStatus?: number;\n [key: string]: unknown;\n}\n\nconst normalizeSyncRouteKey = (value: string): string => {\n const sanitized = value.replaceAll(/^\\/+|\\/+$/g, '');\n if (sanitized.length === 0) return '';\n if (sanitized.startsWith('sync/')) return sanitized;\n return `sync/${sanitized}`;\n};\n\nconst getSyncRouteKeys = ({ fullName, cb }: { fullName?: string; cb?: string }): string[] => {\n const keys = new Set<string>();\n if (typeof fullName === 'string') {\n const normalized = normalizeSyncRouteKey(fullName);\n if (normalized) keys.add(normalized);\n }\n if (typeof cb === 'string') {\n const normalized = normalizeSyncRouteKey(cb);\n if (normalized) keys.add(normalized);\n }\n return [...keys];\n};\n\n//? Per-socket idempotency guard — attaching twice to the same socket would\n//? double-dispatch every sync event.\nconst attachedSyncSockets = new WeakSet<Socket>();\n\n//? Wire the incoming `sync` socket listener that routes server fan-out into the\n//? registered `upsertSyncEventCallback` / `upsertSyncEventStreamCallback`\n//? subscribers. Previously this logic was inlined in the consumer's\n//? `socketInitializer.ts`; relocating it here makes `@luckystack/sync` own its\n//? own receive path, so a consumer only has to dynamic-import this and call it\n//? (the add-later sync wiring is then a single line, not a copy-paste block).\nexport const attachSyncReceiver = (socketInstance: Socket): void => {\n if (attachedSyncSockets.has(socketInstance)) return;\n attachedSyncSockets.add(socketInstance);\n\n socketInstance.on(socketEventNames.sync, (payload: SyncEventPayload) => {\n const { cb, clientOutput, serverOutput, message, status, fullName, errorCode, errorParams } = payload;\n if (shouldLogDev()) {\n getLogger().debug('Server Sync Response', { payload });\n }\n\n const routeKeys = getSyncRouteKeys({ fullName, cb });\n\n if (status === 'stream') {\n if (routeKeys.length === 0) return;\n\n //? SYNC-N7 — strip all reserved envelope keys from the forwarded stream\n //? payload so a consumer handler that accidentally emits `errorCode`,\n //? `errorParams`, `httpStatus`, or `message` in a stream chunk doesn't\n //? pollute the client callback's stream argument with error-envelope fields.\n //? `status`, `fullName`, `cb` were already stripped; the others are added here.\n const streamPayload = { ...payload };\n delete streamPayload.status;\n delete streamPayload.fullName;\n delete streamPayload.cb;\n delete streamPayload.errorCode;\n delete streamPayload.errorParams;\n delete streamPayload.httpStatus;\n delete streamPayload.message;\n\n if (shouldLogStream()) {\n getLogger().debug('Server Sync Stream', { routeKeys, streamPayload });\n }\n\n for (const routeKey of routeKeys) {\n triggerSyncStreamCallbacks(routeKey, streamPayload);\n }\n return;\n }\n\n if (status === 'error') {\n if (errorCode === 'sync.ignore' || message === 'sync.ignore') return;\n if (shouldNotifyDev()) {\n if (errorCode) {\n notify.error({ key: errorCode, params: errorParams });\n } else if (message) {\n notify.error({ key: message });\n }\n }\n return;\n }\n\n if (routeKeys.length === 0) {\n const errorMessage = `Sync response is missing fullName for cb '${cb ?? 'unknown'}'.`;\n if (shouldLogDev()) {\n getLogger().error(errorMessage);\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'sync.invalidRequestFormat' });\n }\n //? SYNC-10 — degrade to a logged warning instead of throwing. This runs\n //? inside the `socket.on(sync)` listener; a `throw` here is an uncaught\n //? exception on a server-controlled (malformed) frame that can kill other\n //? listeners bound to the same emit. The error is already logged + notified\n //? above, so `return` loses no information.\n return;\n }\n\n for (const routeKey of routeKeys) {\n triggerSyncCallbacks(routeKey, clientOutput, serverOutput);\n }\n });\n};\n\ntype SocketStatusSetter = Dispatch<\n SetStateAction<{\n self: statusContent;\n [userId: string]: statusContent;\n }>\n>;\n\nconst buildConnectHandler = ({ setSocketStatus }: { setSocketStatus: SocketStatusSetter }) => () => {\n if (shouldLogSocketStatus()) getLogger().info(\"Connected to server\");\n setSocketStatus(prev => ({ ...prev, self: { ...prev.self, status: \"CONNECTED\" } }));\n};\n\nconst buildDisconnectHandler = ({ setSocketStatus }: { setSocketStatus: SocketStatusSetter }) => () => {\n setSocketStatus(prev => ({ ...prev, self: { ...prev.self, status: \"DISCONNECTED\" } }));\n if (shouldLogSocketStatus()) getLogger().info(\"Disconnected, trying to reconnect...\");\n};\n\nconst buildReconnectAttemptHandler = ({ setSocketStatus }: { setSocketStatus: SocketStatusSetter }) => (attempt: number) => {\n setSocketStatus(prev => ({ ...prev, self: { ...prev.self, status: \"RECONNECTING\", reconnectAttempt: attempt } }));\n if (shouldLogSocketStatus()) getLogger().info(`Reconnecting attempt ${String(attempt)}...`);\n};\n\nconst buildUserAfkHandler = ({\n setSocketStatus, sessionRef,\n}: {\n setSocketStatus: SocketStatusSetter;\n sessionRef: RefObject<SessionLayout | null>;\n}) => ({ userId, endTime }: { userId: string; endTime?: number }) => {\n if (sessionRef.current !== null && userId === sessionRef.current.id) {\n setSocketStatus(prev => ({\n ...prev,\n self: { status: \"DISCONNECTED\", reconnectAttempt: undefined, endTime },\n }));\n } else {\n setSocketStatus(prev => ({\n ...prev,\n [userId]: { status: \"DISCONNECTED\", endTime },\n }));\n }\n};\n\nconst buildUserBackHandler = ({ setSocketStatus }: { setSocketStatus: SocketStatusSetter }) => ({ userId }: { userId: string }) => {\n if (shouldLogSocketStatus()) getLogger().debug(\"userBack\", { userId });\n setSocketStatus(prev => ({\n ...prev,\n [userId]: { status: \"CONNECTED\", endTime: undefined },\n }));\n};\n\nconst buildConnectErrorHandler = ({ setSocketStatus }: { setSocketStatus: SocketStatusSetter }) => (err: { message: string }) => {\n if (shouldLogSocketStatus()) getLogger().debug(\"connect_error\", { err });\n setSocketStatus(prev => ({\n ...prev,\n self: { ...prev.self, status: \"DISCONNECTED\", reconnectAttempt: undefined },\n }));\n if (shouldLogDev()) getLogger().error(`Connection error`, err);\n if (shouldNotifyDev()) notify.error({ key: 'common.connectionError' });\n};\n\n//? SYNC-N6 — `initSyncRequest` owns the canonical lifecycle listeners\n//? (`connect`, `disconnect`, `reconnect_attempt`, `userAfk`, `userBack`,\n//? `connect_error`). A consumer's `socketInitializer.ts` that also registers\n//? `disconnect`/`reconnect_attempt` on the same socket instance creates\n//? duplicate handlers — both fire on every event. Since `initSyncRequest` is\n//? idempotent (tears down the previous set before re-binding), consumer code\n//? should delegate lifecycle status updates here and avoid re-registering\n//? those events directly. Duplicate `console.log` in the consumer is harmless\n//? but produces double log lines.\nexport const initSyncRequest = async ({\n setSocketStatus,\n sessionRef\n}: {\n setSocketStatus: SocketStatusSetter;\n sessionRef: RefObject<SessionLayout | null> | null;\n}) => {\n\n if (!await waitForSocket()) { return; }\n if (!socket) { return; }\n if (!sessionRef) { return; }\n\n if (activeLifecycleHandlers) {\n socket.off(socketEventNames.connect, activeLifecycleHandlers.connect);\n socket.off(socketEventNames.disconnect, activeLifecycleHandlers.disconnect);\n socket.off(socketEventNames.reconnectAttempt, activeLifecycleHandlers.reconnectAttempt);\n socket.off(socketEventNames.userAfk, activeLifecycleHandlers.userAfk);\n socket.off(socketEventNames.userBack, activeLifecycleHandlers.userBack);\n socket.off(socketEventNames.connectError, activeLifecycleHandlers.connectError);\n }\n\n const connect = buildConnectHandler({ setSocketStatus });\n const disconnect = buildDisconnectHandler({ setSocketStatus });\n const reconnectAttempt = buildReconnectAttemptHandler({ setSocketStatus });\n const userAfk = buildUserAfkHandler({ setSocketStatus, sessionRef });\n const userBack = buildUserBackHandler({ setSocketStatus });\n const connectError = buildConnectErrorHandler({ setSocketStatus });\n\n activeLifecycleHandlers = {\n connect,\n disconnect,\n reconnectAttempt,\n userAfk,\n userBack,\n connectError,\n };\n\n socket.on(socketEventNames.connect, connect);\n socket.on(socketEventNames.disconnect, disconnect);\n socket.on(socketEventNames.reconnectAttempt, reconnectAttempt);\n socket.on(socketEventNames.userAfk, userAfk);\n socket.on(socketEventNames.userBack, userBack);\n socket.on(socketEventNames.connectError, connectError);\n\n}"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/syncRequest.ts"],"sourcesContent":["//? All cross-package imports go through `@luckystack/core/client` (the\n//? browser-safe subpath) — never the main `@luckystack/core` barrel which\n//? re-exports server-only modules (paths.ts, db.ts, redis.ts) that must\n//? not enter a Vite client bundle.\nimport type {\n BaseSessionLayout as SessionLayout,\n StreamPayload,\n SyncTypeMap,\n statusContent,\n} from \"@luckystack/core/client\";\nimport {\n getLogger,\n getProjectConfig,\n notify,\n incrementResponseIndex,\n socket,\n waitForSocket,\n enqueueSyncRequest,\n isOnline,\n normalizeErrorResponseCore,\n parseServiceRouteName,\n buildSyncProgressEventName,\n buildSyncResponseEventName,\n socketEventNames,\n} from \"@luckystack/core/client\";\nimport { Dispatch, RefObject, SetStateAction, useCallback, useEffect, useRef } from \"react\";\nimport { Socket } from \"socket.io-client\";\n\nexport type SyncRequestStreamEvent<T extends StreamPayload = StreamPayload> = T;\n\nexport type SyncRouteStreamEvent<T extends StreamPayload = StreamPayload> = T;\n\ntype SyncRequestStreamCallback = (event: SyncRequestStreamEvent) => void;\ntype SyncEventStreamCallback = (params: { stream: SyncRouteStreamEvent }) => void;\n\nconst shouldLogDev = () => getProjectConfig().logging.devLogs;\nconst shouldNotifyDev = () => getProjectConfig().logging.devNotifications;\nconst shouldLogSocketStatus = () => getProjectConfig().logging.socketStatus;\nconst shouldLogStream = () => getProjectConfig().logging.stream;\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Type Helpers for Sync Requests\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\ntype Prettify<T> = { [K in keyof T]: T[K] } & {};\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Global Sync Params\n// ═══════════════════════════════════════════════════════════════════════════════\n\n// All possible sync names across all pages\ntype SyncRouteRecord = UnionToIntersection<{\n [P in keyof SyncTypeMap]: {\n [N in keyof SyncTypeMap[P] as P extends 'root'\n ? `system/${Extract<N, string>}`\n : `${Extract<P, string>}/${Extract<N, string>}`]: SyncTypeMap[P][N]\n }\n}[keyof SyncTypeMap]>;\n\ntype SyncFullName = Extract<keyof SyncRouteRecord, string>;\ntype VersionsForFullName<F extends SyncFullName> = Extract<keyof SyncRouteRecord[F], string>;\n\ntype ClientInputForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = SyncRouteRecord[F][V] extends { clientInput: infer I }\n ? I\n : never;\n\ntype ServerOutputForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = SyncRouteRecord[F][V] extends { serverOutput: infer O }\n ? O\n : never;\n\ntype ClientOutputForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = SyncRouteRecord[F][V] extends { clientOutput: infer O }\n ? O\n : never;\n\ntype ServerStreamForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = SyncRouteRecord[F][V] extends { serverStream: infer O }\n ? O\n : never;\n\ntype ClientStreamForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> = SyncRouteRecord[F][V] extends { clientStream: infer O }\n ? O\n : never;\n\ntype SyncRequestStreamCallbackForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> =\n [ServerStreamForFullName<F, V>] extends [never]\n ? never\n : (event: SyncRequestStreamEvent<Prettify<ServerStreamForFullName<F, V> extends StreamPayload ? ServerStreamForFullName<F, V> : StreamPayload>>) => void;\n\n//? Recipients see chunks from THREE sources, all on the same wire channel:\n//? 1. `_client_v{n}.ts`'s `stream(...)` (per-recipient, runs after _server)\n//? 2. `_server_v{n}.ts`'s `broadcastStream(...)` (room-wide fan-out)\n//? 3. `_server_v{n}.ts`'s `streamTo(tokens, ...)` (selective fan-out)\n//? All three flow into `upsertSyncEventCallback`'s `stream` argument. Both\n//? serverStream and clientStream are folded into the union so the callback\n//? sees every shape the route can emit. If neither side ever streams, the\n//? callback type collapses to never (compile error to register one).\ntype CombinedRouteStream<F extends SyncFullName, V extends VersionsForFullName<F>> =\n | (ClientStreamForFullName<F, V> extends never ? never : ClientStreamForFullName<F, V>)\n | (ServerStreamForFullName<F, V> extends never ? never : ServerStreamForFullName<F, V>);\n\ntype SyncRouteStreamCallbackForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> =\n [CombinedRouteStream<F, V>] extends [never]\n ? never\n : (params: { stream: SyncRouteStreamEvent<Prettify<CombinedRouteStream<F, V> extends StreamPayload ? CombinedRouteStream<F, V> : StreamPayload>> }) => void;\n\ntype SyncParamsForFullName<\n F extends SyncFullName,\n V extends VersionsForFullName<F>\n> = DataRequired<ClientInputForFullName<F, V>> extends true\n ? {\n name: F;\n version: V;\n data: ClientInputForFullName<F, V>;\n receiver: string;\n ignoreSelf?: boolean;\n onStream?: SyncRequestStreamCallbackForFullName<F, V>;\n /**\n * Per-request override of `projectConfig.offlineQueue.dropPolicy`. Lets a\n * specific sync (\"editor cursor move\") pick `'drop-oldest'` while the\n * app default stays `'reject'` for safer sends. When omitted, falls back\n * to the global config.\n */\n offlineDropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';\n /**\n * Optional AbortSignal. When aborted the client emits `syncCancel { cb }`\n * to the server and resolves locally with\n * `{ status: 'error', errorCode: 'request.aborted' }`.\n */\n signal?: AbortSignal;\n }\n : {\n name: F;\n version: V;\n data?: ClientInputForFullName<F, V>;\n receiver: string;\n ignoreSelf?: boolean;\n onStream?: SyncRequestStreamCallbackForFullName<F, V>;\n /** Per-request override (see typed branch). */\n offlineDropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';\n /** Optional AbortSignal — see typed branch. */\n signal?: AbortSignal;\n };\n\ninterface RuntimeSyncParams {\n name?: string;\n version?: string;\n data?: unknown;\n receiver?: string;\n ignoreSelf?: boolean;\n onStream?: SyncRequestStreamCallback;\n offlineDropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';\n signal?: AbortSignal;\n}\n\ninterface SyncErrorParam { key: string; value: string | number | boolean };\n\ninterface SyncResponseError {\n status: 'error';\n message: string;\n errorCode: string;\n errorParams?: SyncErrorParam[];\n httpStatus?: number;\n}\n\ninterface SyncAckResponse {\n status?: 'success' | 'error';\n message?: string;\n result?: unknown;\n errorCode?: string;\n errorParams?: SyncErrorParam[];\n httpStatus?: number;\n}\n\ntype SyncResultForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> =\n [ServerOutputForFullName<F, V>] extends [never]\n ? Record<string, never>\n : ServerOutputForFullName<F, V>;\n\ntype SyncRequestResponseForFullName<F extends SyncFullName, V extends VersionsForFullName<F>> =\n | SyncResponseError\n | {\n status: 'success';\n message: string;\n result: SyncResultForFullName<F, V>;\n };\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Sync Event Callbacks Registry\n// ═══════════════════════════════════════════════════════════════════════════════\n\ntype SyncEventCallback = (params: { clientOutput: unknown; serverOutput: unknown }) => void;\nconst syncEvents: Partial<Record<string, SyncEventCallback[]>> = {};\nconst syncStreamEvents: Partial<Record<string, SyncEventStreamCallback[]>> = {};\nconst noop = () => null;\n\ninterface SyncLifecycleHandlers {\n connect: () => void;\n disconnect: () => void;\n reconnectAttempt: (attempt: number) => void;\n userAfk: (payload: { userId: string; endTime?: number }) => void;\n userBack: (payload: { userId: string }) => void;\n connectError: (err: { message: string }) => void;\n}\n\nlet activeLifecycleHandlers: SyncLifecycleHandlers | null = null;\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\nconst getCallbacksForRoute = (route: string): SyncEventCallback[] => {\n syncEvents[route] ??= [];\n return syncEvents[route];\n};\n\nconst getStreamCallbacksForRoute = (route: string): SyncEventStreamCallback[] => {\n syncStreamEvents[route] ??= [];\n return syncStreamEvents[route];\n};\n\nconst triggerSyncCallbacks = (name: string, clientOutput: unknown, serverOutput: unknown) => {\n const callbacks = syncEvents[name] ?? [];\n if (callbacks.length === 0) {\n //? Streaming-only routes legitimately register ONLY a stream callback —\n //? the final success envelope is implicit in the stream completing. Only\n //? warn when NEITHER callback type is registered for the route.\n const streamCallbacks = syncStreamEvents[name] ?? [];\n if (streamCallbacks.length === 0 && shouldLogDev()) {\n getLogger().warn(`Sync event ${name} has no registered callback on this page`);\n }\n return;\n }\n\n for (const callback of callbacks) {\n callback({ clientOutput, serverOutput });\n }\n};\n\nconst triggerSyncStreamCallbacks = (name: string, stream: SyncRouteStreamEvent) => {\n const callbacks = syncStreamEvents[name] ?? [];\n if (callbacks.length === 0) {\n return;\n }\n\n for (const callback of callbacks) {\n callback({ stream });\n }\n};\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// syncRequest Function Overloads\n// ═══════════════════════════════════════════════════════════════════════════════\n\nconst normalizeSyncError = ({\n response,\n fallbackErrorCode,\n}: {\n response: SyncAckResponse;\n fallbackErrorCode: string;\n}): SyncResponseError => {\n const normalized = normalizeErrorResponseCore({\n response,\n fallbackErrorCode,\n resolveMessage: ({ errorCode }) => {\n if (typeof response.message === 'string' && response.message.trim().length > 0) {\n return response.message;\n }\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,\n };\n};\n\ntype SyncRequestParamsWithOptions<F extends SyncFullName, V extends VersionsForFullName<F>> =\n SyncParamsForFullName<F, V>;\n\n//? Outcome of the pre-flight validation phase of a sync send. Either a\n//? normalized error envelope to resolve the request with, or the sanitized\n//? values the send phase needs. Extracted from `syncRequestInternal` so the\n//? \"validate first, execute second\" split is explicit (the orchestrator stays\n//? a thin promise wrapper around prepare → run).\ntype PreparedSyncRequest =\n | { ok: false; error: SyncResponseError }\n | { ok: true; sanitizedName: string; version: string; data: object; normalizedReceiver: string };\n\n//? Pure-ish validation chain (logs + dev-notifies on failure, same as the\n//? previous inlined block). Async because the socket-readiness gate\n//? (`waitForSocket`) is awaited last, after the cheap structural checks. Each\n//? failure path returns the SAME normalized error envelope the inlined code\n//? resolved with — behaviour-identical.\nconst prepareSyncRequest = async ({\n name,\n version,\n receiver,\n payloadData,\n}: {\n name: unknown;\n version: unknown;\n receiver: unknown;\n payloadData: unknown;\n}): Promise<PreparedSyncRequest> => {\n if (!name || typeof name !== \"string\") {\n if (shouldLogDev()) {\n getLogger().error(\"Invalid name for syncRequest\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'sync.invalidName' });\n }\n return { ok: false, error: normalizeSyncError({\n response: { status: 'error', errorCode: 'sync.invalidName' },\n fallbackErrorCode: 'sync.invalidName',\n }) };\n }\n\n const parsedRoute = parseServiceRouteName(name);\n if (parsedRoute.status === 'error') {\n if (shouldLogDev()) {\n getLogger().error(`[syncRequest] Invalid service route name '${name}'`, undefined, { reason: parsedRoute.reason });\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'routing.invalidServiceRouteName' });\n }\n return { ok: false, error: normalizeSyncError({\n response: {\n status: 'error',\n errorCode: 'routing.invalidServiceRouteName',\n errorParams: [{ key: 'name', value: name }],\n },\n fallbackErrorCode: 'routing.invalidServiceRouteName',\n }) };\n }\n\n const sanitizedName = parsedRoute.normalizedRouteName;\n\n const data = payloadData && typeof payloadData === \"object\" ? payloadData : {};\n\n if (!version || typeof version !== 'string') {\n if (shouldLogDev()) {\n getLogger().error(\"Invalid version for syncRequest\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'sync.invalidVersion' });\n }\n return { ok: false, error: normalizeSyncError({\n response: { status: 'error', errorCode: 'sync.invalidVersion' },\n fallbackErrorCode: 'sync.invalidVersion',\n }) };\n }\n\n const normalizedReceiver = typeof receiver === 'string' ? receiver.trim() : '';\n\n if (!normalizedReceiver) {\n if (shouldLogDev()) {\n getLogger().error(\"You need to provide a receiver for syncRequest, this can be either 'all' to trigger all sockets which we do not recommend or it can be any value such as a code e.g 'Ag2cg4'. this works together with the joinRoom and leaveRoom function\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'sync.missingReceiver' });\n }\n return { ok: false, error: normalizeSyncError({\n response: { status: 'error', errorCode: 'sync.missingReceiver' },\n fallbackErrorCode: 'sync.missingReceiver',\n }) };\n }\n\n if (!await waitForSocket()) {\n return { ok: false, error: normalizeSyncError({\n response: { status: 'error', errorCode: 'sync.ioUnavailable' },\n fallbackErrorCode: 'sync.ioUnavailable',\n }) };\n }\n if (!socket) {\n return { ok: false, error: normalizeSyncError({\n response: { status: 'error', errorCode: 'sync.ioUnavailable' },\n fallbackErrorCode: 'sync.ioUnavailable',\n }) };\n }\n\n return { ok: true, sanitizedName, version, data, normalizedReceiver };\n};\n\nconst syncRequestInternal = <F extends SyncFullName, V extends VersionsForFullName<F>>(\n params: SyncRequestParamsWithOptions<F, V>\n): Promise<Prettify<SyncRequestResponseForFullName<F, V>>> => {\n const runtimeParams = params as RuntimeSyncParams;\n const { name, version, receiver, ignoreSelf, onStream, offlineDropPolicy, signal: externalSignal } = runtimeParams;\n const payloadData = runtimeParams.data;\n\n type RequestOutput = Prettify<SyncRequestResponseForFullName<F, V>>;\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.\n if (externalSignal?.aborted) {\n resolve(normalizeSyncError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }));\n return;\n }\n\n const prepared = await prepareSyncRequest({ name, version, receiver, payloadData });\n if (!prepared.ok) {\n resolve(prepared.error);\n return;\n }\n const { sanitizedName, version: resolvedVersion, data, normalizedReceiver } = prepared;\n\n //? Re-narrow `socket` for the closures below. `prepareSyncRequest`\n //? already guaranteed it is non-null (returns an `ioUnavailable` error\n //? otherwise), but that guard lives in another function so TS can't\n //? carry the narrowing across the call boundary.\n const activeSocket = socket;\n if (!activeSocket) {\n resolve(normalizeSyncError({\n response: { status: 'error', errorCode: 'sync.ioUnavailable' },\n fallbackErrorCode: 'sync.ioUnavailable',\n }));\n return;\n }\n\n const fullName = `sync/${sanitizedName}/${resolvedVersion}`;\n let queueId: string | null = null;\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 //? `enqueueSyncRequest`. 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 //? `enqueueSyncRequest` returns `false` when the offline queue is\n //? full and `dropPolicy: 'reject'` is active. Resolve with a\n //? normalized error envelope so the caller can branch instead of\n //? awaiting forever.\n const enqueued = enqueueSyncRequest({\n id: queueId,\n key: fullName,\n run: (s) => {\n runRequest(s);\n },\n createdAt: Date.now(),\n dropPolicy: offlineDropPolicy,\n //? SYNC-09 — settle the awaiting caller when this QUEUED request is\n //? later EVICTED (drop-oldest by a future enqueue / age expiry)\n //? instead of ever running. Without it the promise would hang\n //? forever, since `run` is the only thing that could otherwise\n //? resolve it. The default `offlineQueue.dropPolicy` is\n //? `'drop-oldest'`, so async eviction is the NORMAL overflow path\n //? for high-churn syncs (cursor moves), not an edge case.\n onDrop: (reason) => {\n if (suppressOnDrop) return;\n resolve(normalizeSyncError({\n response: { status: 'error', errorCode: 'offline.dropped', errorParams: [{ key: 'reason', value: reason }] },\n fallbackErrorCode: 'offline.dropped',\n }));\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(normalizeSyncError({\n response: { status: 'error', errorCode: 'offline.queueFull' },\n fallbackErrorCode: 'offline.queueFull',\n }));\n }\n return;\n }\n\n const tempIndex = incrementResponseIndex();\n\n let cleanupProgressListener: (() => void) | null = null;\n //? CORE-06 — ack-timeout. Armed right after the request is emitted so a\n //? lost acknowledgement (server restart/crash between emit and reply)\n //? settles the promise with `sync.requestTimeout` (504) instead of\n //? hanging the awaiting caller forever. Mirrors `apiRequest`'s\n //? `api.requestTimeoutMs` wiring.\n let responseTimeout: ReturnType<typeof setTimeout> | null = null;\n const clearResponseTimeout = () => {\n if (responseTimeout) {\n clearTimeout(responseTimeout);\n responseTimeout = null;\n }\n };\n\n if (shouldLogDev()) {\n getLogger().debug(`Client Sync Request(${String(tempIndex)})`, { syncName: sanitizedName, data, receiver: normalizedReceiver, ignoreSelf });\n }\n\n //? S13 — server-issued cancel id. The server emits a `{ __cancelId }`\n //? handshake frame on the progress channel before any work runs; we\n //? capture it so an abort can target THIS exact request (not a callback\n //? name reused across concurrent same-route requests, which corrupted\n //? the server-side abort registry). The progress listener is ALWAYS\n //? registered now — even without `onStream` — so the handshake is caught\n //? on non-streaming routes too. The reserved `__cancelId` frame is\n //? filtered out of `onStream` delivery.\n let serverCancelId: string | null = null;\n const progressEventName = buildSyncProgressEventName(tempIndex);\n const progressListener = (streamPayload: SyncRequestStreamEvent & { __cancelId?: string }) => {\n if (typeof streamPayload.__cancelId === 'string') {\n serverCancelId = streamPayload.__cancelId;\n return;\n }\n\n if (typeof onStream !== 'function') return;\n\n if (shouldLogStream()) {\n getLogger().debug(`Server Sync Stream(${String(tempIndex)})`, { syncName: sanitizedName, streamPayload });\n }\n\n onStream(streamPayload);\n };\n\n socketInstance.on(progressEventName, progressListener);\n cleanupProgressListener = () => {\n socketInstance.off(progressEventName, progressListener);\n };\n\n //? Fallback cancel key for the narrow window before the `__cancelId`\n //? handshake frame arrives (abort fired immediately on a slow link).\n //? Best-effort only — once the handshake lands `serverCancelId` is the\n //? authoritative, per-request-unique key the server registered under.\n const syncCb = `${sanitizedName}/${resolvedVersion}`;\n socketInstance.emit(socketEventNames.sync, { name: fullName, data, cb: syncCb, receiver: normalizedReceiver, responseIndex: tempIndex, ignoreSelf });\n\n //? B1 / S13 — bridge the consumer's AbortSignal to the server. When the\n //? signal fires we emit `syncCancel { cb: <serverCancelId> }` so the\n //? server-side handler stops emitting new chunks. We also resolve\n //? locally with `request.aborted` so the awaiting caller settles.\n const responseEventName = buildSyncResponseEventName(tempIndex);\n //? Forward-declared so the abort/timeout paths can proactively `off()`\n //? the lingering ack listener instead of waiting for a reply that was\n //? cancelled / will never arrive.\n let cleanupResponseListener: (() => void) | null = null;\n\n let cleanupExternalAbort: (() => void) | null = null;\n if (externalSignal) {\n const externalAbortHandler = () => {\n clearResponseTimeout();\n socketInstance.emit(socketEventNames.syncCancel, { cb: serverCancelId ?? syncCb });\n cleanupProgressListener?.();\n cleanupProgressListener = null;\n cleanupResponseListener?.();\n cleanupResponseListener = null;\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n resolve(normalizeSyncError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }));\n };\n externalSignal.addEventListener('abort', externalAbortHandler);\n cleanupExternalAbort = () => {\n externalSignal.removeEventListener('abort', externalAbortHandler);\n };\n }\n\n //? Arm the ack-timeout. `sync.requestTimeoutMs` (default 30000) may be\n //? `false` to disable. On expiry we tear down every listener and settle\n //? with a 504 envelope so the awaiting caller never hangs.\n const effectiveTimeoutMs = getProjectConfig().sync.requestTimeoutMs;\n if (typeof effectiveTimeoutMs === 'number' && effectiveTimeoutMs > 0) {\n responseTimeout = setTimeout(() => {\n responseTimeout = null;\n //? SYNC — on ack-timeout, tell the server to STOP the in-flight work\n //? before tearing down locally. Previously the timeout only cleaned up\n //? the client listeners + settled 504, leaving the `_server` handler\n //? (and any fanout/streaming) running to completion uncancelled. The\n //? `__cancelId` handshake has long since arrived by the timeout window\n //? (the server emits it immediately), so `serverCancelId` is the\n //? authoritative key here; `syncCb` is only the sub-RTT fallback.\n socketInstance.emit(socketEventNames.syncCancel, { cb: serverCancelId ?? syncCb });\n cleanupProgressListener?.();\n cleanupProgressListener = null;\n cleanupResponseListener?.();\n cleanupResponseListener = null;\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n if (shouldLogDev()) {\n getLogger().error(`Sync ${sanitizedName} timed out`, undefined, { responseIndex: tempIndex });\n }\n resolve(normalizeSyncError({\n response: { status: 'error', errorCode: 'sync.requestTimeout', httpStatus: 504 },\n fallbackErrorCode: 'sync.requestTimeout',\n }));\n }, effectiveTimeoutMs);\n }\n\n const responseListener = (responseData: SyncAckResponse) => {\n clearResponseTimeout();\n cleanupProgressListener?.();\n cleanupProgressListener = null;\n cleanupResponseListener = null;\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n\n if (responseData.status === \"error\") {\n const normalizedError = normalizeSyncError({\n response: responseData,\n fallbackErrorCode: 'sync.failedRequest',\n });\n\n if (shouldLogDev()) {\n getLogger().error(`Sync ${sanitizedName} failed`, undefined, { message: normalizedError.message });\n }\n if (shouldNotifyDev()) {\n notify.error({\n key: 'sync.failedRequest',\n params: [\n { key: 'name', value: sanitizedName },\n { key: 'message', value: normalizedError.message },\n ],\n });\n }\n resolve(normalizedError);\n return;\n }\n\n if (responseData.status !== 'success') {\n resolve(normalizeSyncError({\n response: responseData,\n fallbackErrorCode: 'sync.invalidServerResponse',\n }));\n return;\n }\n\n const result = responseData.result && typeof responseData.result === 'object'\n ? responseData.result\n : {};\n\n //? `RequestOutput.result` is a generic `Record<string, never>` shape\n //? that the runtime response can't structurally satisfy from the\n //? untyped `responseData.result` (server-side typing lives on the\n //? other side of the socket). The cast is the documented socket\n //? response boundary.\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- socket response boundary\n resolve({\n status: 'success',\n message: typeof responseData.message === 'string' && responseData.message.trim().length > 0\n ? responseData.message\n : `sync ${sanitizedName} success`,\n result,\n } as RequestOutput);\n };\n\n socketInstance.once(responseEventName, responseListener);\n cleanupResponseListener = () => {\n socketInstance.off(responseEventName, responseListener);\n };\n };\n\n runRequest(activeSocket);\n })();\n });\n};\n\nexport function syncRequest<F extends SyncFullName, V extends VersionsForFullName<F>>(\n params: SyncRequestParamsWithOptions<F, V>\n): Promise<Prettify<SyncRequestResponseForFullName<F, V>>> {\n return syncRequestInternal(params);\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// useSyncEvents Hook - Type-Safe Event Registration\n// ═══════════════════════════════════════════════════════════════════════════════\n\n//? Hoisted to module scope so the emitted `.d.ts` for `useSyncEvents` can\n//? reference them. Local interface declarations inside an exported function\n//? trigger TS4025 (\"has or is using private name\") under `declaration: true`.\ninterface TypedCallbackParams<F extends SyncFullName, V extends VersionsForFullName<F>> {\n clientOutput: ClientOutputForFullName<F, V>;\n serverOutput: ServerOutputForFullName<F, V>;\n}\n\ninterface UpsertParams<F extends SyncFullName, V extends VersionsForFullName<F>> {\n name: F;\n version: V;\n callback: (params: TypedCallbackParams<F, V>) => void;\n}\n\ninterface UpsertStreamParams<F extends SyncFullName, V extends VersionsForFullName<F>> {\n name: F;\n version: V;\n callback: SyncRouteStreamCallbackForFullName<F, V>;\n}\n\nexport const useSyncEvents = () => {\n const localRegistryRef = useRef<Map<string, SyncEventCallback>>(new Map());\n const localStreamRegistryRef = useRef<Map<string, SyncEventStreamCallback>>(new Map());\n\n const upsertSyncEventCallback = useCallback(<F extends SyncFullName, V extends VersionsForFullName<F>>(\n params: UpsertParams<F, V>\n ): (() => void) => {\n\n if (typeof params.version !== 'string') {\n if (shouldLogDev()) {\n getLogger().error(\"Invalid version for upsertSyncEventCallback\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'sync.invalidVersion' });\n }\n return noop;\n }\n\n if (typeof params.callback !== 'function') {\n if (shouldLogDev()) {\n getLogger().error(\"Invalid callback for upsertSyncEventCallback\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'sync.invalidCallback' });\n }\n return noop;\n }\n\n const routeName = params.name;\n const parsedRoute = parseServiceRouteName(routeName);\n if (parsedRoute.status === 'error') {\n if (shouldLogDev()) {\n getLogger().error(`Invalid name for upsertSyncEventCallback`, undefined, { routeName, reason: parsedRoute.reason });\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'routing.invalidServiceRouteName' });\n }\n return noop;\n }\n const sanitizedName = parsedRoute.normalizedRouteName;\n\n const routeVersion = params.version;\n const fullName = `sync/${sanitizedName}/${routeVersion}`;\n const callback: SyncEventCallback = ({ clientOutput, serverOutput }) => {\n params.callback({\n clientOutput: clientOutput as ClientOutputForFullName<F, V>,\n serverOutput: serverOutput as ServerOutputForFullName<F, V>,\n });\n };\n const callbacks = getCallbacksForRoute(fullName);\n\n const previousForRoute = localRegistryRef.current.get(fullName);\n if (previousForRoute) {\n syncEvents[fullName] = callbacks.filter((cb) => cb !== previousForRoute);\n }\n\n const nextCallbacks = getCallbacksForRoute(fullName);\n // Multiple components can intentionally subscribe to the same sync event.\n // Only warn when the exact same callback is registered twice.\n if (nextCallbacks.includes(callback)) {\n if (shouldLogDev()) {\n getLogger().warn(`[SyncEvents] Duplicate callback registration was ignored`, { fullName });\n }\n\n localRegistryRef.current.set(fullName, callback);\n\n return () => {\n const current = getCallbacksForRoute(fullName);\n syncEvents[fullName] = current.filter((cb) => cb !== callback);\n\n if (localRegistryRef.current.get(fullName) === callback) {\n localRegistryRef.current.delete(fullName);\n }\n };\n }\n\n nextCallbacks.push(callback);\n syncEvents[fullName] = nextCallbacks;\n localRegistryRef.current.set(fullName, callback);\n\n return () => {\n const current = getCallbacksForRoute(fullName);\n syncEvents[fullName] = current.filter((cb) => cb !== callback);\n\n if (localRegistryRef.current.get(fullName) === callback) {\n localRegistryRef.current.delete(fullName);\n }\n };\n }, []);\n\n const upsertSyncEventStreamCallback = useCallback(<F extends SyncFullName, V extends VersionsForFullName<F>>(\n params: UpsertStreamParams<F, V>\n ): (() => void) => {\n\n if (typeof params.version !== 'string') {\n if (shouldLogDev()) {\n getLogger().error(\"Invalid version for upsertSyncEventStreamCallback\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'sync.invalidVersion' });\n }\n return noop;\n }\n\n if (typeof params.callback !== 'function') {\n if (shouldLogDev()) {\n getLogger().error(\"Invalid callback for upsertSyncEventStreamCallback\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'sync.invalidCallback' });\n }\n return noop;\n }\n\n const routeName = params.name;\n const parsedRoute = parseServiceRouteName(routeName);\n if (parsedRoute.status === 'error') {\n if (shouldLogDev()) {\n getLogger().error(`Invalid name for upsertSyncEventStreamCallback`, undefined, { routeName, reason: parsedRoute.reason });\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'routing.invalidServiceRouteName' });\n }\n return noop;\n }\n const sanitizedName = parsedRoute.normalizedRouteName;\n\n const routeVersion = params.version;\n const fullName = `sync/${sanitizedName}/${routeVersion}`;\n //? Combined union: both `serverStream` (from `broadcastStream` /\n //? `streamTo`) and `clientStream` (from `_client_v{n}.ts`) flow through\n //? this callback. The type matches `SyncRouteStreamCallbackForFullName`.\n const typedCallback = params.callback as (params: {\n stream: SyncRouteStreamEvent<Prettify<CombinedRouteStream<F, V> extends StreamPayload ? CombinedRouteStream<F, V> : StreamPayload>>;\n }) => void;\n const callback: SyncEventStreamCallback = ({ stream }) => {\n typedCallback({\n stream: stream as SyncRouteStreamEvent<Prettify<CombinedRouteStream<F, V> extends StreamPayload ? CombinedRouteStream<F, V> : StreamPayload>>,\n });\n };\n const callbacks = getStreamCallbacksForRoute(fullName);\n\n const previousForRoute = localStreamRegistryRef.current.get(fullName);\n if (previousForRoute) {\n syncStreamEvents[fullName] = callbacks.filter((cb) => cb !== previousForRoute);\n }\n\n const nextCallbacks = getStreamCallbacksForRoute(fullName);\n if (nextCallbacks.includes(callback)) {\n if (shouldLogDev()) {\n getLogger().warn(`[SyncEvents] Duplicate stream callback registration was ignored`, { fullName });\n }\n\n localStreamRegistryRef.current.set(fullName, callback);\n\n return () => {\n const current = getStreamCallbacksForRoute(fullName);\n syncStreamEvents[fullName] = current.filter((cb) => cb !== callback);\n\n if (localStreamRegistryRef.current.get(fullName) === callback) {\n localStreamRegistryRef.current.delete(fullName);\n }\n };\n }\n\n nextCallbacks.push(callback);\n syncStreamEvents[fullName] = nextCallbacks;\n localStreamRegistryRef.current.set(fullName, callback);\n\n return () => {\n const current = getStreamCallbacksForRoute(fullName);\n syncStreamEvents[fullName] = current.filter((cb) => cb !== callback);\n\n if (localStreamRegistryRef.current.get(fullName) === callback) {\n localStreamRegistryRef.current.delete(fullName);\n }\n };\n }, []);\n\n useEffect(() => {\n const localRegistry = localRegistryRef.current;\n const localStreamRegistry = localStreamRegistryRef.current;\n\n return () => {\n for (const [fullName, callback] of localRegistry.entries()) {\n const current = getCallbacksForRoute(fullName);\n syncEvents[fullName] = current.filter((cb) => cb !== callback);\n }\n\n for (const [fullName, callback] of localStreamRegistry.entries()) {\n const current = getStreamCallbacksForRoute(fullName);\n syncStreamEvents[fullName] = current.filter((cb) => cb !== callback);\n }\n\n localRegistry.clear();\n localStreamRegistry.clear();\n };\n }, []);\n\n return { upsertSyncEventCallback, upsertSyncEventStreamCallback };\n}\n\nexport const useSyncEventTrigger = () => {\n const triggerSyncEvent = useCallback((name: string, clientOutput: unknown = {}, serverOutput: unknown = {}) => {\n triggerSyncCallbacks(name, clientOutput, serverOutput);\n }, []);\n\n const triggerSyncStreamEvent = useCallback((name: string, stream: SyncRouteStreamEvent) => {\n triggerSyncStreamCallbacks(name, stream);\n }, []);\n\n return { triggerSyncEvent, triggerSyncStreamEvent }\n}\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Sync Receive Bridge — attachSyncReceiver\n// ═══════════════════════════════════════════════════════════════════════════════\n\n//? Shape of the `sync` socket event the server emits to clients. Mirrors the\n//? server's emit payload; extra keys are stream chunk fields.\ninterface SyncEventPayload {\n cb?: string;\n clientOutput?: unknown;\n serverOutput?: unknown;\n message?: string;\n status?: 'success' | 'error' | 'stream';\n fullName?: string;\n errorCode?: string;\n errorParams?: { key: string; value: string | number | boolean }[];\n httpStatus?: number;\n [key: string]: unknown;\n}\n\nconst normalizeSyncRouteKey = (value: string): string => {\n const sanitized = value.replaceAll(/^\\/+|\\/+$/g, '');\n if (sanitized.length === 0) return '';\n if (sanitized.startsWith('sync/')) return sanitized;\n return `sync/${sanitized}`;\n};\n\nconst getSyncRouteKeys = ({ fullName, cb }: { fullName?: string; cb?: string }): string[] => {\n const keys = new Set<string>();\n if (typeof fullName === 'string') {\n const normalized = normalizeSyncRouteKey(fullName);\n if (normalized) keys.add(normalized);\n }\n if (typeof cb === 'string') {\n const normalized = normalizeSyncRouteKey(cb);\n if (normalized) keys.add(normalized);\n }\n return [...keys];\n};\n\n//? Per-socket idempotency guard — attaching twice to the same socket would\n//? double-dispatch every sync event.\nconst attachedSyncSockets = new WeakSet<Socket>();\n\n//? Wire the incoming `sync` socket listener that routes server fan-out into the\n//? registered `upsertSyncEventCallback` / `upsertSyncEventStreamCallback`\n//? subscribers. Previously this logic was inlined in the consumer's\n//? `socketInitializer.ts`; relocating it here makes `@luckystack/sync` own its\n//? own receive path, so a consumer only has to dynamic-import this and call it\n//? (the add-later sync wiring is then a single line, not a copy-paste block).\nexport const attachSyncReceiver = (socketInstance: Socket): void => {\n if (attachedSyncSockets.has(socketInstance)) return;\n attachedSyncSockets.add(socketInstance);\n\n socketInstance.on(socketEventNames.sync, (payload: SyncEventPayload) => {\n const { cb, clientOutput, serverOutput, message, status, fullName, errorCode, errorParams } = payload;\n if (shouldLogDev()) {\n getLogger().debug('Server Sync Response', { payload });\n }\n\n const routeKeys = getSyncRouteKeys({ fullName, cb });\n\n if (status === 'stream') {\n if (routeKeys.length === 0) return;\n\n //? SYNC-N7 — strip all reserved envelope keys from the forwarded stream\n //? payload so a consumer handler that accidentally emits `errorCode`,\n //? `errorParams`, `httpStatus`, or `message` in a stream chunk doesn't\n //? pollute the client callback's stream argument with error-envelope fields.\n //? `status`, `fullName`, `cb` were already stripped; the others are added here.\n const streamPayload = { ...payload };\n delete streamPayload.status;\n delete streamPayload.fullName;\n delete streamPayload.cb;\n delete streamPayload.errorCode;\n delete streamPayload.errorParams;\n delete streamPayload.httpStatus;\n delete streamPayload.message;\n\n if (shouldLogStream()) {\n getLogger().debug('Server Sync Stream', { routeKeys, streamPayload });\n }\n\n for (const routeKey of routeKeys) {\n triggerSyncStreamCallbacks(routeKey, streamPayload);\n }\n return;\n }\n\n if (status === 'error') {\n if (errorCode === 'sync.ignore' || message === 'sync.ignore') return;\n if (shouldNotifyDev()) {\n if (errorCode) {\n notify.error({ key: errorCode, params: errorParams });\n } else if (message) {\n notify.error({ key: message });\n }\n }\n return;\n }\n\n if (routeKeys.length === 0) {\n const errorMessage = `Sync response is missing fullName for cb '${cb ?? 'unknown'}'.`;\n if (shouldLogDev()) {\n getLogger().error(errorMessage);\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'sync.invalidRequestFormat' });\n }\n //? SYNC-10 — degrade to a logged warning instead of throwing. This runs\n //? inside the `socket.on(sync)` listener; a `throw` here is an uncaught\n //? exception on a server-controlled (malformed) frame that can kill other\n //? listeners bound to the same emit. The error is already logged + notified\n //? above, so `return` loses no information.\n return;\n }\n\n for (const routeKey of routeKeys) {\n triggerSyncCallbacks(routeKey, clientOutput, serverOutput);\n }\n });\n};\n\ntype SocketStatusSetter = Dispatch<\n SetStateAction<{\n self: statusContent;\n [userId: string]: statusContent;\n }>\n>;\n\nconst buildConnectHandler = ({ setSocketStatus }: { setSocketStatus: SocketStatusSetter }) => () => {\n if (shouldLogSocketStatus()) getLogger().info(\"Connected to server\");\n setSocketStatus(prev => ({ ...prev, self: { ...prev.self, status: \"CONNECTED\" } }));\n};\n\nconst buildDisconnectHandler = ({ setSocketStatus }: { setSocketStatus: SocketStatusSetter }) => () => {\n setSocketStatus(prev => ({ ...prev, self: { ...prev.self, status: \"DISCONNECTED\" } }));\n if (shouldLogSocketStatus()) getLogger().info(\"Disconnected, trying to reconnect...\");\n};\n\nconst buildReconnectAttemptHandler = ({ setSocketStatus }: { setSocketStatus: SocketStatusSetter }) => (attempt: number) => {\n setSocketStatus(prev => ({ ...prev, self: { ...prev.self, status: \"RECONNECTING\", reconnectAttempt: attempt } }));\n if (shouldLogSocketStatus()) getLogger().info(`Reconnecting attempt ${String(attempt)}...`);\n};\n\nconst buildUserAfkHandler = ({\n setSocketStatus, sessionRef,\n}: {\n setSocketStatus: SocketStatusSetter;\n sessionRef: RefObject<SessionLayout | null>;\n}) => ({ userId, endTime }: { userId: string; endTime?: number }) => {\n if (sessionRef.current !== null && userId === sessionRef.current.id) {\n setSocketStatus(prev => ({\n ...prev,\n self: { status: \"DISCONNECTED\", reconnectAttempt: undefined, endTime },\n }));\n } else {\n setSocketStatus(prev => ({\n ...prev,\n [userId]: { status: \"DISCONNECTED\", endTime },\n }));\n }\n};\n\nconst buildUserBackHandler = ({ setSocketStatus }: { setSocketStatus: SocketStatusSetter }) => ({ userId }: { userId: string }) => {\n if (shouldLogSocketStatus()) getLogger().debug(\"userBack\", { userId });\n setSocketStatus(prev => ({\n ...prev,\n [userId]: { status: \"CONNECTED\", endTime: undefined },\n }));\n};\n\nconst buildConnectErrorHandler = ({ setSocketStatus }: { setSocketStatus: SocketStatusSetter }) => (err: { message: string }) => {\n if (shouldLogSocketStatus()) getLogger().debug(\"connect_error\", { err });\n setSocketStatus(prev => ({\n ...prev,\n self: { ...prev.self, status: \"DISCONNECTED\", reconnectAttempt: undefined },\n }));\n if (shouldLogDev()) getLogger().error(`Connection error`, err);\n if (shouldNotifyDev()) notify.error({ key: 'common.connectionError' });\n};\n\n//? SYNC-N6 — `initSyncRequest` owns the canonical lifecycle listeners\n//? (`connect`, `disconnect`, `reconnect_attempt`, `userAfk`, `userBack`,\n//? `connect_error`). A consumer's `socketInitializer.ts` that also registers\n//? `disconnect`/`reconnect_attempt` on the same socket instance creates\n//? duplicate handlers — both fire on every event. Since `initSyncRequest` is\n//? idempotent (tears down the previous set before re-binding), consumer code\n//? should delegate lifecycle status updates here and avoid re-registering\n//? those events directly. Duplicate `console.log` in the consumer is harmless\n//? but produces double log lines.\nexport const initSyncRequest = async ({\n setSocketStatus,\n sessionRef\n}: {\n setSocketStatus: SocketStatusSetter;\n sessionRef: RefObject<SessionLayout | null> | null;\n}) => {\n\n if (!await waitForSocket()) { return; }\n if (!socket) { return; }\n if (!sessionRef) { return; }\n\n if (activeLifecycleHandlers) {\n socket.off(socketEventNames.connect, activeLifecycleHandlers.connect);\n socket.off(socketEventNames.disconnect, activeLifecycleHandlers.disconnect);\n socket.off(socketEventNames.reconnectAttempt, activeLifecycleHandlers.reconnectAttempt);\n socket.off(socketEventNames.userAfk, activeLifecycleHandlers.userAfk);\n socket.off(socketEventNames.userBack, activeLifecycleHandlers.userBack);\n socket.off(socketEventNames.connectError, activeLifecycleHandlers.connectError);\n }\n\n const connect = buildConnectHandler({ setSocketStatus });\n const disconnect = buildDisconnectHandler({ setSocketStatus });\n const reconnectAttempt = buildReconnectAttemptHandler({ setSocketStatus });\n const userAfk = buildUserAfkHandler({ setSocketStatus, sessionRef });\n const userBack = buildUserBackHandler({ setSocketStatus });\n const connectError = buildConnectErrorHandler({ setSocketStatus });\n\n activeLifecycleHandlers = {\n connect,\n disconnect,\n reconnectAttempt,\n userAfk,\n userBack,\n connectError,\n };\n\n socket.on(socketEventNames.connect, connect);\n socket.on(socketEventNames.disconnect, disconnect);\n socket.on(socketEventNames.reconnectAttempt, reconnectAttempt);\n socket.on(socketEventNames.userAfk, userAfk);\n socket.on(socketEventNames.userBack, userBack);\n socket.on(socketEventNames.connectError, connectError);\n\n}"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getRuntimeSyncMaps,
|
|
7
7
|
validateRequest,
|
|
8
8
|
extractTokenFromSocket,
|
|
9
|
-
formatRoomName,
|
|
9
|
+
formatRoomName as formatRoomName2,
|
|
10
10
|
getIoInstance as getIoInstance2,
|
|
11
11
|
tryCatch as tryCatch2,
|
|
12
12
|
parseTransportRouteName,
|
|
@@ -25,12 +25,13 @@ import {
|
|
|
25
25
|
setCurrentErrorTrackerIdentity,
|
|
26
26
|
registerSyncAbortController,
|
|
27
27
|
unregisterSyncAbortController,
|
|
28
|
-
|
|
28
|
+
resolveEnvKey
|
|
29
29
|
} from "@luckystack/core";
|
|
30
30
|
|
|
31
31
|
// src/_shared/streamEmitters.ts
|
|
32
32
|
import {
|
|
33
33
|
dispatchHook,
|
|
34
|
+
formatRoomName,
|
|
34
35
|
getIoInstance,
|
|
35
36
|
getLogger,
|
|
36
37
|
getProjectConfig as getProjectConfig2,
|
|
@@ -100,12 +101,14 @@ var collectRoomSocketsForPressure = (receiver) => {
|
|
|
100
101
|
var buildSyncStreamEmitters = ({
|
|
101
102
|
cb,
|
|
102
103
|
receiver,
|
|
104
|
+
userId,
|
|
103
105
|
resolvedName,
|
|
104
106
|
emitOriginatorChunk,
|
|
105
107
|
logLabel,
|
|
106
108
|
signal,
|
|
107
109
|
originatorSocket
|
|
108
110
|
}) => {
|
|
111
|
+
const physicalReceiver = !receiver || receiver === "all" ? receiver : formatRoomName(receiver, { purpose: "broadcast", userId: userId ?? null });
|
|
109
112
|
const chunkCounters = /* @__PURE__ */ new Map();
|
|
110
113
|
const bumpChunkIndex = (recipient) => {
|
|
111
114
|
const next = (chunkCounters.get(recipient) ?? 0) + 1;
|
|
@@ -152,7 +155,7 @@ var buildSyncStreamEmitters = ({
|
|
|
152
155
|
const io = getIoInstance();
|
|
153
156
|
if (!io) return;
|
|
154
157
|
dispatchStreamHooks(receiver, payload);
|
|
155
|
-
io.to(
|
|
158
|
+
io.to(physicalReceiver).emit(socketEventNames.sync, buildBroadcastFrame(payload));
|
|
156
159
|
};
|
|
157
160
|
const emitStreamToTokens = (tokens, payload = {}) => {
|
|
158
161
|
if (isAborted()) {
|
|
@@ -180,7 +183,7 @@ var buildSyncStreamEmitters = ({
|
|
|
180
183
|
const packetThreshold = Math.max(1, Math.ceil(effectiveThresholdBytes / AVG_PACKET_BYTES));
|
|
181
184
|
const targets = [];
|
|
182
185
|
if (originatorSocket) targets.push(originatorSocket);
|
|
183
|
-
for (const sock of collectRoomSocketsForPressure(
|
|
186
|
+
for (const sock of collectRoomSocketsForPressure(physicalReceiver)) {
|
|
184
187
|
if (sock !== originatorSocket) targets.push(sock);
|
|
185
188
|
}
|
|
186
189
|
if (targets.length === 0) return;
|
|
@@ -278,7 +281,7 @@ var processClientSyncForRecipient = async ({
|
|
|
278
281
|
sourceUserId,
|
|
279
282
|
//? Redact the raw bearer session token before it lands in the
|
|
280
283
|
//? error-tracker context (`captureException`) — a verbatim token there is
|
|
281
|
-
//? a usable credential (defeats HttpOnly-cookie mode). The
|
|
284
|
+
//? a usable credential (defeats HttpOnly-cookie mode). The 4-char prefix
|
|
282
285
|
//? still correlates error events.
|
|
283
286
|
targetToken: redactToken(tempToken),
|
|
284
287
|
receiver,
|
|
@@ -362,7 +365,6 @@ var authorizeSyncReceiver = ({
|
|
|
362
365
|
// src/handleSyncRequest.ts
|
|
363
366
|
var applySyncRateLimits = async ({
|
|
364
367
|
resolvedName,
|
|
365
|
-
token,
|
|
366
368
|
socket,
|
|
367
369
|
user,
|
|
368
370
|
responseIndex,
|
|
@@ -381,8 +383,8 @@ var applySyncRateLimits = async ({
|
|
|
381
383
|
if (defaultApiLimit !== false && defaultApiLimit > 0) {
|
|
382
384
|
const identityCb = config.rateLimiting.identity;
|
|
383
385
|
const customIdentity = identityCb?.({ routeName: resolvedName, userId: user?.id ?? null, ip: resolvedIp, transport: "socket" }) ?? null;
|
|
384
|
-
const requesterIdentity = customIdentity?.id ??
|
|
385
|
-
const keyPrefix = customIdentity?.scope ?? (
|
|
386
|
+
const requesterIdentity = customIdentity?.id ?? user?.id ?? resolvedIp;
|
|
387
|
+
const keyPrefix = customIdentity?.scope ?? (user?.id ? "user" : "ip");
|
|
386
388
|
const rateLimitKey = `${keyPrefix}:${requesterIdentity}:sync:${resolvedName}`;
|
|
387
389
|
const { allowed, resetIn } = await checkRateLimit({
|
|
388
390
|
key: rateLimitKey,
|
|
@@ -394,14 +396,14 @@ var applySyncRateLimits = async ({
|
|
|
394
396
|
//? Parity with the API handler: an anonymous per-route bucket is IP-keyed,
|
|
395
397
|
//? so the scope is `ip` (with `route` still set to mark it a per-route
|
|
396
398
|
//? bucket vs the global `:sync:all` IP bucket), never `route`.
|
|
397
|
-
scope:
|
|
399
|
+
scope: user?.id ? "user" : "ip",
|
|
398
400
|
key: rateLimitKey,
|
|
399
401
|
limit: defaultApiLimit,
|
|
400
402
|
windowMs: config.rateLimiting.windowMs,
|
|
401
403
|
count: defaultApiLimit + 1,
|
|
402
404
|
route: resolvedName,
|
|
403
405
|
userId: user?.id,
|
|
404
|
-
ip:
|
|
406
|
+
ip: user?.id ? void 0 : resolvedIp
|
|
405
407
|
});
|
|
406
408
|
if (typeof responseIndex === "number") {
|
|
407
409
|
socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
|
|
@@ -419,7 +421,7 @@ var applySyncRateLimits = async ({
|
|
|
419
421
|
}
|
|
420
422
|
}
|
|
421
423
|
const defaultIpLimit = config.rateLimiting.defaultIpLimit;
|
|
422
|
-
const requesterIsLoopback = config.rateLimiting.skipLoopbackInDev &&
|
|
424
|
+
const requesterIsLoopback = config.rateLimiting.skipLoopbackInDev && resolveEnvKey() !== "production" && isLoopbackIp(resolvedIp);
|
|
423
425
|
if (!requesterIsLoopback && defaultIpLimit !== false && defaultIpLimit > 0) {
|
|
424
426
|
const requesterIp = resolvedIp;
|
|
425
427
|
const ipKey = `ip:${requesterIp}:sync:all`;
|
|
@@ -475,7 +477,7 @@ function validateSyncMessage({
|
|
|
475
477
|
socket,
|
|
476
478
|
preferredLocale
|
|
477
479
|
}) {
|
|
478
|
-
if (typeof msg
|
|
480
|
+
if (typeof msg !== "object" || msg === null || Array.isArray(msg)) {
|
|
479
481
|
if (shouldLogDev()) {
|
|
480
482
|
getLogger3().warn("sync: socket message was not a json object");
|
|
481
483
|
}
|
|
@@ -662,7 +664,15 @@ function runReceiverAuth({
|
|
|
662
664
|
receiver,
|
|
663
665
|
allowClientReceiverAll: syncConfig.allowClientReceiverAll,
|
|
664
666
|
requireRoomMembership: syncConfig.requireRoomMembership,
|
|
665
|
-
|
|
667
|
+
//? Sockets physically JOIN the FORMATTED room (loadSocket runs the room name
|
|
668
|
+
//? through formatRoomName at join time), so membership must be tested against
|
|
669
|
+
//? the SAME physical name the fanout fetch uses (see physicalReceiver below) —
|
|
670
|
+
//? NOT the raw client `receiver`. Otherwise a non-identity
|
|
671
|
+
//? registerRoomNameFormatter (multi-tenant) makes every joined member fail the
|
|
672
|
+
//? check on the socket transport while HTTP still works.
|
|
673
|
+
isMember: () => socket.rooms.has(
|
|
674
|
+
receiver === "all" ? "all" : formatRoomName2(receiver, { purpose: "broadcast", userId: user?.id ?? null })
|
|
675
|
+
)
|
|
666
676
|
});
|
|
667
677
|
if (!receiverAuth.allowed) {
|
|
668
678
|
if (shouldLogDev()) {
|
|
@@ -897,11 +907,10 @@ async function runSyncFanout({
|
|
|
897
907
|
cleanupRequest,
|
|
898
908
|
ignoreSelf,
|
|
899
909
|
token,
|
|
900
|
-
cb,
|
|
901
910
|
syncObject,
|
|
902
911
|
functionsObject
|
|
903
912
|
}) {
|
|
904
|
-
const physicalReceiver = receiver === "all" ? "all" :
|
|
913
|
+
const physicalReceiver = receiver === "all" ? "all" : formatRoomName2(receiver, { purpose: "broadcast", userId: user?.id ?? null });
|
|
905
914
|
const sockets = physicalReceiver === "all" ? await ioInstance.fetchSockets() : await ioInstance.in(physicalReceiver).fetchSockets();
|
|
906
915
|
if (sockets.length === 0) {
|
|
907
916
|
if (shouldLogDev()) {
|
|
@@ -982,7 +991,13 @@ async function runSyncFanout({
|
|
|
982
991
|
serverOutput: effectiveServerOutput,
|
|
983
992
|
receiver,
|
|
984
993
|
resolvedName,
|
|
985
|
-
|
|
994
|
+
//? Server-authoritative callback key (SYNC security): route recipient
|
|
995
|
+
//? callbacks off the SERVER-resolved route, never the client-supplied
|
|
996
|
+
//? `cb`. Otherwise a room co-member could send a legit `name` but a `cb`
|
|
997
|
+
//? for a DIFFERENT route and fire other clients' handlers for that route
|
|
998
|
+
//? (cross-route callback spoofing). The client `cb` stays only for the
|
|
999
|
+
//? originator's own cancel handshake.
|
|
1000
|
+
callbackKey: resolvedName,
|
|
986
1001
|
transport: "socket",
|
|
987
1002
|
handlerName: "handleSyncRequest",
|
|
988
1003
|
logLabel: "sync",
|
|
@@ -996,7 +1011,10 @@ async function runSyncFanout({
|
|
|
996
1011
|
});
|
|
997
1012
|
} else {
|
|
998
1013
|
const result = {
|
|
999
|
-
cb
|
|
1014
|
+
//? Server-authoritative cb (SYNC security): emit the SERVER-resolved
|
|
1015
|
+
//? route so a spoofed client `cb` can't route this payload to a different
|
|
1016
|
+
//? route's callbacks on recipients. Mirrors the _client-path callbackKey.
|
|
1017
|
+
cb: resolvedName,
|
|
1000
1018
|
fullName: resolvedName,
|
|
1001
1019
|
serverOutput: effectiveServerOutput,
|
|
1002
1020
|
clientOutput: {},
|
|
@@ -1076,6 +1094,7 @@ async function handleSyncRequestInner({ msg, socket, token }) {
|
|
|
1076
1094
|
const { emitServerSyncStream, emitBroadcastSyncStream, emitStreamToTokens, flushPressure } = buildSyncStreamEmitters({
|
|
1077
1095
|
cb,
|
|
1078
1096
|
receiver,
|
|
1097
|
+
userId: user?.id ?? null,
|
|
1079
1098
|
resolvedName,
|
|
1080
1099
|
logLabel: "sync",
|
|
1081
1100
|
signal: abortController.signal,
|
|
@@ -1174,7 +1193,7 @@ import {
|
|
|
1174
1193
|
getRuntimeSyncMaps as getRuntimeSyncMapsFromSource,
|
|
1175
1194
|
validateRequest as validateRequest2,
|
|
1176
1195
|
extractTokenFromSocket as extractTokenFromSocket2,
|
|
1177
|
-
formatRoomName as
|
|
1196
|
+
formatRoomName as formatRoomName3,
|
|
1178
1197
|
getIoInstance as getIoInstance3,
|
|
1179
1198
|
tryCatch as tryCatch3,
|
|
1180
1199
|
parseTransportRouteName as parseTransportRouteName2,
|
|
@@ -1185,13 +1204,12 @@ import {
|
|
|
1185
1204
|
getLogger as getLogger4,
|
|
1186
1205
|
runWithErrorTrackerIdentityScope as runWithErrorTrackerIdentityScope2,
|
|
1187
1206
|
setCurrentErrorTrackerIdentity as setCurrentErrorTrackerIdentity2,
|
|
1188
|
-
deriveTokenBucketId as deriveTokenBucketId2,
|
|
1189
1207
|
isLoopbackIp as isLoopbackIp2,
|
|
1190
|
-
extractLanguageFromHeader as extractLanguageFromHeader2
|
|
1208
|
+
extractLanguageFromHeader as extractLanguageFromHeader2,
|
|
1209
|
+
resolveEnvKey as resolveEnvKey2
|
|
1191
1210
|
} from "@luckystack/core";
|
|
1192
1211
|
var applyHttpSyncRateLimits = async ({
|
|
1193
1212
|
resolvedName,
|
|
1194
|
-
token,
|
|
1195
1213
|
requesterIp,
|
|
1196
1214
|
user,
|
|
1197
1215
|
buildSyncError,
|
|
@@ -1203,8 +1221,8 @@ var applyHttpSyncRateLimits = async ({
|
|
|
1203
1221
|
if (effectiveSyncLimit !== false && effectiveSyncLimit > 0) {
|
|
1204
1222
|
const identityCb = config.rateLimiting.identity;
|
|
1205
1223
|
const customIdentity = identityCb?.({ routeName: resolvedName, userId: user?.id ?? null, ip: requesterIp ?? "", transport: "http" }) ?? null;
|
|
1206
|
-
const requesterIdentity = customIdentity?.id ??
|
|
1207
|
-
const keyPrefix = customIdentity?.scope ?? (
|
|
1224
|
+
const requesterIdentity = customIdentity?.id ?? user?.id ?? (requesterIp ?? "unknown");
|
|
1225
|
+
const keyPrefix = customIdentity?.scope ?? (user?.id ? "user" : "ip");
|
|
1208
1226
|
const rateLimitKey = `${keyPrefix}:${requesterIdentity}:sync:${resolvedName}`;
|
|
1209
1227
|
const { allowed, resetIn } = await checkRateLimit2({
|
|
1210
1228
|
key: rateLimitKey,
|
|
@@ -1216,14 +1234,14 @@ var applyHttpSyncRateLimits = async ({
|
|
|
1216
1234
|
//? Parity with the API handler: an anonymous per-route bucket is IP-keyed,
|
|
1217
1235
|
//? so the scope is `ip` (with `route` still set to mark it a per-route
|
|
1218
1236
|
//? bucket vs the global `:sync:all` IP bucket), never `route`.
|
|
1219
|
-
scope:
|
|
1237
|
+
scope: user?.id ? "user" : "ip",
|
|
1220
1238
|
key: rateLimitKey,
|
|
1221
1239
|
limit: effectiveSyncLimit,
|
|
1222
1240
|
windowMs: config.rateLimiting.windowMs,
|
|
1223
1241
|
count: effectiveSyncLimit + 1,
|
|
1224
1242
|
route: resolvedName,
|
|
1225
1243
|
userId: user?.id,
|
|
1226
|
-
ip:
|
|
1244
|
+
ip: user?.id ? void 0 : requesterIp
|
|
1227
1245
|
});
|
|
1228
1246
|
return buildSyncError({
|
|
1229
1247
|
response: {
|
|
@@ -1238,7 +1256,7 @@ var applyHttpSyncRateLimits = async ({
|
|
|
1238
1256
|
}
|
|
1239
1257
|
}
|
|
1240
1258
|
const defaultIpLimit = config.rateLimiting.defaultIpLimit;
|
|
1241
|
-
const requesterIsLoopback = config.rateLimiting.skipLoopbackInDev &&
|
|
1259
|
+
const requesterIsLoopback = config.rateLimiting.skipLoopbackInDev && resolveEnvKey2() !== "production" && isLoopbackIp2(requesterIp ?? "");
|
|
1242
1260
|
if (!requesterIsLoopback && defaultIpLimit !== false && defaultIpLimit > 0) {
|
|
1243
1261
|
const ipBucket = requesterIp ?? "unknown";
|
|
1244
1262
|
const ipKey = `ip:${ipBucket}:sync:all`;
|
|
@@ -1271,7 +1289,7 @@ var applyHttpSyncRateLimits = async ({
|
|
|
1271
1289
|
return null;
|
|
1272
1290
|
};
|
|
1273
1291
|
async function stageResolveRoute(ctx) {
|
|
1274
|
-
const { name, cb, normalizedReceiver, user, buildSyncError, preferredLocale } = ctx;
|
|
1292
|
+
const { name, cb, data, normalizedReceiver, user, buildSyncError, preferredLocale } = ctx;
|
|
1275
1293
|
if (!name || typeof name !== "string") {
|
|
1276
1294
|
return {
|
|
1277
1295
|
error: buildSyncError({
|
|
@@ -1281,6 +1299,15 @@ async function stageResolveRoute(ctx) {
|
|
|
1281
1299
|
})
|
|
1282
1300
|
};
|
|
1283
1301
|
}
|
|
1302
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
1303
|
+
return {
|
|
1304
|
+
error: buildSyncError({
|
|
1305
|
+
response: { status: "error", errorCode: "sync.invalidRequest" },
|
|
1306
|
+
preferred: preferredLocale,
|
|
1307
|
+
userLanguage: user?.language
|
|
1308
|
+
})
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1284
1311
|
const parsedRoute = parseTransportRouteName2({ value: name, prefix: "sync" });
|
|
1285
1312
|
if (parsedRoute.status === "error") {
|
|
1286
1313
|
return {
|
|
@@ -1477,6 +1504,7 @@ async function stageExecuteServer(resolvedName, serverSyncEntry, functionsObject
|
|
|
1477
1504
|
const { emitServerSyncStream, emitBroadcastSyncStream, emitStreamToTokens, flushPressure } = buildSyncStreamEmitters({
|
|
1478
1505
|
cb,
|
|
1479
1506
|
receiver: normalizedReceiver,
|
|
1507
|
+
userId: user?.id ?? null,
|
|
1480
1508
|
resolvedName,
|
|
1481
1509
|
logLabel: "http sync",
|
|
1482
1510
|
signal: effectiveAbortSignal,
|
|
@@ -1595,10 +1623,17 @@ async function stageFanout(resolvedName, callbackName, serverOutput, syncObject,
|
|
|
1595
1623
|
userLanguage: user?.language
|
|
1596
1624
|
});
|
|
1597
1625
|
}
|
|
1598
|
-
const physicalReceiver = normalizedReceiver === "all" ? "all" :
|
|
1626
|
+
const physicalReceiver = normalizedReceiver === "all" ? "all" : formatRoomName3(normalizedReceiver, { purpose: "broadcast", userId: user?.id ?? null });
|
|
1599
1627
|
const sockets = physicalReceiver === "all" ? await ioInstance.fetchSockets() : await ioInstance.in(physicalReceiver).fetchSockets();
|
|
1600
1628
|
let recipientCount = 0;
|
|
1629
|
+
const { fanoutYieldEvery, fanoutYieldMs } = getProjectConfig4().sync;
|
|
1630
|
+
const yieldEvery = Math.max(1, fanoutYieldEvery);
|
|
1631
|
+
let fanoutIterCount = 1;
|
|
1601
1632
|
for (const tempSocket of sockets) {
|
|
1633
|
+
fanoutIterCount++;
|
|
1634
|
+
if (fanoutIterCount % yieldEvery === 0) {
|
|
1635
|
+
await new Promise((resolve) => setTimeout(resolve, fanoutYieldMs));
|
|
1636
|
+
}
|
|
1602
1637
|
const tempToken = extractTokenFromSocket2(tempSocket);
|
|
1603
1638
|
if (ignoreSelf && typeof ignoreSelf === "boolean" && token && token === tempToken) {
|
|
1604
1639
|
continue;
|