deepspace 0.4.5 → 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.
Files changed (35) hide show
  1. package/README.md +90 -0
  2. package/dist/cli.js +3171 -1166
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.d.ts +31 -4
  5. package/dist/index.js +511 -366
  6. package/dist/index.js.map +1 -1
  7. package/dist/server.d.ts +27 -22
  8. package/dist/server.js +117 -94
  9. package/dist/server.js.map +1 -1
  10. package/dist/testing.js.map +1 -1
  11. package/dist/worker.d.ts +27 -22
  12. package/dist/worker.js +117 -94
  13. package/dist/worker.js.map +1 -1
  14. package/features/admin-page/src/AdminPage.tsx +3 -5
  15. package/features/canvas/src/CanvasEditorPage.tsx +0 -2
  16. package/features/canvas/src/components/CanvasView.tsx +1 -1
  17. package/features/docs/src/LibrarySidebar.tsx +7 -1
  18. package/features/docs/src/use-yjs-room-with-awareness.ts +4 -0
  19. package/features/file-attachments/src/AttachmentsPage.tsx +7 -1
  20. package/features/file-attachments/src/useFilePreview.ts +7 -5
  21. package/features/file-manager/src/FileManagerPage.tsx +2 -3
  22. package/features/integration-test/feature.json +10 -3
  23. package/features/integration-test/src/IntegrationTestPage.tsx +15 -12
  24. package/features/landing/src/LandingPage.tsx +1 -1
  25. package/features/landing/src/sections/FeaturesGridSection.tsx +0 -92
  26. package/features/messaging/src/components/chat/MessageItem.tsx +5 -1
  27. package/features/messaging/src/hooks/useChatChannel.ts +5 -1
  28. package/features/presence-test/feature.json +2 -1
  29. package/features/sidebar/src/AppSidebar.tsx +1 -1
  30. package/features/tasks/src/TasksPage.tsx +1 -1
  31. package/features/testing/feature.json +2 -1
  32. package/features/testing/src/TestPage.tsx +8 -8
  33. package/package.json +4 -4
  34. package/scripts/add-feature.cjs +277 -272
  35. package/dist/testing.d.ts +0 -115
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/testing/fixtures.ts","../src/testing/accounts.ts","../src/testing/storage-state.ts"],"sourcesContent":["/**\n * Multi-user Playwright fixture for DeepSpace apps.\n *\n * Usage:\n *\n * import { test, expect } from 'deepspace/testing'\n *\n * test('A sends, B sees', async ({ users }) => {\n * const [a, b] = await users(2)\n * await a.page.goto('/chat')\n * await b.page.goto('/chat')\n * await a.page.getByTestId('message-input').fill('hi')\n * await a.page.getByTestId('send-message-btn').click()\n * await expect(b.page.getByText('hi')).toBeVisible()\n * })\n *\n * `users(N)` picks the first N test accounts from\n * `~/.deepspace/test-accounts.json` (created via\n * `deepspace test-accounts create`). `users(['Alice', 'Bob'])` picks\n * specific accounts by name.\n *\n * Each user gets a separate browser context with cached storageState\n * (per-account JSON in `~/.deepspace/playwright-states/`). Sign-in\n * happens once per account, not once per test, sidestepping Better\n * Auth's per-IP rate limit on `/api/auth/sign-in/email`.\n *\n * Contexts are auto-closed when the test finishes.\n */\n\nimport { test as baseTest, type Browser, type BrowserContext, type Page } from '@playwright/test'\nimport {\n type TestAccount,\n pickTestAccounts,\n findTestAccountByName,\n} from './accounts'\nimport { ensureStorageState } from './storage-state'\n\nexport interface MultiplayerUser {\n context: BrowserContext\n page: Page\n email: string\n name: string\n /** Test account user ID, if known from the accounts registry. */\n userId?: string\n}\n\n/**\n * Fixture function signature. Pass either a count (`users(2)`) or an\n * array of account names (`users(['Alice', 'Bob'])`). Returns one entry\n * per requested user.\n */\nexport type UsersFixture = (\n selector: number | string[],\n options?: { label?: string },\n) => Promise<MultiplayerUser[]>\n\ninterface MultiplayerFixtures {\n users: UsersFixture\n}\n\nasync function buildUser(\n browser: Browser,\n account: TestAccount,\n baseURL: string,\n): Promise<{ user: MultiplayerUser; cleanup: () => Promise<void> }> {\n const statePath = await ensureStorageState(browser, account, baseURL)\n const context = await browser.newContext({ storageState: statePath, baseURL })\n const page = await context.newPage()\n return {\n user: {\n context,\n page,\n email: account.email,\n name: account.name ?? account.email.split('@')[0],\n userId: account.userId,\n },\n cleanup: async () => {\n await context.close()\n },\n }\n}\n\nexport const test = baseTest.extend<MultiplayerFixtures>({\n users: async ({ browser, baseURL }, use) => {\n if (!baseURL) {\n throw new Error(\n '`users` fixture requires a baseURL. Set it on your Playwright config ' +\n '(e.g. `use: { baseURL: \"http://localhost:5173\" }`) or via the ' +\n 'webServer.url shortcut.',\n )\n }\n\n const cleanups: Array<() => Promise<void>> = []\n\n const usersFn: UsersFixture = async (selector, options) => {\n const accounts: TestAccount[] = Array.isArray(selector)\n ? selector.map((name) => findTestAccountByName(name))\n : pickTestAccounts(selector, options)\n\n const built = await Promise.all(\n accounts.map((acct) => buildUser(browser, acct, baseURL)),\n )\n for (const b of built) cleanups.push(b.cleanup)\n return built.map((b) => b.user)\n }\n\n await use(usersFn)\n\n // Tear down any contexts we created. Errors are swallowed — a test\n // failure shouldn't be masked by a cleanup error.\n for (const cleanup of cleanups) {\n await cleanup().catch(() => {})\n }\n },\n})\n\nexport { expect } from '@playwright/test'\n","/**\n * Account-credential plumbing for the testing module.\n *\n * Reads test accounts from `~/.deepspace/test-accounts.json` (the same\n * file `deepspace test-accounts create` writes). Each account has at\n * minimum `{email, password}`; `name` and `label` are optional.\n *\n * Used by the multi-user Playwright fixture in `./fixtures.ts` and is\n * also exported standalone in case suites want to do their own\n * orchestration.\n */\n\nimport { existsSync, readFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nexport interface TestAccount {\n email: string\n password: string\n name?: string\n label?: string | null\n id?: string\n userId?: string\n createdAt?: number\n}\n\nconst ACCOUNTS_PATH = join(homedir(), '.deepspace', 'test-accounts.json')\n\n/**\n * Load all test accounts the developer has created. Returns an empty\n * array if the file doesn't exist yet.\n */\nexport function loadAllTestAccounts(): TestAccount[] {\n if (!existsSync(ACCOUNTS_PATH)) return []\n try {\n const raw = JSON.parse(readFileSync(ACCOUNTS_PATH, 'utf-8'))\n if (!Array.isArray(raw)) return []\n // Filter out any malformed entries (missing email or password). Keep\n // bare {email, password} rows so the file format stays forgiving.\n return raw.filter(\n (r) => r && typeof r.email === 'string' && typeof r.password === 'string',\n )\n } catch {\n return []\n }\n}\n\n/**\n * Pick `count` test accounts from the local registry, optionally\n * filtered by `label`. Order-stable: sorted by `createdAt` ascending so\n * the first test account you created is always the first one returned.\n *\n * Throws with a helpful message if not enough accounts exist.\n */\nexport function pickTestAccounts(\n count: number,\n options?: { label?: string },\n): TestAccount[] {\n const all = loadAllTestAccounts()\n const filtered = options?.label\n ? all.filter((a) => a.label === options.label)\n : all\n const sorted = filtered.slice().sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0))\n\n if (sorted.length < count) {\n const labelHint = options?.label ? ` (label=\"${options.label}\")` : ''\n throw new Error(\n `Multiplayer test needs ${count} test accounts${labelHint}, but only ${sorted.length} ` +\n `are present in ${ACCOUNTS_PATH}. Create more with:\\n` +\n ` deepspace test-accounts create --email <name>@deepspace.test --password <pw> --name \"<name>\"${\n options?.label ? ` --label ${options.label}` : ''\n }`,\n )\n }\n\n return sorted.slice(0, count)\n}\n\n/**\n * Find a single account by name. Used by `users(['Alice', 'Bob'])`.\n * Throws if not found.\n */\nexport function findTestAccountByName(name: string): TestAccount {\n const all = loadAllTestAccounts()\n const match = all.find((a) => a.name === name)\n if (!match) {\n throw new Error(\n `No test account named \"${name}\" in ${ACCOUNTS_PATH}. Create with:\\n` +\n ` deepspace test-accounts create --email ${name.toLowerCase()}@deepspace.test --password <pw> --name \"${name}\"`,\n )\n }\n return match\n}\n","/**\n * Per-account `storageState` cache for Playwright.\n *\n * Better Auth applies a per-IP rate limit on `/api/auth/sign-in/email`\n * (~5 sign-ins / 60s / endpoint). A multiplayer suite that spins up\n * 5+ users per spec would hit the limit and start failing in non-obvious\n * ways. This module signs each account in *once*, persists the\n * resulting browser cookies + storage to disk, and reuses the file on\n * subsequent runs.\n *\n * Cache layout:\n * ~/.deepspace/playwright-states/<sha256(email)>.json\n *\n * Validity: a cached file is reused if Playwright successfully loads it\n * AND the resulting context produces a non-anonymous session on the\n * target app. If validation fails, we fall back to a fresh sign-in and\n * overwrite the cache.\n */\n\nimport { createHash } from 'node:crypto'\nimport { existsSync, mkdirSync, writeFileSync, readFileSync, statSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport type { Browser, BrowserContext } from '@playwright/test'\n\nconst STATES_DIR = join(homedir(), '.deepspace', 'playwright-states')\n\n/** Default freshness window — re-sign-in if the cache is older than this. */\nconst DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000 // 7 days\n\nfunction statePathForEmail(email: string): string {\n const hash = createHash('sha256').update(email.trim().toLowerCase()).digest('hex').slice(0, 24)\n return join(STATES_DIR, `${hash}.json`)\n}\n\nfunction ensureStatesDir() {\n if (!existsSync(STATES_DIR)) mkdirSync(STATES_DIR, { recursive: true })\n}\n\n/** Returns true if a cache file exists and is younger than `maxAgeMs`. */\nfunction isCacheFresh(path: string, maxAgeMs: number): boolean {\n if (!existsSync(path)) return false\n try {\n const age = Date.now() - statSync(path).mtimeMs\n return age < maxAgeMs\n } catch {\n return false\n }\n}\n\n/**\n * Sign `account` in by POSTing `/api/auth/sign-in/email` against\n * `baseURL` and capturing the resulting cookies. Returns the path to the\n * Playwright `storageState` JSON file.\n *\n * The browser-driven approach is intentional: we want the cookies set\n * on the same origin Playwright contexts will load from, otherwise the\n * Set-Cookie domain attribute won't match.\n */\nasync function signInAndSaveState(\n browser: Browser,\n account: { email: string; password: string },\n baseURL: string,\n outPath: string,\n): Promise<string> {\n ensureStatesDir()\n const ctx = await browser.newContext({ baseURL })\n try {\n const page = await ctx.newPage()\n // Hit the app first so the origin is established (some auth plugins\n // require a same-origin Origin header on sign-in).\n await page.goto('/')\n const ok = await page.evaluate(\n async ({ email, password }: { email: string; password: string }) => {\n const res = await fetch('/api/auth/sign-in/email', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email, password }),\n })\n return res.ok\n },\n account,\n )\n if (!ok) {\n throw new Error(\n `Sign-in failed for ${account.email}. Check the password is current; ` +\n `if the account was deleted on the server, re-create with ` +\n `\\`deepspace test-accounts create\\`.`,\n )\n }\n await ctx.storageState({ path: outPath })\n return outPath\n } finally {\n await ctx.close()\n }\n}\n\nexport interface EnsureStorageStateOptions {\n /** Max age of a cached state file before we re-sign-in. Default 7 days. */\n maxAgeMs?: number\n /** Force a fresh sign-in even if the cache is fresh. */\n force?: boolean\n}\n\n/**\n * Ensure a Playwright `storageState` file exists for `account` and\n * return its path. Signs in once if the cache is missing, stale, or\n * `force: true`.\n */\nexport async function ensureStorageState(\n browser: Browser,\n account: { email: string; password: string },\n baseURL: string,\n options: EnsureStorageStateOptions = {},\n): Promise<string> {\n const path = statePathForEmail(account.email)\n const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS\n\n if (!options.force && isCacheFresh(path, maxAgeMs)) {\n return path\n }\n\n return signInAndSaveState(browser, account, baseURL, path)\n}\n\n/**\n * Convenience: open a fresh `BrowserContext` for `account` using the\n * cached storage state (signing in if needed). The caller is responsible\n * for closing the context.\n */\nexport async function newSignedInContext(\n browser: Browser,\n account: { email: string; password: string },\n baseURL: string,\n options: EnsureStorageStateOptions = {},\n): Promise<BrowserContext> {\n const statePath = await ensureStorageState(browser, account, baseURL, options)\n return browser.newContext({ storageState: statePath, baseURL })\n}\n\n/** Read a cached storageState file (mostly for debugging). */\nexport function readCachedState(email: string): unknown | null {\n const path = statePathForEmail(email)\n if (!existsSync(path)) return null\n try {\n return JSON.parse(readFileSync(path, 'utf-8'))\n } catch {\n return null\n }\n}\n\n/** Where the cache for `email` lives. Useful for diagnostics. */\nexport function getStatePathForEmail(email: string): string {\n return statePathForEmail(email)\n}\n"],"mappings":"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","names":["existsSync","readFileSync","homedir","join"]}
1
+ {"version":3,"sources":["../src/testing/fixtures.ts","../src/testing/accounts.ts","../src/testing/storage-state.ts"],"sourcesContent":["/**\n * Multi-user Playwright fixture for DeepSpace apps.\n *\n * Usage:\n *\n * import { test, expect } from 'deepspace/testing'\n *\n * test('A sends, B sees', async ({ users }) => {\n * const [a, b] = await users(2)\n * await a.page.goto('/chat')\n * await b.page.goto('/chat')\n * await a.page.getByTestId('message-input').fill('hi')\n * await a.page.getByTestId('send-message-btn').click()\n * await expect(b.page.getByText('hi')).toBeVisible()\n * })\n *\n * `users(N)` picks the first N test accounts from\n * `~/.deepspace/test-accounts.json` (created via\n * `deepspace test-accounts create`). `users(['Alice', 'Bob'])` picks\n * specific accounts by name.\n *\n * Each user gets a separate browser context with cached storageState\n * (per-account JSON in `~/.deepspace/playwright-states/`). Sign-in\n * happens once per account, not once per test, sidestepping Better\n * Auth's per-IP rate limit on `/api/auth/sign-in/email`.\n *\n * Contexts are auto-closed when the test finishes.\n */\n\nimport { test as baseTest, type Browser, type BrowserContext, type Page } from '@playwright/test'\nimport {\n type TestAccount,\n pickTestAccounts,\n findTestAccountByName,\n} from './accounts'\nimport { ensureStorageState } from './storage-state'\n\nexport interface MultiplayerUser {\n context: BrowserContext\n page: Page\n email: string\n name: string\n /** Test account user ID, if known from the accounts registry. */\n userId?: string\n}\n\n/**\n * Fixture function signature. Pass either a count (`users(2)`) or an\n * array of account names (`users(['Alice', 'Bob'])`). Returns one entry\n * per requested user.\n */\nexport type UsersFixture = (\n selector: number | string[],\n options?: { label?: string },\n) => Promise<MultiplayerUser[]>\n\ninterface MultiplayerFixtures {\n users: UsersFixture\n}\n\nasync function buildUser(\n browser: Browser,\n account: TestAccount,\n baseURL: string,\n): Promise<{ user: MultiplayerUser; cleanup: () => Promise<void> }> {\n const statePath = await ensureStorageState(browser, account, baseURL)\n const context = await browser.newContext({ storageState: statePath, baseURL })\n const page = await context.newPage()\n return {\n user: {\n context,\n page,\n email: account.email,\n name: account.name ?? account.email.split('@')[0],\n userId: account.userId,\n },\n cleanup: async () => {\n await context.close()\n },\n }\n}\n\nexport const test = baseTest.extend<MultiplayerFixtures>({\n users: async ({ browser, baseURL }, use) => {\n if (!baseURL) {\n throw new Error(\n '`users` fixture requires a baseURL. Set it on your Playwright config ' +\n '(e.g. `use: { baseURL: \"http://localhost:5173\" }`) or via the ' +\n 'webServer.url shortcut.',\n )\n }\n\n const cleanups: Array<() => Promise<void>> = []\n\n const usersFn: UsersFixture = async (selector, options) => {\n const accounts: TestAccount[] = Array.isArray(selector)\n ? selector.map((name) => findTestAccountByName(name))\n : pickTestAccounts(selector, options)\n\n const built = await Promise.all(\n accounts.map((acct) => buildUser(browser, acct, baseURL)),\n )\n for (const b of built) cleanups.push(b.cleanup)\n return built.map((b) => b.user)\n }\n\n await use(usersFn)\n\n // Tear down any contexts we created. Errors are swallowed — a test\n // failure shouldn't be masked by a cleanup error.\n for (const cleanup of cleanups) {\n await cleanup().catch(() => {})\n }\n },\n})\n\nexport { expect } from '@playwright/test'\n","/**\n * Account-credential plumbing for the testing module.\n *\n * Reads test accounts from `~/.deepspace/test-accounts.json` (the same\n * file `deepspace test-accounts create` writes). Each account has at\n * minimum `{email, password}`; `name` and `label` are optional.\n *\n * Used by the multi-user Playwright fixture in `./fixtures.ts` and is\n * also exported standalone in case suites want to do their own\n * orchestration.\n */\n\nimport { existsSync, readFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nexport interface TestAccount {\n email: string\n password: string\n name?: string\n label?: string | null\n id?: string\n userId?: string\n createdAt?: number\n}\n\nconst ACCOUNTS_PATH = join(homedir(), '.deepspace', 'test-accounts.json')\n\n/**\n * Load all test accounts the developer has created. Returns an empty\n * array if the file doesn't exist yet.\n */\nexport function loadAllTestAccounts(): TestAccount[] {\n if (!existsSync(ACCOUNTS_PATH)) return []\n try {\n const raw = JSON.parse(readFileSync(ACCOUNTS_PATH, 'utf-8'))\n if (!Array.isArray(raw)) return []\n // Filter out any malformed entries (missing email or password). Keep\n // bare {email, password} rows so the file format stays forgiving.\n return raw.filter(\n (r) => r && typeof r.email === 'string' && typeof r.password === 'string',\n )\n } catch {\n return []\n }\n}\n\n/**\n * Pick `count` test accounts from the local registry, optionally\n * filtered by `label`. Order-stable: sorted by `createdAt` ascending so\n * the first test account you created is always the first one returned.\n *\n * Throws with a helpful message if not enough accounts exist.\n */\nexport function pickTestAccounts(\n count: number,\n options?: { label?: string },\n): TestAccount[] {\n const all = loadAllTestAccounts()\n const filtered = options?.label\n ? all.filter((a) => a.label === options.label)\n : all\n const sorted = filtered.slice().sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0))\n\n if (sorted.length < count) {\n const labelHint = options?.label ? ` (label=\"${options.label}\")` : ''\n throw new Error(\n `Multiplayer test needs ${count} test accounts${labelHint}, but only ${sorted.length} ` +\n `are present in ${ACCOUNTS_PATH}. Create more with:\\n` +\n ` deepspace test-accounts create --email <name>@deepspace.test --password <pw> --name \"<name>\"${\n options?.label ? ` --label ${options.label}` : ''\n }`,\n )\n }\n\n return sorted.slice(0, count)\n}\n\n/**\n * Find a single account by name. Used by `users(['Alice', 'Bob'])`.\n * Throws if not found.\n */\nexport function findTestAccountByName(name: string): TestAccount {\n const all = loadAllTestAccounts()\n const match = all.find((a) => a.name === name)\n if (!match) {\n throw new Error(\n `No test account named \"${name}\" in ${ACCOUNTS_PATH}. Create with:\\n` +\n ` deepspace test-accounts create --email ${name.toLowerCase()}@deepspace.test --password <pw> --name \"${name}\"`,\n )\n }\n return match\n}\n","/**\n * Per-account `storageState` cache for Playwright.\n *\n * Better Auth applies a per-IP rate limit on `/api/auth/sign-in/email`\n * (~5 sign-ins / 60s / endpoint). A multiplayer suite that spins up\n * 5+ users per spec would hit the limit and start failing in non-obvious\n * ways. This module signs each account in *once*, persists the\n * resulting browser cookies + storage to disk, and reuses the file on\n * subsequent runs.\n *\n * Cache layout:\n * ~/.deepspace/playwright-states/<sha256(email)>.json\n *\n * Validity: a cached file is reused if Playwright successfully loads it\n * AND the resulting context produces a non-anonymous session on the\n * target app. If validation fails, we fall back to a fresh sign-in and\n * overwrite the cache.\n */\n\nimport { createHash } from 'node:crypto'\nimport { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport type { Browser, BrowserContext } from '@playwright/test'\n\nconst STATES_DIR = join(homedir(), '.deepspace', 'playwright-states')\n\n/** Default freshness window — re-sign-in if the cache is older than this. */\nconst DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000 // 7 days\n\nfunction statePathForEmail(email: string): string {\n const hash = createHash('sha256').update(email.trim().toLowerCase()).digest('hex').slice(0, 24)\n return join(STATES_DIR, `${hash}.json`)\n}\n\nfunction ensureStatesDir() {\n if (!existsSync(STATES_DIR)) mkdirSync(STATES_DIR, { recursive: true })\n}\n\n/** Returns true if a cache file exists and is younger than `maxAgeMs`. */\nfunction isCacheFresh(path: string, maxAgeMs: number): boolean {\n if (!existsSync(path)) return false\n try {\n const age = Date.now() - statSync(path).mtimeMs\n return age < maxAgeMs\n } catch {\n return false\n }\n}\n\n/**\n * Sign `account` in by POSTing `/api/auth/sign-in/email` against\n * `baseURL` and capturing the resulting cookies. Returns the path to the\n * Playwright `storageState` JSON file.\n *\n * The browser-driven approach is intentional: we want the cookies set\n * on the same origin Playwright contexts will load from, otherwise the\n * Set-Cookie domain attribute won't match.\n */\nasync function signInAndSaveState(\n browser: Browser,\n account: { email: string; password: string },\n baseURL: string,\n outPath: string,\n): Promise<string> {\n ensureStatesDir()\n const ctx = await browser.newContext({ baseURL })\n try {\n const page = await ctx.newPage()\n // Hit the app first so the origin is established (some auth plugins\n // require a same-origin Origin header on sign-in).\n await page.goto('/')\n const ok = await page.evaluate(\n async ({ email, password }: { email: string; password: string }) => {\n const res = await fetch('/api/auth/sign-in/email', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email, password }),\n })\n return res.ok\n },\n account,\n )\n if (!ok) {\n throw new Error(\n `Sign-in failed for ${account.email}. Check the password is current; ` +\n `if the account was deleted on the server, re-create with ` +\n `\\`deepspace test-accounts create\\`.`,\n )\n }\n await ctx.storageState({ path: outPath })\n return outPath\n } finally {\n await ctx.close()\n }\n}\n\nexport interface EnsureStorageStateOptions {\n /** Max age of a cached state file before we re-sign-in. Default 7 days. */\n maxAgeMs?: number\n /** Force a fresh sign-in even if the cache is fresh. */\n force?: boolean\n}\n\n/**\n * Ensure a Playwright `storageState` file exists for `account` and\n * return its path. Signs in once if the cache is missing, stale, or\n * `force: true`.\n */\nexport async function ensureStorageState(\n browser: Browser,\n account: { email: string; password: string },\n baseURL: string,\n options: EnsureStorageStateOptions = {},\n): Promise<string> {\n const path = statePathForEmail(account.email)\n const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS\n\n if (!options.force && isCacheFresh(path, maxAgeMs)) {\n return path\n }\n\n return signInAndSaveState(browser, account, baseURL, path)\n}\n\n/**\n * Convenience: open a fresh `BrowserContext` for `account` using the\n * cached storage state (signing in if needed). The caller is responsible\n * for closing the context.\n */\nexport async function newSignedInContext(\n browser: Browser,\n account: { email: string; password: string },\n baseURL: string,\n options: EnsureStorageStateOptions = {},\n): Promise<BrowserContext> {\n const statePath = await ensureStorageState(browser, account, baseURL, options)\n return browser.newContext({ storageState: statePath, baseURL })\n}\n\n/** Read a cached storageState file (mostly for debugging). */\nexport function readCachedState(email: string): unknown | null {\n const path = statePathForEmail(email)\n if (!existsSync(path)) return null\n try {\n return JSON.parse(readFileSync(path, 'utf-8'))\n } catch {\n return null\n }\n}\n\n/** Where the cache for `email` lives. Useful for diagnostics. */\nexport function getStatePathForEmail(email: string): string {\n return statePathForEmail(email)\n}\n"],"mappings":"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","names":["existsSync","readFileSync","homedir","join"]}
package/dist/worker.d.ts CHANGED
@@ -917,7 +917,7 @@ declare abstract class BaseRoom<E = Record<string, unknown>> {
917
917
  fetch(request: Request): Promise<Response>;
918
918
  private handleWebSocketUpgrade;
919
919
  webSocketMessage(ws: WebSocket, message: ArrayBuffer | string): Promise<void>;
920
- webSocketClose(ws: WebSocket, code: number, reason: string): Promise<void>;
920
+ webSocketClose(ws: WebSocket, _code: number, _reason: string): Promise<void>;
921
921
  webSocketError(ws: WebSocket, error: unknown): Promise<void>;
922
922
  alarm(): Promise<void>;
923
923
  /**
@@ -925,7 +925,7 @@ declare abstract class BaseRoom<E = Record<string, unknown>> {
925
925
  * Return an augmented attachment to serialize on the WebSocket,
926
926
  * or void to use the default attachment.
927
927
  */
928
- protected onConnect(ws: WebSocket, user: UserAttachment): UserAttachment | void | Promise<UserAttachment | void>;
928
+ protected onConnect(_ws: WebSocket, _user: UserAttachment): UserAttachment | void | Promise<UserAttachment | void>;
929
929
  /**
930
930
  * Called for each parsed JSON message.
931
931
  */
@@ -940,7 +940,7 @@ declare abstract class BaseRoom<E = Record<string, unknown>> {
940
940
  /**
941
941
  * Called when a WebSocket disconnects.
942
942
  */
943
- protected onDisconnect(ws: WebSocket, user: UserAttachment): void | Promise<void>;
943
+ protected onDisconnect(_ws: WebSocket, _user: UserAttachment): void | Promise<void>;
944
944
  /**
945
945
  * Called for HTTP requests that are NOT WebSocket upgrades.
946
946
  */
@@ -1322,7 +1322,6 @@ declare class SchemaRegistry {
1322
1322
  registerTrusted(schema: CollectionSchema): void;
1323
1323
  get(name: string): CollectionSchema | undefined;
1324
1324
  has(name: string): boolean;
1325
- hasTrusted(name: string): boolean;
1326
1325
  all(): CollectionSchema[];
1327
1326
  names(): string[];
1328
1327
  }
@@ -1394,6 +1393,12 @@ declare class RecordRoom<E = Record<string, unknown>> extends BaseRoom<E> {
1394
1393
  private ownerUserId;
1395
1394
  /** True until the first fetch() completes — detects hibernation wake-up */
1396
1395
  private freshConstruct;
1396
+ /**
1397
+ * Per-connection `[DO Perf]` timing logs are noisy on every hot path, so
1398
+ * they're gated behind a `DEEPSPACE_DO_PERF` env binding (set it to any
1399
+ * truthy value on the worker to opt in). Off by default.
1400
+ */
1401
+ private get perfLogEnabled();
1397
1402
  constructor(state: DurableObjectState, env: unknown, schemas?: CollectionSchema[], config?: RecordRoomConfig);
1398
1403
  private getPermissionContext;
1399
1404
  fetch(request: Request): Promise<Response>;
@@ -1406,12 +1411,13 @@ declare class RecordRoom<E = Record<string, unknown>> extends BaseRoom<E> {
1406
1411
  private ensureCollectionTable;
1407
1412
  private ensureAllCollectionTables;
1408
1413
  protected onConnect(ws: WebSocket, user: UserAttachment): Promise<ConnectionAttachment>;
1409
- protected onMessage(ws: WebSocket, user: UserAttachment, msg: {
1410
- type: string;
1411
- [key: string]: unknown;
1412
- }): Promise<void>;
1413
- protected onBinaryMessage(ws: WebSocket, user: UserAttachment, data: ArrayBuffer): Promise<void>;
1414
- protected onDisconnect(ws: WebSocket, user: UserAttachment): void;
1414
+ /**
1415
+ * Required to satisfy BaseRoom's abstract contract, but never invoked:
1416
+ * RecordRoom overrides `webSocketMessage`/`webSocketClose` directly (below),
1417
+ * so the BaseRoom dispatch path that would call this never runs. All real
1418
+ * message handling lives in `handleRecordMessage`.
1419
+ */
1420
+ protected onMessage(): void;
1415
1421
  webSocketMessage(ws: WebSocket, message: ArrayBuffer | string): Promise<void>;
1416
1422
  webSocketClose(ws: WebSocket, code: number, reason: string): Promise<void>;
1417
1423
  webSocketError(ws: WebSocket, error: unknown): Promise<void>;
@@ -1455,12 +1461,12 @@ declare class YjsRoom<E = Record<string, unknown>> extends BaseRoom<E> {
1455
1461
  private getDoc;
1456
1462
  private persistDoc;
1457
1463
  protected onConnect(ws: WebSocket, user: UserAttachment): YjsAttachment;
1458
- protected onMessage(ws: WebSocket, user: UserAttachment, message: {
1464
+ protected onMessage(_ws: WebSocket, _user: UserAttachment, _message: {
1459
1465
  type: string;
1460
1466
  [key: string]: unknown;
1461
1467
  }): void;
1462
1468
  protected onBinaryMessage(ws: WebSocket, user: UserAttachment, data: ArrayBuffer): void;
1463
- protected onDisconnect(ws: WebSocket, user: UserAttachment): void;
1469
+ protected onDisconnect(ws: WebSocket, _user: UserAttachment): void;
1464
1470
  private handleSync;
1465
1471
  private handleAwareness;
1466
1472
  private readAwarenessUpdates;
@@ -1542,13 +1548,13 @@ declare abstract class GameRoom<E = Record<string, unknown>> extends BaseRoom<E>
1542
1548
  */
1543
1549
  protected abstract onTick(state: Record<string, unknown>, inputs: GameInput[], tick: number): Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined>;
1544
1550
  /** Called when a player connects */
1545
- protected onPlayerJoin(player: Player): void;
1551
+ protected onPlayerJoin(_player: Player): void;
1546
1552
  /** Called when a player disconnects */
1547
- protected onPlayerLeave(player: Player): void;
1553
+ protected onPlayerLeave(_player: Player): void;
1548
1554
  /** Called when the game starts */
1549
1555
  protected onGameStart(): void;
1550
1556
  /** Called when the game ends */
1551
- protected onGameEnd(finalState: Record<string, unknown>): void;
1557
+ protected onGameEnd(_finalState: Record<string, unknown>): void;
1552
1558
  /**
1553
1559
  * Called once when the DO first hydrates persisted state from storage.
1554
1560
  * Receives the parsed state blob as it was written by a previous build.
@@ -2822,10 +2828,10 @@ interface CronContext {
2822
2828
  query(collection: string, opts?: {
2823
2829
  where?: Record<string, unknown>;
2824
2830
  limit?: number;
2825
- }): Promise<any[]>;
2826
- create(collection: string, data: Record<string, unknown>): Promise<any>;
2827
- update(collection: string, recordId: string, data: Record<string, unknown>): Promise<any>;
2828
- delete(collection: string, recordId: string): Promise<any>;
2831
+ }): Promise<unknown[]>;
2832
+ create(collection: string, data: Record<string, unknown>): Promise<unknown>;
2833
+ update(collection: string, recordId: string, data: Record<string, unknown>): Promise<unknown>;
2834
+ delete(collection: string, recordId: string): Promise<unknown>;
2829
2835
  };
2830
2836
  /**
2831
2837
  * Call a platform integration endpoint (e.g. "openai/chat-completion",
@@ -2833,7 +2839,7 @@ interface CronContext {
2833
2839
  * APP_OWNER_JWT the rest of the SDK uses for server-side billed calls.
2834
2840
  */
2835
2841
  integrations: {
2836
- call(endpoint: string, params?: Record<string, unknown>): Promise<any>;
2842
+ call(endpoint: string, params?: Record<string, unknown>): Promise<unknown>;
2837
2843
  };
2838
2844
  /** App owner's user ID */
2839
2845
  ownerUserId: string;
@@ -3455,8 +3461,7 @@ interface VerifiedAuth {
3455
3461
  userId: string;
3456
3462
  claims: JwtClaims;
3457
3463
  }
3458
- interface VerifyResult extends VerifiedAuth {
3459
- }
3464
+ type VerifyResult = VerifiedAuth;
3460
3465
  interface TokenDebugInfo {
3461
3466
  iss?: string | null;
3462
3467
  aud?: string | string[] | null;
package/dist/worker.js CHANGED
@@ -70,7 +70,7 @@ var BaseRoom = class {
70
70
  console.error(`[${this.constructor.name}] Message error:`, e);
71
71
  }
72
72
  }
73
- async webSocketClose(ws, code, reason) {
73
+ async webSocketClose(ws, _code, _reason) {
74
74
  const attachment = ws.deserializeAttachment();
75
75
  if (attachment) {
76
76
  try {
@@ -95,12 +95,12 @@ var BaseRoom = class {
95
95
  * Return an augmented attachment to serialize on the WebSocket,
96
96
  * or void to use the default attachment.
97
97
  */
98
- onConnect(ws, user) {
98
+ onConnect(_ws, _user) {
99
99
  }
100
100
  /**
101
101
  * Called when a WebSocket disconnects.
102
102
  */
103
- onDisconnect(ws, user) {
103
+ onDisconnect(_ws, _user) {
104
104
  }
105
105
  // ==========================================================================
106
106
  // Utilities
@@ -350,7 +350,7 @@ function coerceValue(value, storage, interpretation) {
350
350
  if (m) return parseFloat(m[1]) / 100;
351
351
  }
352
352
  if (interpretation.kind === "currency") {
353
- const cleaned2 = s.replace(/[^0-9.\-]/g, "");
353
+ const cleaned2 = s.replace(/[^0-9.-]/g, "");
354
354
  const n2 = parseFloat(cleaned2);
355
355
  if (!isNaN(n2)) return n2;
356
356
  return s;
@@ -540,9 +540,6 @@ var SchemaRegistry = class {
540
540
  has(name) {
541
541
  return this.trusted.has(name);
542
542
  }
543
- hasTrusted(name) {
544
- return this.trusted.has(name);
545
- }
546
543
  all() {
547
544
  return Array.from(this.trusted.values());
548
545
  }
@@ -1141,7 +1138,7 @@ function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRb
1141
1138
  const columns = getResolvedColumns(schema);
1142
1139
  const existing = getRecord(ctx.sql, collection, recordId, schema);
1143
1140
  const isUpdate = existing !== null;
1144
- let mergedData = isUpdate ? { ...existing.data, ...data } : { ...data };
1141
+ const mergedData = isUpdate ? { ...existing.data, ...data } : { ...data };
1145
1142
  for (const col of columns) {
1146
1143
  if (col.readonly) continue;
1147
1144
  if (!isUpdate) {
@@ -1196,7 +1193,10 @@ function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRb
1196
1193
  const permCtx = ctx.getPermissionContext();
1197
1194
  if (isUpdate) {
1198
1195
  if (!canUpdate(schema, userRole, { ...existing, recordId }, userId, permCtx)) {
1199
- return { success: false, error: `UPDATE DENIED: role=${userRole}, collection=${collection}` };
1196
+ return {
1197
+ success: false,
1198
+ error: `UPDATE DENIED: role=${userRole}, collection=${collection}`
1199
+ };
1200
1200
  }
1201
1201
  const fieldError = checkFieldPermissions(schema, userRole, finalData, existing.data);
1202
1202
  if (fieldError) {
@@ -1204,7 +1204,10 @@ function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRb
1204
1204
  }
1205
1205
  } else {
1206
1206
  if (!canCreate(schema, userRole)) {
1207
- return { success: false, error: `CREATE DENIED: role=${userRole}, collection=${collection}` };
1207
+ return {
1208
+ success: false,
1209
+ error: `CREATE DENIED: role=${userRole}, collection=${collection}`
1210
+ };
1208
1211
  }
1209
1212
  }
1210
1213
  }
@@ -1226,7 +1229,10 @@ function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRb
1226
1229
  );
1227
1230
  if (existing2.toArray().length > 0) {
1228
1231
  const fields = schema.uniqueOn.map((f) => `${f}=${finalData[f] ?? "null"}`).join(", ");
1229
- return { success: false, error: `Duplicate: a record with ${fields} already exists in ${collection}` };
1232
+ return {
1233
+ success: false,
1234
+ error: `Duplicate: a record with ${fields} already exists in ${collection}`
1235
+ };
1230
1236
  }
1231
1237
  }
1232
1238
  if (isUpdate) {
@@ -1240,13 +1246,16 @@ function putRecord(ctx, collection, recordId, data, userId, userRole, skipUserRb
1240
1246
  ctx.sql.exec(`UPDATE "${tbl}" SET ${setClauses.join(", ")} WHERE _row_id = ?`, ...params);
1241
1247
  } else {
1242
1248
  const colIds = Object.keys(colValues);
1243
- const allCols = ["_row_id", "_created_by", "_created_at", "_updated_at", ...colIds.map((c) => `"${c}"`)];
1249
+ const allCols = [
1250
+ "_row_id",
1251
+ "_created_by",
1252
+ "_created_at",
1253
+ "_updated_at",
1254
+ ...colIds.map((c) => `"${c}"`)
1255
+ ];
1244
1256
  const placeholders = allCols.map(() => "?").join(", ");
1245
1257
  const params = [recordId, userId, now, now, ...colIds.map((c) => colValues[c])];
1246
- ctx.sql.exec(
1247
- `INSERT INTO "${tbl}" (${allCols.join(", ")}) VALUES (${placeholders})`,
1248
- ...params
1249
- );
1258
+ ctx.sql.exec(`INSERT INTO "${tbl}" (${allCols.join(", ")}) VALUES (${placeholders})`, ...params);
1250
1259
  }
1251
1260
  const saved = getRecord(ctx.sql, collection, recordId, schema);
1252
1261
  const record = {
@@ -2653,9 +2662,21 @@ var RecordRoom = class extends BaseRoom {
2653
2662
  ownerUserId;
2654
2663
  /** True until the first fetch() completes — detects hibernation wake-up */
2655
2664
  freshConstruct = true;
2665
+ /**
2666
+ * Per-connection `[DO Perf]` timing logs are noisy on every hot path, so
2667
+ * they're gated behind a `DEEPSPACE_DO_PERF` env binding (set it to any
2668
+ * truthy value on the worker to opt in). Off by default.
2669
+ */
2670
+ get perfLogEnabled() {
2671
+ return !!this.env?.DEEPSPACE_DO_PERF;
2672
+ }
2656
2673
  constructor(state, env, schemas = [], config = {}) {
2657
2674
  super(state, env);
2658
- this.schemaRegistry = new SchemaRegistry([...SYSTEM_COLLECTION_SCHEMAS, BASE_USERS_SCHEMA, ...schemas]);
2675
+ this.schemaRegistry = new SchemaRegistry([
2676
+ ...SYSTEM_COLLECTION_SCHEMAS,
2677
+ BASE_USERS_SCHEMA,
2678
+ ...schemas
2679
+ ]);
2659
2680
  this.ownerUserId = config.ownerUserId ?? null;
2660
2681
  const wsCount = this.state.getWebSockets().length;
2661
2682
  if (wsCount > 0) {
@@ -2739,10 +2760,7 @@ var RecordRoom = class extends BaseRoom {
2739
2760
  const now = (/* @__PURE__ */ new Date()).toISOString();
2740
2761
  for (const row of oldUsers) {
2741
2762
  const r = row;
2742
- const existing = this.sql.exec(
2743
- `SELECT 1 FROM c_users WHERE _row_id = ?`,
2744
- r.user_id
2745
- ).toArray();
2763
+ const existing = this.sql.exec(`SELECT 1 FROM c_users WHERE _row_id = ?`, r.user_id).toArray();
2746
2764
  if (existing.length > 0) continue;
2747
2765
  this.sql.exec(
2748
2766
  `INSERT INTO c_users (_row_id, _created_by, _created_at, _updated_at, col_email, col_name, col_imageurl, col_role, col_createdat, col_lastseenat) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -2776,7 +2794,6 @@ var RecordRoom = class extends BaseRoom {
2776
2794
  }
2777
2795
  let migrated = 0;
2778
2796
  let skipped = 0;
2779
- const now = (/* @__PURE__ */ new Date()).toISOString();
2780
2797
  for (const row of rows) {
2781
2798
  const r = row;
2782
2799
  const schema = this.schemaRegistry.get(r.collection);
@@ -2785,17 +2802,11 @@ var RecordRoom = class extends BaseRoom {
2785
2802
  continue;
2786
2803
  }
2787
2804
  const tbl = collectionTableName(r.collection);
2788
- const tblExists = this.sql.exec(
2789
- `SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
2790
- tbl
2791
- ).toArray().length > 0;
2805
+ const tblExists = this.sql.exec(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, tbl).toArray().length > 0;
2792
2806
  if (!tblExists) {
2793
2807
  this.ensureCollectionTable(schema);
2794
2808
  }
2795
- const existing = this.sql.exec(
2796
- `SELECT 1 FROM "${tbl}" WHERE _row_id = ?`,
2797
- r.record_id
2798
- ).toArray();
2809
+ const existing = this.sql.exec(`SELECT 1 FROM "${tbl}" WHERE _row_id = ?`, r.record_id).toArray();
2799
2810
  if (existing.length > 0) {
2800
2811
  skipped++;
2801
2812
  continue;
@@ -2810,9 +2821,21 @@ var RecordRoom = class extends BaseRoom {
2810
2821
  const columns = schema.columns.map(resolveColumn);
2811
2822
  const colValues = dataToColumnValues(data, columns);
2812
2823
  const colIds = Object.keys(colValues);
2813
- const allCols = ["_row_id", "_created_by", "_created_at", "_updated_at", ...colIds.map((c) => `"${c}"`)];
2824
+ const allCols = [
2825
+ "_row_id",
2826
+ "_created_by",
2827
+ "_created_at",
2828
+ "_updated_at",
2829
+ ...colIds.map((c) => `"${c}"`)
2830
+ ];
2814
2831
  const placeholders = allCols.map(() => "?").join(", ");
2815
- const params = [r.record_id, r.created_by, r.created_at, r.updated_at, ...colIds.map((c) => colValues[c])];
2832
+ const params = [
2833
+ r.record_id,
2834
+ r.created_by,
2835
+ r.created_at,
2836
+ r.updated_at,
2837
+ ...colIds.map((c) => colValues[c])
2838
+ ];
2816
2839
  this.sql.exec(
2817
2840
  `INSERT INTO "${tbl}" (${allCols.join(", ")}) VALUES (${placeholders})`,
2818
2841
  ...params
@@ -2820,7 +2843,9 @@ var RecordRoom = class extends BaseRoom {
2820
2843
  migrated++;
2821
2844
  }
2822
2845
  if (migrated > 0 || skipped > 0) {
2823
- console.log(`[RecordRoom] Migrated ${migrated} records from document-mode, ${skipped} skipped`);
2846
+ console.log(
2847
+ `[RecordRoom] Migrated ${migrated} records from document-mode, ${skipped} skipped`
2848
+ );
2824
2849
  }
2825
2850
  this.sql.exec(`DROP TABLE IF EXISTS records`);
2826
2851
  } catch (error) {
@@ -2843,7 +2868,12 @@ var RecordRoom = class extends BaseRoom {
2843
2868
  // ============================================================================
2844
2869
  async onConnect(ws, user) {
2845
2870
  await this.ensureInitialized();
2846
- const timing = this._fetchTiming ?? { fetchStart: Date.now(), isColdStart: false, needsInit: false, initMs: 0 };
2871
+ const timing = this._fetchTiming ?? {
2872
+ fetchStart: Date.now(),
2873
+ isColdStart: false,
2874
+ needsInit: false,
2875
+ initMs: 0
2876
+ };
2847
2877
  const userId = user.userId;
2848
2878
  const userName = user.userName;
2849
2879
  const userEmail = user.userEmail;
@@ -2854,7 +2884,16 @@ var RecordRoom = class extends BaseRoom {
2854
2884
  const isOwner2 = this.ownerUserId != null && userId === this.ownerUserId;
2855
2885
  const regStart = Date.now();
2856
2886
  const defaultRole = this.schemaRegistry.get("users")?.defaultRole ?? "member";
2857
- const registeredUser = await registerUser(this.sql, userId, userName, userEmail, userImageUrl, isOwner2, defaultRole, this.schemaRegistry);
2887
+ const registeredUser = await registerUser(
2888
+ this.sql,
2889
+ userId,
2890
+ userName,
2891
+ userEmail,
2892
+ userImageUrl,
2893
+ isOwner2,
2894
+ defaultRole,
2895
+ this.schemaRegistry
2896
+ );
2858
2897
  const regMs = Date.now() - regStart;
2859
2898
  attachment = {
2860
2899
  userId: registeredUser.id,
@@ -2868,7 +2907,10 @@ var RecordRoom = class extends BaseRoom {
2868
2907
  };
2869
2908
  this.send(ws, { type: MSG.USER_INFO, payload: registeredUser });
2870
2909
  const totalMs = Date.now() - timing.fetchStart;
2871
- console.log(`[DO Perf] cold=${timing.isColdStart} | init: ${timing.initMs}ms reg: ${regMs}ms | total: ${totalMs}ms`);
2910
+ if (this.perfLogEnabled)
2911
+ console.log(
2912
+ `[DO Perf] cold=${timing.isColdStart} | init: ${timing.initMs}ms reg: ${regMs}ms | total: ${totalMs}ms`
2913
+ );
2872
2914
  } else {
2873
2915
  attachment = {
2874
2916
  userId,
@@ -2879,63 +2921,37 @@ var RecordRoom = class extends BaseRoom {
2879
2921
  yjsSubscriptions: [],
2880
2922
  yjsClientId: this.nextYjsClientId++
2881
2923
  };
2882
- this.send(ws, { type: MSG.USER_INFO, payload: {
2883
- id: userId,
2884
- name: "Anonymous",
2885
- email: "",
2886
- role: ROLE_ANONYMOUS,
2887
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2888
- lastSeenAt: (/* @__PURE__ */ new Date()).toISOString()
2889
- } });
2924
+ this.send(ws, {
2925
+ type: MSG.USER_INFO,
2926
+ payload: {
2927
+ id: userId,
2928
+ name: "Anonymous",
2929
+ email: "",
2930
+ role: ROLE_ANONYMOUS,
2931
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2932
+ lastSeenAt: (/* @__PURE__ */ new Date()).toISOString()
2933
+ }
2934
+ });
2890
2935
  const totalMs = Date.now() - timing.fetchStart;
2891
- console.log(`[DO Perf] cold=${timing.isColdStart} anon | total: ${totalMs}ms`);
2936
+ if (this.perfLogEnabled)
2937
+ console.log(`[DO Perf] cold=${timing.isColdStart} anon | total: ${totalMs}ms`);
2892
2938
  }
2893
2939
  this._fetchTiming = null;
2894
2940
  return attachment;
2895
2941
  }
2896
- async onMessage(ws, user, msg) {
2897
- await this.ensureInitialized();
2898
- const attachment = ws.deserializeAttachment();
2899
- if (!attachment) {
2900
- this.send(ws, { type: MSG.ERROR, payload: { error: "Connection not found" } });
2901
- return;
2902
- }
2903
- try {
2904
- await this.handleRecordMessage(ws, attachment, msg);
2905
- } catch (e) {
2906
- const errMsg = e instanceof Error ? e.message : String(e);
2907
- console.error(`[RecordRoom] Message handler error: ${errMsg}`);
2908
- this.send(ws, { type: MSG.ERROR, payload: { error: `Invalid message: ${errMsg}` } });
2909
- }
2910
- }
2911
- async onBinaryMessage(ws, user, data) {
2912
- await this.ensureInitialized();
2913
- const attachment = ws.deserializeAttachment();
2914
- if (!attachment) return;
2915
- try {
2916
- await handleYjsBinaryMessage(
2917
- this.createYjsContext(),
2918
- ws,
2919
- attachment,
2920
- new Uint8Array(data)
2921
- );
2922
- } catch (e) {
2923
- console.error("[RecordRoom] Yjs binary message error:", e);
2924
- }
2925
- }
2926
- onDisconnect(ws, user) {
2927
- console.log(`[RecordRoom] webSocketClose`);
2928
- try {
2929
- const attachment = ws.deserializeAttachment();
2930
- if (attachment) {
2931
- broadcastAwarenessRemoval(this.createYjsContext(), attachment);
2932
- }
2933
- } catch {
2934
- }
2942
+ /**
2943
+ * Required to satisfy BaseRoom's abstract contract, but never invoked:
2944
+ * RecordRoom overrides `webSocketMessage`/`webSocketClose` directly (below),
2945
+ * so the BaseRoom dispatch path that would call this never runs. All real
2946
+ * message handling lives in `handleRecordMessage`.
2947
+ */
2948
+ onMessage() {
2949
+ throw new Error("RecordRoom.onMessage is unreachable; use webSocketMessage");
2935
2950
  }
2936
2951
  // ============================================================================
2937
- // Hibernation API (delegates to BaseRoom via super, but we need
2938
- // to ensure webSocketMessage gets the ensureInitialized treatment)
2952
+ // Hibernation API — RecordRoom overrides these directly (rather than using
2953
+ // BaseRoom's onMessage/onBinaryMessage/onDisconnect hooks) so it can run
2954
+ // ensureInitialized() before every message after a hibernation wake-up.
2939
2955
  // ============================================================================
2940
2956
  async webSocketMessage(ws, message) {
2941
2957
  await this.ensureInitialized();
@@ -3006,7 +3022,12 @@ var RecordRoom = class extends BaseRoom {
3006
3022
  handleUserList(userCtx, ws, attachment);
3007
3023
  break;
3008
3024
  case MSG.USER_UPDATE:
3009
- handleUserUpdate(recordCtx, ws, attachment, payload);
3025
+ handleUserUpdate(
3026
+ recordCtx,
3027
+ ws,
3028
+ attachment,
3029
+ payload
3030
+ );
3010
3031
  break;
3011
3032
  case MSG.SET_ROLE:
3012
3033
  await handleSetRole(userCtx, ws, attachment, payload);
@@ -3172,7 +3193,7 @@ var YjsRoom = class extends BaseRoom {
3172
3193
  this.sendAwarenessSnapshot(ws);
3173
3194
  return attachment;
3174
3195
  }
3175
- onMessage(ws, user, message) {
3196
+ onMessage(_ws, _user, _message) {
3176
3197
  }
3177
3198
  onBinaryMessage(ws, user, data) {
3178
3199
  const bytes = new Uint8Array(data);
@@ -3184,7 +3205,7 @@ var YjsRoom = class extends BaseRoom {
3184
3205
  this.handleAwareness(ws, decoder, bytes);
3185
3206
  }
3186
3207
  }
3187
- onDisconnect(ws, user) {
3208
+ onDisconnect(ws, _user) {
3188
3209
  const attachment = ws.deserializeAttachment();
3189
3210
  if (attachment?.awarenessClientId == null) return;
3190
3211
  const clientId = attachment.awarenessClientId;
@@ -3545,16 +3566,16 @@ var GameRoom = class extends BaseRoom {
3545
3566
  return this.currentTick;
3546
3567
  }
3547
3568
  /** Called when a player connects */
3548
- onPlayerJoin(player) {
3569
+ onPlayerJoin(_player) {
3549
3570
  }
3550
3571
  /** Called when a player disconnects */
3551
- onPlayerLeave(player) {
3572
+ onPlayerLeave(_player) {
3552
3573
  }
3553
3574
  /** Called when the game starts */
3554
3575
  onGameStart() {
3555
3576
  }
3556
3577
  /** Called when the game ends */
3557
- onGameEnd(finalState) {
3578
+ onGameEnd(_finalState) {
3558
3579
  }
3559
3580
  /**
3560
3581
  * Called once when the DO first hydrates persisted state from storage.
@@ -6139,7 +6160,8 @@ function buildCronContext(env, ownerUserId, roomId = "default") {
6139
6160
  if (opts?.where) params.where = opts.where;
6140
6161
  if (opts?.limit) params.limit = opts.limit;
6141
6162
  const data = await executeTool3("records.query", params);
6142
- return data.records || [];
6163
+ const records2 = data?.records;
6164
+ return records2 ?? [];
6143
6165
  },
6144
6166
  async create(collection, data) {
6145
6167
  return executeTool3("records.create", { collection, data });
@@ -6296,7 +6318,8 @@ async function composioTools(env, options) {
6296
6318
  if (!list.ok || !list.success) {
6297
6319
  throw new Error(`composioTools: failed to list Composio tools: ${list.detail}`);
6298
6320
  }
6299
- const items = Array.isArray(list.data?.items) ? list.data.items : [];
6321
+ const listItems = list.data?.items;
6322
+ const items = Array.isArray(listItems) ? listItems : [];
6300
6323
  const tools = {};
6301
6324
  for (const item of items) {
6302
6325
  if (!item?.slug) continue;