@luckystack/devkit 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/supervisor.ts"],"sourcesContent":["#!/usr/bin/env node\n//? CRITICAL INVARIANT: this process must NEVER merge `.env` files into its own\n//? `process.env`. The child is spawned with `{ ...process.env }`, and the\n//? child's own `loadEnvFiles()` loads `.env` with `override: false` — any\n//? `.env`-derived value that leaks into the supervisor's env would therefore\n//? WIN over freshly edited file values on every restart (stale-env bug).\n//? That is why this file imports NOTHING from `@luckystack/core` (core runs\n//? `bootstrapEnv()` as an import side-effect) and reads env files via\n//? `dotenv.parse` (pure — no `process.env` mutation). The previous design\n//? (an `ambientEnvSnapshot` module imported first) broke as soon as tsup\n//? inlined the snapshot into the entry body: ESM imports are hoisted, so\n//? core's side-effect ran BEFORE the snapshot line.\nimport { spawn, type ChildProcess } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { performance } from 'node:perf_hooks';\nimport path from 'node:path';\nimport { watch } from 'chokidar';\nimport { parse as parseDotenv } from 'dotenv';\n\nconst RESTART_DEBOUNCE_MS = 150;\nconst CRASH_RESTART_DELAY_MS = 300;\n//? Rapid-crash-loop breaker. A child that exits non-zero within\n//? FAST_CRASH_THRESHOLD_MS of booting counts as a \"fast crash\" (it died during\n//? startup rather than after serving). MAX_CONSECUTIVE_FAST_CRASHES of those in\n//? a row means the startup error is not transient (a port already in use, a\n//? syntax error, a missing env) — restarting forever just spams the console, so\n//? we give up and print an actionable message. A child that survives longer than\n//? the threshold resets the counter, so a normal restart loop is unaffected.\nconst FAST_CRASH_THRESHOLD_MS = 3000;\nconst MAX_CONSECUTIVE_FAST_CRASHES = 4;\n//? Force-exit grace: how long we wait for the child to honour SIGTERM before\n//? the supervisor hard-exits. Windows does not deliver SIGKILL so the only\n//? lever is this timer. 1 500 ms is intentionally short (dev restarts should\n//? be fast); increase via LUCKYSTACK_SUPERVISOR_GRACE_MS for slow servers.\nconst SHUTDOWN_GRACE_MS = Number(process.env.LUCKYSTACK_SUPERVISOR_GRACE_MS) || 1500;\n\n//? Mirrors `getEnvFiles()` from @luckystack/core — duplicated on purpose so the\n//? supervisor never imports core (see the invariant above). Keep in sync.\nconst DEFAULT_ENV_FILES = ['.env', '.env.local'];\nconst getEnvFiles = (): string[] => {\n const override = process.env.LUCKYSTACK_ENV_FILES;\n if (override) {\n const list = override.split(',').map((entry) => entry.trim()).filter(Boolean);\n if (list.length > 0) return list;\n }\n return DEFAULT_ENV_FILES;\n};\n\n//? Resolve NODE_ENV without mutating `process.env`: a real ambient env var\n//? wins; otherwise read the env files via the pure `dotenv.parse` (\"later\n//? overrides earlier\", matching loadEnvFiles' file order).\nconst resolveNodeEnv = (): string => {\n if (process.env.NODE_ENV) return process.env.NODE_ENV;\n let fromFiles: string | undefined;\n for (const file of getEnvFiles()) {\n const filePath = path.resolve(process.cwd(), file);\n if (!existsSync(filePath)) continue;\n const parsed = parseDotenv(readFileSync(filePath, 'utf8'));\n if (typeof parsed.NODE_ENV === 'string' && parsed.NODE_ENV.length > 0) fromFiles = parsed.NODE_ENV;\n }\n return fromFiles ?? 'development';\n};\n\n//? chokidar v5 removed glob support — a `server/bootstrap/**/*.ts` pattern is no\n//? longer expanded; it is watched as a LITERAL path that never exists, so changes\n//? under `server/bootstrap` / `server/auth` silently never triggered a restart.\n//? Watch the concrete files directly, and the bootstrap/auth DIRECTORIES\n//? recursively (chokidar's default for a directory), then filter events down to\n//? the file types the old globs targeted in the `all` handler below.\nconst CORE_WATCH_FILES = [\n 'config.ts',\n ...getEnvFiles(),\n 'server/server.ts',\n 'server/functions/db.ts',\n 'server/functions/redis.ts',\n 'server/functions/sentry.ts',\n];\n//? Only watch dirs that exist — chokidar watching a recursive non-existent\n//? directory is wasteful/odd; a dir created later is picked up on next boot.\nconst CORE_WATCH_DIRS = ['server/bootstrap', 'server/auth'].filter((dir) =>\n existsSync(path.resolve(process.cwd(), dir)),\n);\nconst CORE_WATCH_TARGETS = [...CORE_WATCH_FILES, ...CORE_WATCH_DIRS];\n\nconst tsxCliPath = path.resolve(process.cwd(), 'node_modules', 'tsx', 'dist', 'cli.mjs');\n//? Scaffolded projects carry a dedicated server tsconfig — pass it when present\n//? so the child matches a manual `tsx --tsconfig tsconfig.server.json` run.\nconst tsconfigServerArgs = existsSync(path.resolve(process.cwd(), 'tsconfig.server.json'))\n ? ['--tsconfig', 'tsconfig.server.json']\n : [];\nconst childArgs = [tsxCliPath, ...tsconfigServerArgs, 'server/server.ts'];\n\nlet childProcess: ChildProcess | null = null;\nlet pendingRestart = false;\nlet restartTimer: NodeJS.Timeout | null = null;\nlet crashRestartTimer: NodeJS.Timeout | null = null;\nlet childBootStartedAt = 0;\nlet consecutiveFastCrashes = 0;\nlet isShuttingDown = false;\n\nconst startChild = () => {\n if (isShuttingDown) return;\n childBootStartedAt = performance.now();\n const spawned = spawn(process.execPath, childArgs, {\n stdio: 'inherit',\n //? `process.env` is guaranteed `.env`-free here (see the invariant at the\n //? top of this file), so the child loads `.env` fresh on every restart and\n //? picks up edited values — mirroring a cold `npm run server` boot.\n env: {\n ...process.env,\n LUCKYSTACK_CORE_SUPERVISED: 'true',\n },\n });\n childProcess = spawned;\n\n console.log(`[Supervisor] Started server process (pid: ${String(spawned.pid)})`);\n\n //? `'error'` and `'exit'` can BOTH fire for a single spawn (a failed spawn\n //? emits `'error'`, and some platforms then emit `'exit'` too). This flag\n //? resolves THIS child's lifecycle exactly once regardless of event order, so\n //? we never double-schedule a restart.\n let handled = false;\n\n //? A failed `spawn` (ENOENT/EACCES — e.g. a missing `tsx`) emits `'error'`,\n //? NOT `'exit'`. Without this listener Node re-throws it as an uncaught\n //? exception that kills the SUPERVISOR instead of retrying — the dev wrapper\n //? would just disappear. Route it through the same crash-restart delay.\n spawned.on('error', (err) => {\n console.log(`[Supervisor] Failed to spawn server process: ${String(err)}`, 'red');\n if (handled) return;\n handled = true;\n childProcess = null;\n\n if (isShuttingDown) {\n process.exit(1);\n }\n\n console.log(`[Supervisor] Retrying server spawn in ${String(CRASH_RESTART_DELAY_MS)}ms`);\n crashRestartTimer = setTimeout(() => {\n crashRestartTimer = null;\n startChild();\n }, CRASH_RESTART_DELAY_MS);\n });\n\n spawned.on('exit', (code, signal) => {\n const uptimeMs = Math.round(performance.now() - childBootStartedAt);\n const shouldRestart = pendingRestart;\n\n if (handled) return;\n handled = true;\n childProcess = null;\n\n //? Once the user has asked us to stop, never spawn another child — no matter\n //? what the previous one's exit reason was. Without this guard a crash that\n //? races with Ctrl+C re-spawns indefinitely.\n if (isShuttingDown) {\n process.exit(0);\n }\n\n if (shouldRestart) {\n pendingRestart = false;\n //? A watcher-driven restart is an explicit edit to a watched file — give\n //? the new code a clean slate instead of inheriting a previous crash burst.\n consecutiveFastCrashes = 0;\n console.log(`[Supervisor] Restarting server after ${String(uptimeMs)}ms uptime`);\n startChild();\n return;\n }\n\n if (signal === 'SIGTERM' || signal === 'SIGINT') {\n return;\n }\n\n if (typeof code === 'number' && code !== 0) {\n //? Rapid-crash-loop breaker: a child that died within the boot window\n //? counts toward the consecutive-fast-crash tally; one that lived longer\n //? proves the startup error cleared, so reset the tally.\n if (uptimeMs < FAST_CRASH_THRESHOLD_MS) {\n consecutiveFastCrashes += 1;\n } else {\n consecutiveFastCrashes = 0;\n }\n\n if (consecutiveFastCrashes >= MAX_CONSECUTIVE_FAST_CRASHES) {\n console.log(\n `[Supervisor] Server crashed ${String(consecutiveFastCrashes)} times within ` +\n `${String(FAST_CRASH_THRESHOLD_MS / 1000)}s of starting — giving up. Fix the startup ` +\n `error above (e.g. a port already in use), then re-run \\`npm run server\\`.`,\n );\n process.exit(1);\n }\n\n console.log(`[Supervisor] Server crashed with code ${String(code)}. Restarting in ${String(CRASH_RESTART_DELAY_MS)}ms`);\n crashRestartTimer = setTimeout(() => {\n crashRestartTimer = null;\n startChild();\n }, CRASH_RESTART_DELAY_MS);\n }\n });\n};\n\nconst scheduleRestart = ({\n event,\n changedPath,\n}: {\n event: string;\n changedPath: string;\n}) => {\n if (restartTimer) {\n clearTimeout(restartTimer);\n }\n\n restartTimer = setTimeout(() => {\n restartTimer = null;\n pendingRestart = true;\n\n console.log(`[Supervisor] Core change detected (${event}): ${changedPath}. Restarting server`);\n\n if (!childProcess) {\n pendingRestart = false;\n startChild();\n return;\n }\n\n childProcess.kill('SIGTERM');\n }, RESTART_DEBOUNCE_MS);\n};\n\nconst shutdownSupervisor = () => {\n isShuttingDown = true;\n\n if (restartTimer) {\n clearTimeout(restartTimer);\n restartTimer = null;\n }\n if (crashRestartTimer) {\n clearTimeout(crashRestartTimer);\n crashRestartTimer = null;\n }\n\n pendingRestart = false;\n\n if (childProcess) {\n //? Child is still alive — its 'exit' handler will see isShuttingDown\n //? and call process.exit(0) once the child is gone. Force-exit after a\n //? short grace period in case the child ignores SIGTERM (Windows).\n childProcess.kill('SIGTERM');\n setTimeout(() => process.exit(0), SHUTDOWN_GRACE_MS).unref();\n } else {\n //? No child running (e.g. we were between crashes, sitting in the\n //? crash-restart setTimeout we just cleared above) — exit immediately.\n process.exit(0);\n }\n};\n\nif (resolveNodeEnv() === 'production') {\n //? In production the supervisor has no file watcher, but it still needs to\n //? forward shutdown signals so the child gets a chance to drain (e.g. flush\n //? error trackers, close DB connections). Without these handlers a SIGTERM\n //? from a process manager (systemd, Docker) kills the supervisor instantly\n //? and the child may never exit cleanly.\n process.on('SIGINT', () => {\n isShuttingDown = true;\n shutdownSupervisor();\n });\n process.on('SIGTERM', () => {\n isShuttingDown = true;\n shutdownSupervisor();\n });\n startChild();\n} else {\n const watcher = watch(CORE_WATCH_TARGETS, {\n ignoreInitial: true,\n awaitWriteFinish: {\n stabilityThreshold: 120,\n pollInterval: 20,\n },\n });\n\n watcher.on('all', (event, changedPath) => {\n //? Ignore pure directory churn from the recursively-watched bootstrap/auth dirs.\n if (event === 'addDir' || event === 'unlinkDir') return;\n //? Restore the old `**/*.ts` glob intent: within the watched directories only\n //? TS sources matter. A `.ts` suffix covers every concrete watched TS file too\n //? (config.ts, server.ts, functions/*.ts); the non-TS watched files are the\n //? `.env*` set, matched explicitly. A stray non-TS file under a watched dir is\n //? correctly ignored.\n const normalized = changedPath.replaceAll('\\\\', '/');\n const relevant = normalized.endsWith('.ts') || CORE_WATCH_FILES.some((file) => normalized.endsWith(file));\n if (!relevant) return;\n scheduleRestart({ event, changedPath });\n });\n\n const handleSignal = (signalName: string) => {\n //? Set the flag synchronously so any in-flight child 'exit' handler\n //? running BEFORE watcher.close() resolves won't schedule a new spawn.\n if (isShuttingDown) {\n //? Second Ctrl+C — user is impatient. Hard-exit immediately.\n process.exit(1);\n }\n isShuttingDown = true;\n console.log(`[Supervisor] Received ${signalName}, shutting down`);\n void watcher.close().then(() => {\n shutdownSupervisor();\n }).catch(() => {\n shutdownSupervisor();\n });\n };\n\n process.on('SIGINT', () => { handleSignal('SIGINT'); });\n process.on('SIGTERM', () => { handleSignal('SIGTERM'); });\n\n startChild();\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/supervisor.ts"],"sourcesContent":["#!/usr/bin/env node\r\n//? CRITICAL INVARIANT: this process must NEVER merge `.env` files into its own\r\n//? `process.env`. The child is spawned with `{ ...process.env }`, and the\r\n//? child's own `loadEnvFiles()` loads `.env` with `override: false` — any\r\n//? `.env`-derived value that leaks into the supervisor's env would therefore\r\n//? WIN over freshly edited file values on every restart (stale-env bug).\r\n//? That is why this file imports NOTHING from `@luckystack/core` (core runs\r\n//? `bootstrapEnv()` as an import side-effect) and reads env files via\r\n//? `dotenv.parse` (pure — no `process.env` mutation). The previous design\r\n//? (an `ambientEnvSnapshot` module imported first) broke as soon as tsup\r\n//? inlined the snapshot into the entry body: ESM imports are hoisted, so\r\n//? core's side-effect ran BEFORE the snapshot line.\r\nimport { spawn, type ChildProcess } from 'node:child_process';\r\nimport { existsSync, readFileSync } from 'node:fs';\r\nimport { performance } from 'node:perf_hooks';\r\nimport path from 'node:path';\r\nimport { watch } from 'chokidar';\r\nimport { parse as parseDotenv } from 'dotenv';\r\n\r\nconst RESTART_DEBOUNCE_MS = 150;\r\nconst CRASH_RESTART_DELAY_MS = 300;\r\n//? Rapid-crash-loop breaker. A child that exits non-zero within\r\n//? FAST_CRASH_THRESHOLD_MS of booting counts as a \"fast crash\" (it died during\r\n//? startup rather than after serving). MAX_CONSECUTIVE_FAST_CRASHES of those in\r\n//? a row means the startup error is not transient (a port already in use, a\r\n//? syntax error, a missing env) — restarting forever just spams the console, so\r\n//? we give up and print an actionable message. A child that survives longer than\r\n//? the threshold resets the counter, so a normal restart loop is unaffected.\r\nconst FAST_CRASH_THRESHOLD_MS = 3000;\r\nconst MAX_CONSECUTIVE_FAST_CRASHES = 4;\r\n//? Force-exit grace: how long we wait for the child to honour SIGTERM before\r\n//? the supervisor hard-exits. Windows does not deliver SIGKILL so the only\r\n//? lever is this timer. 1 500 ms is intentionally short (dev restarts should\r\n//? be fast); increase via LUCKYSTACK_SUPERVISOR_GRACE_MS for slow servers.\r\nconst SHUTDOWN_GRACE_MS = Number(process.env.LUCKYSTACK_SUPERVISOR_GRACE_MS) || 1500;\r\n\r\n//? Mirrors `getEnvFiles()` from @luckystack/core — duplicated on purpose so the\r\n//? supervisor never imports core (see the invariant above). Keep in sync.\r\nconst DEFAULT_ENV_FILES = ['.env', '.env.local'];\r\nconst getEnvFiles = (): string[] => {\r\n const override = process.env.LUCKYSTACK_ENV_FILES;\r\n if (override) {\r\n const list = override.split(',').map((entry) => entry.trim()).filter(Boolean);\r\n if (list.length > 0) return list;\r\n }\r\n return DEFAULT_ENV_FILES;\r\n};\r\n\r\n//? Resolve NODE_ENV without mutating `process.env`: a real ambient env var\r\n//? wins; otherwise read the env files via the pure `dotenv.parse` (\"later\r\n//? overrides earlier\", matching loadEnvFiles' file order).\r\nconst resolveNodeEnv = (): string => {\r\n if (process.env.NODE_ENV) return process.env.NODE_ENV;\r\n let fromFiles: string | undefined;\r\n for (const file of getEnvFiles()) {\r\n const filePath = path.resolve(process.cwd(), file);\r\n if (!existsSync(filePath)) continue;\r\n const parsed = parseDotenv(readFileSync(filePath, 'utf8'));\r\n if (typeof parsed.NODE_ENV === 'string' && parsed.NODE_ENV.length > 0) fromFiles = parsed.NODE_ENV;\r\n }\r\n return fromFiles ?? 'development';\r\n};\r\n\r\n//? chokidar v5 removed glob support — a `server/bootstrap/**/*.ts` pattern is no\r\n//? longer expanded; it is watched as a LITERAL path that never exists, so changes\r\n//? under `server/bootstrap` / `server/auth` silently never triggered a restart.\r\n//? Watch the concrete files directly, and the bootstrap/auth DIRECTORIES\r\n//? recursively (chokidar's default for a directory), then filter events down to\r\n//? the file types the old globs targeted in the `all` handler below.\r\nconst CORE_WATCH_FILES = [\r\n 'config.ts',\r\n ...getEnvFiles(),\r\n 'server/server.ts',\r\n 'server/functions/db.ts',\r\n 'server/functions/redis.ts',\r\n 'server/functions/sentry.ts',\r\n];\r\n//? Only watch dirs that exist — chokidar watching a recursive non-existent\r\n//? directory is wasteful/odd; a dir created later is picked up on next boot.\r\nconst CORE_WATCH_DIRS = ['server/bootstrap', 'server/auth'].filter((dir) =>\r\n existsSync(path.resolve(process.cwd(), dir)),\r\n);\r\nconst CORE_WATCH_TARGETS = [...CORE_WATCH_FILES, ...CORE_WATCH_DIRS];\r\n\r\nconst tsxCliPath = path.resolve(process.cwd(), 'node_modules', 'tsx', 'dist', 'cli.mjs');\r\n//? Scaffolded projects carry a dedicated server tsconfig — pass it when present\r\n//? so the child matches a manual `tsx --tsconfig tsconfig.server.json` run.\r\nconst tsconfigServerArgs = existsSync(path.resolve(process.cwd(), 'tsconfig.server.json'))\r\n ? ['--tsconfig', 'tsconfig.server.json']\r\n : [];\r\nconst childArgs = [tsxCliPath, ...tsconfigServerArgs, 'server/server.ts'];\r\n\r\nlet childProcess: ChildProcess | null = null;\r\nlet pendingRestart = false;\r\nlet restartTimer: NodeJS.Timeout | null = null;\r\nlet crashRestartTimer: NodeJS.Timeout | null = null;\r\nlet childBootStartedAt = 0;\r\nlet consecutiveFastCrashes = 0;\r\nlet isShuttingDown = false;\r\n\r\nconst startChild = () => {\r\n if (isShuttingDown) return;\r\n childBootStartedAt = performance.now();\r\n const spawned = spawn(process.execPath, childArgs, {\r\n stdio: 'inherit',\r\n //? `process.env` is guaranteed `.env`-free here (see the invariant at the\r\n //? top of this file), so the child loads `.env` fresh on every restart and\r\n //? picks up edited values — mirroring a cold `npm run server` boot.\r\n env: {\r\n ...process.env,\r\n LUCKYSTACK_CORE_SUPERVISED: 'true',\r\n },\r\n });\r\n childProcess = spawned;\r\n\r\n console.log(`[Supervisor] Started server process (pid: ${String(spawned.pid)})`);\r\n\r\n //? `'error'` and `'exit'` can BOTH fire for a single spawn (a failed spawn\r\n //? emits `'error'`, and some platforms then emit `'exit'` too). This flag\r\n //? resolves THIS child's lifecycle exactly once regardless of event order, so\r\n //? we never double-schedule a restart.\r\n let handled = false;\r\n\r\n //? A failed `spawn` (ENOENT/EACCES — e.g. a missing `tsx`) emits `'error'`,\r\n //? NOT `'exit'`. Without this listener Node re-throws it as an uncaught\r\n //? exception that kills the SUPERVISOR instead of retrying — the dev wrapper\r\n //? would just disappear. Route it through the same crash-restart delay.\r\n spawned.on('error', (err) => {\r\n console.log(`[Supervisor] Failed to spawn server process: ${String(err)}`, 'red');\r\n if (handled) return;\r\n handled = true;\r\n childProcess = null;\r\n\r\n if (isShuttingDown) {\r\n process.exit(1);\r\n }\r\n\r\n console.log(`[Supervisor] Retrying server spawn in ${String(CRASH_RESTART_DELAY_MS)}ms`);\r\n crashRestartTimer = setTimeout(() => {\r\n crashRestartTimer = null;\r\n startChild();\r\n }, CRASH_RESTART_DELAY_MS);\r\n });\r\n\r\n spawned.on('exit', (code, signal) => {\r\n const uptimeMs = Math.round(performance.now() - childBootStartedAt);\r\n const shouldRestart = pendingRestart;\r\n\r\n if (handled) return;\r\n handled = true;\r\n childProcess = null;\r\n\r\n //? Once the user has asked us to stop, never spawn another child — no matter\r\n //? what the previous one's exit reason was. Without this guard a crash that\r\n //? races with Ctrl+C re-spawns indefinitely.\r\n if (isShuttingDown) {\r\n process.exit(0);\r\n }\r\n\r\n if (shouldRestart) {\r\n pendingRestart = false;\r\n //? A watcher-driven restart is an explicit edit to a watched file — give\r\n //? the new code a clean slate instead of inheriting a previous crash burst.\r\n consecutiveFastCrashes = 0;\r\n console.log(`[Supervisor] Restarting server after ${String(uptimeMs)}ms uptime`);\r\n startChild();\r\n return;\r\n }\r\n\r\n if (signal === 'SIGTERM' || signal === 'SIGINT') {\r\n return;\r\n }\r\n\r\n if (typeof code === 'number' && code !== 0) {\r\n //? Rapid-crash-loop breaker: a child that died within the boot window\r\n //? counts toward the consecutive-fast-crash tally; one that lived longer\r\n //? proves the startup error cleared, so reset the tally.\r\n if (uptimeMs < FAST_CRASH_THRESHOLD_MS) {\r\n consecutiveFastCrashes += 1;\r\n } else {\r\n consecutiveFastCrashes = 0;\r\n }\r\n\r\n if (consecutiveFastCrashes >= MAX_CONSECUTIVE_FAST_CRASHES) {\r\n console.log(\r\n `[Supervisor] Server crashed ${String(consecutiveFastCrashes)} times within ` +\r\n `${String(FAST_CRASH_THRESHOLD_MS / 1000)}s of starting — giving up. Fix the startup ` +\r\n `error above (e.g. a port already in use), then re-run \\`npm run server\\`.`,\r\n );\r\n process.exit(1);\r\n }\r\n\r\n console.log(`[Supervisor] Server crashed with code ${String(code)}. Restarting in ${String(CRASH_RESTART_DELAY_MS)}ms`);\r\n crashRestartTimer = setTimeout(() => {\r\n crashRestartTimer = null;\r\n startChild();\r\n }, CRASH_RESTART_DELAY_MS);\r\n }\r\n });\r\n};\r\n\r\nconst scheduleRestart = ({\r\n event,\r\n changedPath,\r\n}: {\r\n event: string;\r\n changedPath: string;\r\n}) => {\r\n if (restartTimer) {\r\n clearTimeout(restartTimer);\r\n }\r\n\r\n restartTimer = setTimeout(() => {\r\n restartTimer = null;\r\n pendingRestart = true;\r\n\r\n console.log(`[Supervisor] Core change detected (${event}): ${changedPath}. Restarting server`);\r\n\r\n if (!childProcess) {\r\n pendingRestart = false;\r\n startChild();\r\n return;\r\n }\r\n\r\n childProcess.kill('SIGTERM');\r\n }, RESTART_DEBOUNCE_MS);\r\n};\r\n\r\nconst shutdownSupervisor = () => {\r\n isShuttingDown = true;\r\n\r\n if (restartTimer) {\r\n clearTimeout(restartTimer);\r\n restartTimer = null;\r\n }\r\n if (crashRestartTimer) {\r\n clearTimeout(crashRestartTimer);\r\n crashRestartTimer = null;\r\n }\r\n\r\n pendingRestart = false;\r\n\r\n if (childProcess) {\r\n //? Child is still alive — its 'exit' handler will see isShuttingDown\r\n //? and call process.exit(0) once the child is gone. Force-exit after a\r\n //? short grace period in case the child ignores SIGTERM (Windows).\r\n childProcess.kill('SIGTERM');\r\n setTimeout(() => process.exit(0), SHUTDOWN_GRACE_MS).unref();\r\n } else {\r\n //? No child running (e.g. we were between crashes, sitting in the\r\n //? crash-restart setTimeout we just cleared above) — exit immediately.\r\n process.exit(0);\r\n }\r\n};\r\n\r\nif (resolveNodeEnv() === 'production') {\r\n //? In production the supervisor has no file watcher, but it still needs to\r\n //? forward shutdown signals so the child gets a chance to drain (e.g. flush\r\n //? error trackers, close DB connections). Without these handlers a SIGTERM\r\n //? from a process manager (systemd, Docker) kills the supervisor instantly\r\n //? and the child may never exit cleanly.\r\n process.on('SIGINT', () => {\r\n isShuttingDown = true;\r\n shutdownSupervisor();\r\n });\r\n process.on('SIGTERM', () => {\r\n isShuttingDown = true;\r\n shutdownSupervisor();\r\n });\r\n startChild();\r\n} else {\r\n const watcher = watch(CORE_WATCH_TARGETS, {\r\n ignoreInitial: true,\r\n awaitWriteFinish: {\r\n stabilityThreshold: 120,\r\n pollInterval: 20,\r\n },\r\n });\r\n\r\n watcher.on('all', (event, changedPath) => {\r\n //? Ignore pure directory churn from the recursively-watched bootstrap/auth dirs.\r\n if (event === 'addDir' || event === 'unlinkDir') return;\r\n //? Restore the old `**/*.ts` glob intent: within the watched directories only\r\n //? TS sources matter. A `.ts` suffix covers every concrete watched TS file too\r\n //? (config.ts, server.ts, functions/*.ts); the non-TS watched files are the\r\n //? `.env*` set, matched explicitly. A stray non-TS file under a watched dir is\r\n //? correctly ignored.\r\n const normalized = changedPath.replaceAll('\\\\', '/');\r\n const relevant = normalized.endsWith('.ts') || CORE_WATCH_FILES.some((file) => normalized.endsWith(file));\r\n if (!relevant) return;\r\n scheduleRestart({ event, changedPath });\r\n });\r\n\r\n const handleSignal = (signalName: string) => {\r\n //? Set the flag synchronously so any in-flight child 'exit' handler\r\n //? running BEFORE watcher.close() resolves won't schedule a new spawn.\r\n if (isShuttingDown) {\r\n //? Second Ctrl+C — user is impatient. Hard-exit immediately.\r\n process.exit(1);\r\n }\r\n isShuttingDown = true;\r\n console.log(`[Supervisor] Received ${signalName}, shutting down`);\r\n void watcher.close().then(() => {\r\n shutdownSupervisor();\r\n }).catch(() => {\r\n shutdownSupervisor();\r\n });\r\n };\r\n\r\n process.on('SIGINT', () => { handleSignal('SIGINT'); });\r\n process.on('SIGTERM', () => { handleSignal('SIGTERM'); });\r\n\r\n startChild();\r\n}\r\n"],"mappings":"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","names":[]}
@@ -1,55 +1,55 @@
1
- /* eslint-disable unicorn/no-abusive-eslint-disable */
2
- /* eslint-disable */
3
-
4
- //@ts-ignore We replace {{REL_PATH}} with the relative path to the project root at scaffold time.
5
- import { AuthProps, SessionLayout } from '{{REL_PATH}}config';
6
- //@ts-ignore We replace {{REL_PATH}} with the relative path to the project root at scaffold time.
7
- import { Functions, ApiResponse, MaybePromise, ApiStreamEmitter } from '{{REL_PATH}}src/_sockets/apiTypes.generated';
8
-
9
- // Set the request limit per minute. Set to false to use the default config value config.rateLimiting
10
- export const rateLimit: number | false = 20;
11
-
12
- // HTTP method for this API. If not set, inferred from name (get* = GET, delete* = DELETE, else POST)
13
- export const httpMethod: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'POST';
14
-
15
- export const auth: AuthProps = {
16
- login: true,
17
- additional: []
18
- };
19
-
20
- export interface ApiParams {
21
- data: {
22
- // Define your input data shape here e.g.
23
- // name: string;
24
- // email: string;
25
- };
26
- user: SessionLayout;
27
- functions: Functions;
28
- stream: ApiStreamEmitter;
29
- //? Aborts on client-side cancel (`apiRequest({ signal })`) or socket
30
- //? disconnect. Check `abortSignal.aborted` in long-running loops and
31
- //? bail out — already-emitted chunks are not unsent, but new ones are
32
- //? short-circuited automatically.
33
- abortSignal: AbortSignal;
34
- //? Awaitable backpressure helper. Resolves once the originator socket's
35
- //? pending write-buffer drops below the threshold (default 1 MB). Opt-in;
36
- //? handlers that don't stream can ignore it.
37
- flushPressure: (options?: { thresholdBytes?: number }) => Promise<void>;
38
- }
39
-
40
- export const main = ({ }: ApiParams): MaybePromise<ApiResponse> => {
41
- // Stream payload types are generated from your stream(...) calls.
42
- // stream({ phase: 'started', progress: 0 });
43
- // stream({ phase: 'done', progress: 100, done: true });
44
-
45
- // Error responses must include errorCode
46
- // return { status: 'error', errorCode: 'api.someError', errorParams: [{ key: 'id', value: 1 }] };
47
-
48
- // Optional: set custom HTTP status on this response
49
- // return { status: 'success', httpStatus: 201 };
50
-
51
- return {
52
- status: 'success',
53
- // Your response data here
54
- };
1
+ /* eslint-disable unicorn/no-abusive-eslint-disable */
2
+ /* eslint-disable */
3
+
4
+ //@ts-ignore We replace {{REL_PATH}} with the relative path to the project root at scaffold time.
5
+ import { AuthProps, SessionLayout } from '{{REL_PATH}}config';
6
+ //@ts-ignore We replace {{REL_PATH}} with the relative path to the project root at scaffold time.
7
+ import { Functions, ApiResponse, MaybePromise, ApiStreamEmitter } from '{{REL_PATH}}src/_sockets/apiTypes.generated';
8
+
9
+ // Set the request limit per minute. Set to false to use the default config value config.rateLimiting
10
+ export const rateLimit: number | false = 20;
11
+
12
+ // HTTP method for this API. If not set, inferred from name (get* = GET, delete* = DELETE, else POST)
13
+ export const httpMethod: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'POST';
14
+
15
+ export const auth: AuthProps = {
16
+ login: true,
17
+ additional: []
18
+ };
19
+
20
+ export interface ApiParams {
21
+ data: {
22
+ // Define your input data shape here e.g.
23
+ // name: string;
24
+ // email: string;
25
+ };
26
+ user: SessionLayout;
27
+ functions: Functions;
28
+ stream: ApiStreamEmitter;
29
+ //? Aborts on client-side cancel (`apiRequest({ signal })`) or socket
30
+ //? disconnect. Check `abortSignal.aborted` in long-running loops and
31
+ //? bail out — already-emitted chunks are not unsent, but new ones are
32
+ //? short-circuited automatically.
33
+ abortSignal: AbortSignal;
34
+ //? Awaitable backpressure helper. Resolves once the originator socket's
35
+ //? pending write-buffer drops below the threshold (default 1 MB). Opt-in;
36
+ //? handlers that don't stream can ignore it.
37
+ flushPressure: (options?: { thresholdBytes?: number }) => Promise<void>;
38
+ }
39
+
40
+ export const main = ({ }: ApiParams): MaybePromise<ApiResponse> => {
41
+ // Stream payload types are generated from your stream(...) calls.
42
+ // stream({ phase: 'started', progress: 0 });
43
+ // stream({ phase: 'done', progress: 100, done: true });
44
+
45
+ // Error responses must include errorCode
46
+ // return { status: 'error', errorCode: 'api.someError', errorParams: [{ key: 'id', value: 1 }] };
47
+
48
+ // Optional: set custom HTTP status on this response
49
+ // return { status: 'success', httpStatus: 201 };
50
+
51
+ return {
52
+ status: 'success',
53
+ // Your response data here
54
+ };
55
55
  };
@@ -1,35 +1,35 @@
1
- /* eslint-disable unicorn/no-abusive-eslint-disable */
2
- /* eslint-disable */
3
-
4
- //? Dashboard page template — wrapped in the `'dashboard'` template
5
- //? (sidebar + main content). Includes a login-required middleware as a
6
- //? sane default — adjust the role check or remove the middleware if this
7
- //? page is public.
8
-
9
- //@ts-ignore We replace {{REL_PATH}} with the relative path to the project root at scaffold time.
10
- import type { PageMiddleware } from '@luckystack/core/client';
11
- //@ts-ignore We replace {{REL_PATH}} with the relative path to the project root at scaffold time.
12
- import type { SessionLayout } from '{{REL_PATH}}config';
13
-
14
- export const template = 'dashboard';
15
-
16
- //? Default: require login. Customize for role checks
17
- //? (e.g. `if (!session.admin) return undefined;` to bounce non-admins back).
18
- export const middleware: PageMiddleware<SessionLayout> = ({ session }) => {
19
- if (!session) return { success: false, redirect: '/login' };
20
- return { success: true };
21
- };
22
-
23
- interface PageProps {
24
- params: Record<string, string | undefined>;
25
- searchParams: Record<string, string>;
26
- }
27
-
28
- export default function Page({ params, searchParams }: PageProps) {
29
- return (
30
- <div className='flex flex-col gap-4 p-6 w-full h-full'>
31
- <h1 className='text-2xl font-semibold text-title'>{/* TODO: page title */}</h1>
32
- <div className='text-common'>{/* TODO: page content */}</div>
33
- </div>
34
- );
35
- }
1
+ /* eslint-disable unicorn/no-abusive-eslint-disable */
2
+ /* eslint-disable */
3
+
4
+ //? Dashboard page template — wrapped in the `'dashboard'` template
5
+ //? (sidebar + main content). Includes a login-required middleware as a
6
+ //? sane default — adjust the role check or remove the middleware if this
7
+ //? page is public.
8
+
9
+ //@ts-ignore We replace {{REL_PATH}} with the relative path to the project root at scaffold time.
10
+ import type { PageMiddleware } from '@luckystack/core/client';
11
+ //@ts-ignore We replace {{REL_PATH}} with the relative path to the project root at scaffold time.
12
+ import type { SessionLayout } from '{{REL_PATH}}config';
13
+
14
+ export const template = 'dashboard';
15
+
16
+ //? Default: require login. Customize for role checks
17
+ //? (e.g. `if (!session.admin) return undefined;` to bounce non-admins back).
18
+ export const middleware: PageMiddleware<SessionLayout> = ({ session }) => {
19
+ if (!session) return { success: false, redirect: '/login' };
20
+ return { success: true };
21
+ };
22
+
23
+ interface PageProps {
24
+ params: Record<string, string | undefined>;
25
+ searchParams: Record<string, string>;
26
+ }
27
+
28
+ export default function Page({ params, searchParams }: PageProps) {
29
+ return (
30
+ <div className='flex flex-col gap-4 p-6 w-full h-full'>
31
+ <h1 className='text-2xl font-semibold text-title'>{/* TODO: page title */}</h1>
32
+ <div className='text-common'>{/* TODO: page content */}</div>
33
+ </div>
34
+ );
35
+ }
@@ -1,32 +1,32 @@
1
- /* eslint-disable unicorn/no-abusive-eslint-disable */
2
- /* eslint-disable */
3
-
4
- //? Plain page template — no UI chrome. Use for landing/public pages where
5
- //? the default surrounding layout (`'plain'`) is what you want. Per-page
6
- //? middleware is OPTIONAL — uncomment when this route needs a guard.
7
-
8
- //@ts-ignore We replace {{REL_PATH}} with the relative path to the project root at scaffold time.
9
- // import type { PageMiddleware } from '@luckystack/core/client';
10
- //@ts-ignore We replace {{REL_PATH}} with the relative path to the project root at scaffold time.
11
- // import type { SessionLayout } from '{{REL_PATH}}config';
12
-
13
- export const template = 'plain';
14
-
15
- // export const middleware: PageMiddleware<SessionLayout> = ({ session }) => {
16
- // if (!session) return { success: false, redirect: '/login' };
17
- // return { success: true };
18
- // };
19
-
20
- interface PageProps {
21
- params: Record<string, string | undefined>;
22
- searchParams: Record<string, string>;
23
- }
24
-
25
- export default function Page({ params, searchParams }: PageProps) {
26
- return (
27
- <div className='flex items-center justify-center w-full h-full text-title'>
28
- {/* TODO: replace with your page content */}
29
- <span>page scaffold — replace this</span>
30
- </div>
31
- );
32
- }
1
+ /* eslint-disable unicorn/no-abusive-eslint-disable */
2
+ /* eslint-disable */
3
+
4
+ //? Plain page template — no UI chrome. Use for landing/public pages where
5
+ //? the default surrounding layout (`'plain'`) is what you want. Per-page
6
+ //? middleware is OPTIONAL — uncomment when this route needs a guard.
7
+
8
+ //@ts-ignore We replace {{REL_PATH}} with the relative path to the project root at scaffold time.
9
+ // import type { PageMiddleware } from '@luckystack/core/client';
10
+ //@ts-ignore We replace {{REL_PATH}} with the relative path to the project root at scaffold time.
11
+ // import type { SessionLayout } from '{{REL_PATH}}config';
12
+
13
+ export const template = 'plain';
14
+
15
+ // export const middleware: PageMiddleware<SessionLayout> = ({ session }) => {
16
+ // if (!session) return { success: false, redirect: '/login' };
17
+ // return { success: true };
18
+ // };
19
+
20
+ interface PageProps {
21
+ params: Record<string, string | undefined>;
22
+ searchParams: Record<string, string>;
23
+ }
24
+
25
+ export default function Page({ params, searchParams }: PageProps) {
26
+ return (
27
+ <div className='flex items-center justify-center w-full h-full text-title'>
28
+ {/* TODO: replace with your page content */}
29
+ <span>page scaffold — replace this</span>
30
+ </div>
31
+ );
32
+ }
@@ -1,41 +1,41 @@
1
- /* eslint-disable unicorn/no-abusive-eslint-disable */
2
- /* eslint-disable */
3
-
4
- //@ts-ignore We replace {{REL_PATH}} with the relative path to the project root at scaffold time.
5
- import { Functions, SyncClientResponse, SyncClientStreamEmitter } from '{{REL_PATH}}src/_sockets/apiTypes.generated';
6
-
7
-
8
- export interface SyncParams {
9
- clientInput: {
10
- // Define the data shape sent from the client e.g.
11
- // message: string;
12
- };
13
- serverOutput: {
14
- // Define the data shape returned from the server e.g.
15
- // message: string;
16
- };
17
- token: string | null; // target client's session token (fetch session only when needed)
18
- functions: Functions; // contains all functions that are available on the server in the functions folder
19
- roomCode: string; // room code
20
- stream: SyncClientStreamEmitter;
21
- }
22
-
23
- export const main = async ({ }: SyncParams): Promise<SyncClientResponse> => {
24
- // THIS FILE RUNS ON THE SERVER AND IT EXECUTES FOR EVERY CLIENT THAT IS IN THE GIVEN ROOM
25
- // stream payload types are generated from your stream(...) calls in this file
26
- // Use functions.session.getSession(token) when you need session data for this target client.
27
-
28
- // Return { status: 'error', message: '...' } OR { status: 'error', errorCode: '...' }
29
- // Returning error here only affects the current target client and does not stop other clients.
30
-
31
- // Example: Only allow users on set page to receive the event
32
- // const targetUser = token ? await functions.session.getSession(token) : null;
33
- // if (targetUser?.location?.pathName === '/your-page') {
34
- // return { status: 'success' };
35
- // }
36
-
37
- return {
38
- status: 'success',
39
- // Add any additional data to pass to the client
40
- };
1
+ /* eslint-disable unicorn/no-abusive-eslint-disable */
2
+ /* eslint-disable */
3
+
4
+ //@ts-ignore We replace {{REL_PATH}} with the relative path to the project root at scaffold time.
5
+ import { Functions, SyncClientResponse, SyncClientStreamEmitter } from '{{REL_PATH}}src/_sockets/apiTypes.generated';
6
+
7
+
8
+ export interface SyncParams {
9
+ clientInput: {
10
+ // Define the data shape sent from the client e.g.
11
+ // message: string;
12
+ };
13
+ serverOutput: {
14
+ // Define the data shape returned from the server e.g.
15
+ // message: string;
16
+ };
17
+ token: string | null; // target client's session token (fetch session only when needed)
18
+ functions: Functions; // contains all functions that are available on the server in the functions folder
19
+ roomCode: string; // room code
20
+ stream: SyncClientStreamEmitter;
21
+ }
22
+
23
+ export const main = async ({ }: SyncParams): Promise<SyncClientResponse> => {
24
+ // THIS FILE RUNS ON THE SERVER AND IT EXECUTES FOR EVERY CLIENT THAT IS IN THE GIVEN ROOM
25
+ // stream payload types are generated from your stream(...) calls in this file
26
+ // Use functions.session.getSession(token) when you need session data for this target client.
27
+
28
+ // Return { status: 'error', message: '...' } OR { status: 'error', errorCode: '...' }
29
+ // Returning error here only affects the current target client and does not stop other clients.
30
+
31
+ // Example: Only allow users on set page to receive the event
32
+ // const targetUser = token ? await functions.session.getSession(token) : null;
33
+ // if (targetUser?.location?.pathName === '/your-page') {
34
+ // return { status: 'success' };
35
+ // }
36
+
37
+ return {
38
+ status: 'success',
39
+ // Add any additional data to pass to the client
40
+ };
41
41
  };
@@ -1,38 +1,38 @@
1
- /* eslint-disable unicorn/no-abusive-eslint-disable */
2
- /* eslint-disable */
3
-
4
- //@ts-ignore We replace {{REL_PATH}} with the relative path to the project root at scaffold time.
5
- import { Functions, SyncClientResponse, SyncClientInput, SyncServerOutput, MaybePromise, SyncClientStreamEmitter } from '{{REL_PATH}}src/_sockets/apiTypes.generated';
6
-
7
- // Types are imported from the generated file based on the _server_v{number}.ts definition
8
- type PagePath = '{{PAGE_PATH}}';
9
- type SyncName = '{{SYNC_NAME}}';
10
-
11
- export interface SyncParams {
12
- clientInput: SyncClientInput<PagePath, SyncName>;
13
- serverOutput: SyncServerOutput<PagePath, SyncName>;
14
- token: string | null; // target client's session token (fetch session only when needed)
15
- functions: Functions; // contains all functions that are available on the server in the functions folder
16
- roomCode: string; // room code
17
- stream: SyncClientStreamEmitter;
18
- }
19
-
20
- export const main = ({ }: SyncParams): MaybePromise<SyncClientResponse> => {
21
- // PAIRED SYNC: Types are shared with the _server_v{number}.ts file
22
- // clientInput type comes from _server_v{number}.ts SyncParams
23
- // serverOutput type is inferred from _server_v{number}.ts return value
24
- // stream payload types are generated from your stream(...) calls in this file
25
- // Use functions.session.getSession(token) when you need session data for this target client.
26
- // Returning error here only affects the current target client and does not stop other clients.
27
-
28
- // Example: Only allow users on set page to receive the event
29
- // const targetUser = token ? await functions.session.getSession(token) : null;
30
- // if (targetUser?.location?.pathName === '/your-page') {
31
- // return { status: 'success' };
32
- // }
33
-
34
- return {
35
- status: 'success',
36
- // Add any additional data to pass to the client
37
- };
38
- };
1
+ /* eslint-disable unicorn/no-abusive-eslint-disable */
2
+ /* eslint-disable */
3
+
4
+ //@ts-ignore We replace {{REL_PATH}} with the relative path to the project root at scaffold time.
5
+ import { Functions, SyncClientResponse, SyncClientInput, SyncServerOutput, MaybePromise, SyncClientStreamEmitter } from '{{REL_PATH}}src/_sockets/apiTypes.generated';
6
+
7
+ // Types are imported from the generated file based on the _server_v{number}.ts definition
8
+ type PagePath = '{{PAGE_PATH}}';
9
+ type SyncName = '{{SYNC_NAME}}';
10
+
11
+ export interface SyncParams {
12
+ clientInput: SyncClientInput<PagePath, SyncName>;
13
+ serverOutput: SyncServerOutput<PagePath, SyncName>;
14
+ token: string | null; // target client's session token (fetch session only when needed)
15
+ functions: Functions; // contains all functions that are available on the server in the functions folder
16
+ roomCode: string; // room code
17
+ stream: SyncClientStreamEmitter;
18
+ }
19
+
20
+ export const main = ({ }: SyncParams): MaybePromise<SyncClientResponse> => {
21
+ // PAIRED SYNC: Types are shared with the _server_v{number}.ts file
22
+ // clientInput type comes from _server_v{number}.ts SyncParams
23
+ // serverOutput type is inferred from _server_v{number}.ts return value
24
+ // stream payload types are generated from your stream(...) calls in this file
25
+ // Use functions.session.getSession(token) when you need session data for this target client.
26
+ // Returning error here only affects the current target client and does not stop other clients.
27
+
28
+ // Example: Only allow users on set page to receive the event
29
+ // const targetUser = token ? await functions.session.getSession(token) : null;
30
+ // if (targetUser?.location?.pathName === '/your-page') {
31
+ // return { status: 'success' };
32
+ // }
33
+
34
+ return {
35
+ status: 'success',
36
+ // Add any additional data to pass to the client
37
+ };
38
+ };
@@ -1,36 +1,36 @@
1
- /* eslint-disable unicorn/no-abusive-eslint-disable */
2
- /* eslint-disable */
3
-
4
- //@ts-ignore We replace {{REL_PATH}} with the relative path to the project root at scaffold time.
5
- import { Functions, SyncClientResponse, MaybePromise, SyncClientStreamEmitter } from '{{REL_PATH}}src/_sockets/apiTypes.generated';
6
-
7
-
8
- export interface SyncParams {
9
- clientInput: {
10
- // Define the data shape sent from the client e.g.
11
- // message: string;
12
- };
13
- // Note: No serverOutput in client-only syncs (no _server_v{number}.ts file)
14
- token: string | null; // target client's session token (fetch session only when needed)
15
- functions: Functions; // contains all functions that are available on the server in the functions folder
16
- roomCode: string; // room code
17
- stream: SyncClientStreamEmitter;
18
- }
19
-
20
- export const main = ({ }: SyncParams): MaybePromise<SyncClientResponse> => {
21
- // CLIENT-ONLY SYNC: No server processing, runs for each client in the room
22
- // stream payload types are generated from your stream(...) calls in this file
23
- // Use functions.session.getSession(token) when you need session data for this target client.
24
- // Returning error here only affects the current target client and does not stop other clients.
25
-
26
- // Example: Only allow users on set page to receive the event
27
- // const targetUser = token ? await functions.session.getSession(token) : null;
28
- // if (targetUser?.location?.pathName === '/your-page') {
29
- // return { status: 'success' };
30
- // }
31
-
32
- return {
33
- status: 'success',
34
- // Add any additional data to pass to the client
35
- };
36
- };
1
+ /* eslint-disable unicorn/no-abusive-eslint-disable */
2
+ /* eslint-disable */
3
+
4
+ //@ts-ignore We replace {{REL_PATH}} with the relative path to the project root at scaffold time.
5
+ import { Functions, SyncClientResponse, MaybePromise, SyncClientStreamEmitter } from '{{REL_PATH}}src/_sockets/apiTypes.generated';
6
+
7
+
8
+ export interface SyncParams {
9
+ clientInput: {
10
+ // Define the data shape sent from the client e.g.
11
+ // message: string;
12
+ };
13
+ // Note: No serverOutput in client-only syncs (no _server_v{number}.ts file)
14
+ token: string | null; // target client's session token (fetch session only when needed)
15
+ functions: Functions; // contains all functions that are available on the server in the functions folder
16
+ roomCode: string; // room code
17
+ stream: SyncClientStreamEmitter;
18
+ }
19
+
20
+ export const main = ({ }: SyncParams): MaybePromise<SyncClientResponse> => {
21
+ // CLIENT-ONLY SYNC: No server processing, runs for each client in the room
22
+ // stream payload types are generated from your stream(...) calls in this file
23
+ // Use functions.session.getSession(token) when you need session data for this target client.
24
+ // Returning error here only affects the current target client and does not stop other clients.
25
+
26
+ // Example: Only allow users on set page to receive the event
27
+ // const targetUser = token ? await functions.session.getSession(token) : null;
28
+ // if (targetUser?.location?.pathName === '/your-page') {
29
+ // return { status: 'success' };
30
+ // }
31
+
32
+ return {
33
+ status: 'success',
34
+ // Add any additional data to pass to the client
35
+ };
36
+ };