@fitzzero/quickdraw-core 5.1.2 → 5.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +793 -57
  2. package/dist/{admin-DWWrT1h6.d.ts → admin-CS7JB_UH.d.ts} +2 -2
  3. package/dist/{adminSchemas-D4bFKsrb.d.ts → adminSchemas-29cGgrg5.d.ts} +1 -1
  4. package/dist/{auth-DXiSJZaf.d.ts → auth-DtdYjrZ9.d.ts} +11 -7
  5. package/dist/{caller-BT-Mv4bD.d.ts → caller-BTwGl7vm.d.ts} +183 -15
  6. package/dist/chunk-2FDSKLBO.js +82 -0
  7. package/dist/chunk-2FDSKLBO.js.map +1 -0
  8. package/dist/{chunk-GSIPUNJL.js → chunk-64I6MYBC.js} +139 -792
  9. package/dist/chunk-64I6MYBC.js.map +1 -0
  10. package/dist/chunk-7473HK24.js +17 -0
  11. package/dist/chunk-7473HK24.js.map +1 -0
  12. package/dist/chunk-7Y5ROPXL.js +33 -0
  13. package/dist/chunk-7Y5ROPXL.js.map +1 -0
  14. package/dist/chunk-A2RBQD66.js +7 -0
  15. package/dist/chunk-A2RBQD66.js.map +1 -0
  16. package/dist/chunk-ASAMUGHG.js +651 -0
  17. package/dist/chunk-ASAMUGHG.js.map +1 -0
  18. package/dist/{chunk-B2P7AZ25.js → chunk-CYE4UBOB.js} +6 -4
  19. package/dist/chunk-CYE4UBOB.js.map +1 -0
  20. package/dist/chunk-DNP65TD5.js +832 -0
  21. package/dist/chunk-DNP65TD5.js.map +1 -0
  22. package/dist/{chunk-5CGO426C.js → chunk-GYLWEJHW.js} +5 -1
  23. package/dist/chunk-GYLWEJHW.js.map +1 -0
  24. package/dist/{chunk-AONAFOYU.js → chunk-I3JCFS7H.js} +25 -13
  25. package/dist/chunk-I3JCFS7H.js.map +1 -0
  26. package/dist/{chunk-X5CWPX7H.js → chunk-IIQ3WMJK.js} +13 -2
  27. package/dist/chunk-IIQ3WMJK.js.map +1 -0
  28. package/dist/{chunk-6K5BY47X.js → chunk-JVTONQFI.js} +1277 -277
  29. package/dist/chunk-JVTONQFI.js.map +1 -0
  30. package/dist/{chunk-RL4VKLBX.js → chunk-KKQANRLG.js} +2 -2
  31. package/dist/{chunk-RL4VKLBX.js.map → chunk-KKQANRLG.js.map} +1 -1
  32. package/dist/{chunk-GKH3LJ7N.js → chunk-MPQAT55F.js} +38 -205
  33. package/dist/chunk-MPQAT55F.js.map +1 -0
  34. package/dist/chunk-NX2V6SIL.js +202 -0
  35. package/dist/chunk-NX2V6SIL.js.map +1 -0
  36. package/dist/{chunk-7QCUX4TK.js → chunk-NZHA4TZD.js} +72 -175
  37. package/dist/chunk-NZHA4TZD.js.map +1 -0
  38. package/dist/{chunk-MBH6IDSM.js → chunk-OCZJ4EYV.js} +40 -17
  39. package/dist/chunk-OCZJ4EYV.js.map +1 -0
  40. package/dist/{chunk-RJ5I2VO3.js → chunk-PHCURHC7.js} +6 -6
  41. package/dist/chunk-PHCURHC7.js.map +1 -0
  42. package/dist/{chunk-4YO5H5ON.js → chunk-Q4OGQM5I.js} +4 -10
  43. package/dist/chunk-Q4OGQM5I.js.map +1 -0
  44. package/dist/chunk-QSU47XEX.js +13 -0
  45. package/dist/chunk-QSU47XEX.js.map +1 -0
  46. package/dist/{chunk-LCC7HNFP.js → chunk-RZXJTDH5.js} +2 -2
  47. package/dist/{chunk-Z2LZ7IZ7.js → chunk-SN3OZVEQ.js} +5 -31
  48. package/dist/chunk-SN3OZVEQ.js.map +1 -0
  49. package/dist/{chunk-Z5ILULF2.js → chunk-SQ5VUG77.js} +4 -3
  50. package/dist/{chunk-Z5ILULF2.js.map → chunk-SQ5VUG77.js.map} +1 -1
  51. package/dist/chunk-STH5DLQP.js +1192 -0
  52. package/dist/chunk-STH5DLQP.js.map +1 -0
  53. package/dist/{chunk-NMXYENH4.js → chunk-VZFJTKIW.js} +6 -6
  54. package/dist/{chunk-753VJIUP.js → chunk-X4ZU4QH5.js} +43 -9
  55. package/dist/chunk-X4ZU4QH5.js.map +1 -0
  56. package/dist/chunk-XD7SRDUY.js +188 -0
  57. package/dist/chunk-XD7SRDUY.js.map +1 -0
  58. package/dist/chunk-Z7I6SONO.js +825 -0
  59. package/dist/chunk-Z7I6SONO.js.map +1 -0
  60. package/dist/cli/quickdraw-docs.js +131 -113
  61. package/dist/cli/quickdraw-docs.js.map +1 -1
  62. package/dist/client/core.d.ts +387 -0
  63. package/dist/client/core.js +109 -0
  64. package/dist/client/core.js.map +1 -0
  65. package/dist/client/index.d.ts +30 -193
  66. package/dist/client/index.js +56 -217
  67. package/dist/client/index.js.map +1 -1
  68. package/dist/{clientTypes-hWcPUVS9.d.ts → clientTypes-CUUNJwWs.d.ts} +3 -3
  69. package/dist/{connection-CgijdGpT.d.ts → connection-2EeQ_Et0.d.ts} +16 -9
  70. package/dist/{createServer-Cp-Wk500.d.ts → createServer-Bx5HOcYK.d.ts} +37 -9
  71. package/dist/{createTestApp-BB_PkqQ7.d.ts → createTestApp-DAMANT79.d.ts} +36 -9
  72. package/dist/{envelope-DzWyIXjj.d.ts → envelope-BMBaxppV.d.ts} +200 -8
  73. package/dist/{http-DotQ5bj8.d.ts → http-YzLAar3w.d.ts} +15 -50
  74. package/dist/index.d.ts +7 -6
  75. package/dist/index.js +61 -28
  76. package/dist/index.js.map +1 -1
  77. package/dist/{infer-BmoZsL9a.d.ts → infer-Br62cnyW.d.ts} +39 -1
  78. package/dist/{metrics-DW80Exve.d.ts → metrics-CTVpw0gu.d.ts} +2 -2
  79. package/dist/parser.js +2 -1
  80. package/dist/prisma/index.d.ts +13 -6
  81. package/dist/prisma/index.js +155 -9
  82. package/dist/prisma/index.js.map +1 -1
  83. package/dist/remote-N7OUAGOW.js +15 -0
  84. package/dist/remote-N7OUAGOW.js.map +1 -0
  85. package/dist/server/auth/index.d.ts +3 -3
  86. package/dist/server/auth/index.js +5 -3
  87. package/dist/server/auth/index.js.map +1 -1
  88. package/dist/server/index.d.ts +140 -36
  89. package/dist/server/index.js +162 -55
  90. package/dist/server/index.js.map +1 -1
  91. package/dist/server/mcp/index.d.ts +34 -18
  92. package/dist/server/mcp/index.js +29 -12
  93. package/dist/server/mcp/index.js.map +1 -1
  94. package/dist/server/otel.d.ts +3 -3
  95. package/dist/{sharing-UDLn1049.d.ts → sharing-BrsmlDv_.d.ts} +3 -3
  96. package/dist/testing/client.d.ts +13 -13
  97. package/dist/testing/client.js +17 -10
  98. package/dist/testing/client.js.map +1 -1
  99. package/dist/testing/index.d.ts +49 -12
  100. package/dist/testing/index.js +102 -19
  101. package/dist/testing/index.js.map +1 -1
  102. package/dist/testing/mock.d.ts +5 -5
  103. package/dist/testing/mock.js +15 -9
  104. package/dist/tracked-BR5b2O-U.d.ts +43 -0
  105. package/dist/{types-CYliTP65.d.ts → types-CLRCQGnO.d.ts} +4 -3
  106. package/dist/types-tMagHbjy.d.ts +151 -0
  107. package/dist/utils/index.d.ts +16 -4
  108. package/dist/utils/index.js +6 -3
  109. package/package.json +5 -1
  110. package/dist/chunk-4YO5H5ON.js.map +0 -1
  111. package/dist/chunk-5CGO426C.js.map +0 -1
  112. package/dist/chunk-6K5BY47X.js.map +0 -1
  113. package/dist/chunk-753VJIUP.js.map +0 -1
  114. package/dist/chunk-7QCUX4TK.js.map +0 -1
  115. package/dist/chunk-AONAFOYU.js.map +0 -1
  116. package/dist/chunk-B2P7AZ25.js.map +0 -1
  117. package/dist/chunk-GKH3LJ7N.js.map +0 -1
  118. package/dist/chunk-GSIPUNJL.js.map +0 -1
  119. package/dist/chunk-MBH6IDSM.js.map +0 -1
  120. package/dist/chunk-RJ5I2VO3.js.map +0 -1
  121. package/dist/chunk-X5CWPX7H.js.map +0 -1
  122. package/dist/chunk-Z2LZ7IZ7.js.map +0 -1
  123. /package/dist/{chunk-LCC7HNFP.js.map → chunk-RZXJTDH5.js.map} +0 -0
  124. /package/dist/{chunk-NMXYENH4.js.map → chunk-VZFJTKIW.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/client/auth.ts","../../src/client/newId.ts","../../src/client/live/usePresence.ts","../../src/client/join.ts","../../src/client/adminScreen.ts"],"sourcesContent":["// The browser side of the auth routes kit (RFC 0003 section 12.6), and\n// token storage. `authProviders`, `signInUrl`, `signOut` and\n// `signOutEverywhere` use the routes `createAuthRoutes` serves (`GET\n// {basePath}/providers`, `GET {basePath}/{provider}/start`, `POST\n// {basePath}/logout`, `POST {basePath}/logout-all`); the last three replace\n// 4.1's `getOAuthUrl`, `logout` and `logoutAllDevices`\n// (4.1 `src/client/utils/auth.ts`), which called routes the kit does not\n// serve and sent only a stored token, so with cookie sessions they signed\n// nobody out (finding F3.3). The token helpers keep a bearer token for\n// clients without cookies; they read `localStorage`, so they stay on\n// `./client`; `parseJWTPayload`, which needs no storage, moved to the\n// isomorphic `./utils` entry (`../utils/jwt.ts`).\n//\n// A POST sends the session cookie (`credentials: \"include\"`: the API is\n// often on another origin, whose CORS must then allow the web app's origin\n// with credentials) and `Content-Type: application/json`, which the routes\n// require (a cross-site form cannot send it), and the stored token as a\n// bearer token when there is one. A refusal or an unreachable server\n// rejects with a `QuickdrawError`: nothing fails silently.\n//\n// 4.1 assumed a browser wherever `window` exists. React Native defines\n// `window` without `localStorage`, and a browser with storage turned off\n// throws when it is used; there the token is kept in memory for the life of\n// the app. Without `window` (a server rendering a page) nothing is kept, so\n// one request's token never reaches another. The API's URL defaults to\n// `NEXT_PUBLIC_API_URL` where `process.env` can be read (Next.js inlines it\n// when it builds a page), and to `http://localhost:4000` elsewhere.\n\nimport { QuickdrawError, isErrorCode } from \"../protocol/errors\";\nimport { isRecord } from \"../protocol/guards\";\n\nconst AUTH_TOKEN_KEY = \"auth_token\";\n\nconst DEFAULT_API_URL = \"http://localhost:4000\";\n\n/** The token, where `localStorage` cannot be used. */\nlet memoryToken: string | null = null;\n\n/** True where a client keeps a token: there is a `window` (a browser, React Native). */\nfunction onClient(): boolean {\n return typeof window !== \"undefined\";\n}\n\n/** The token `localStorage` holds; `undefined` when there is no `localStorage` to use. */\nfunction storedToken(): string | null | undefined {\n try {\n return globalThis.localStorage.getItem(AUTH_TOKEN_KEY);\n } catch {\n return undefined;\n }\n}\n\n/** Tells the page the token changed, where there is a page to tell. */\nfunction announce(): void {\n if (typeof window.dispatchEvent === \"function\" && typeof Event === \"function\") {\n window.dispatchEvent(new Event(\"auth-token-changed\"));\n }\n}\n\n/** The API's URL: `apiUrl`, else `NEXT_PUBLIC_API_URL` where `process.env` can be read, else localhost. */\nfunction apiUrlOf(apiUrl: string | undefined): string {\n if (apiUrl !== undefined) {\n return apiUrl;\n }\n try {\n // Written out whole, so Next.js can inline it when it builds a page.\n return process.env.NEXT_PUBLIC_API_URL ?? DEFAULT_API_URL;\n } catch {\n // No `process` here (React Native, a bundler that provides none).\n return DEFAULT_API_URL;\n }\n}\n\n/**\n * Get the stored auth token: from `localStorage`, or from memory where it\n * cannot be used; `null` without a `window`.\n */\nexport function getAuthToken(): string | null {\n if (!onClient()) return null;\n const stored = storedToken();\n return stored === undefined ? memoryToken : stored;\n}\n\n/**\n * Store the auth token (in `localStorage`, else in memory) and dispatch a\n * change event. Does nothing without a `window`.\n */\nexport function setAuthToken(token: string): void {\n if (!onClient()) return;\n memoryToken = token;\n try {\n globalThis.localStorage.setItem(AUTH_TOKEN_KEY, token);\n } catch {\n // No usable localStorage: the token is kept in memory.\n }\n announce();\n}\n\n/**\n * Remove the auth token and dispatch a change event. Does nothing without a\n * `window`.\n */\nexport function clearAuthToken(): void {\n if (!onClient()) return;\n memoryToken = null;\n try {\n globalThis.localStorage.removeItem(AUTH_TOKEN_KEY);\n } catch {\n // No usable localStorage: forgetting the token in memory is enough.\n }\n announce();\n}\n\n/** Where the auth routes are, for the browser helpers. */\nexport interface AuthRoutesTarget {\n /**\n * The API's URL, where `createAuthRoutes` is mounted:\n * `\"https://api.example.com\"`. Default: `NEXT_PUBLIC_API_URL` where it can\n * be read, else `http://localhost:4000`.\n */\n readonly apiUrl?: string;\n /** The routes' `basePath`, as `createAuthRoutes` was given it. Default `/auth`. */\n readonly basePath?: string;\n}\n\n/** Options of {@link signInUrl}. */\nexport interface SignInUrlOptions extends AuthRoutesTarget {\n /**\n * The page to come back to: an origin or a URL on one, of which the\n * routes keep the origin when `allowedOrigins` lists it, and land on\n * `{origin}{successPath}`. Default: the current page's origin where there\n * is a page; elsewhere the routes take their first allowed origin.\n */\n readonly returnTo?: string;\n}\n\nconst PROVIDER_ID = /^[a-z0-9][a-z0-9-]*$/;\n\n/** The routes' base URL: `{apiUrl}{basePath}`, checked. */\nfunction routesUrl(owner: string, target: AuthRoutesTarget): string {\n const base = apiUrlOf(target.apiUrl).replace(/\\/+$/, \"\");\n const path = target.basePath ?? \"/auth\";\n if (typeof path !== \"string\" || !path.startsWith(\"/\")) {\n throw new TypeError(`${owner}: basePath must start with \"/\", as createAuthRoutes takes it`);\n }\n return `${base}${path.replace(/\\/+$/, \"\")}`;\n}\n\n/** The current page's origin, where there is a page. */\nfunction pageOrigin(): string | undefined {\n try {\n const origin: unknown = (globalThis as { readonly location?: { readonly origin?: unknown } })\n .location?.origin;\n return typeof origin === \"string\" && origin !== \"null\" ? origin : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * The URL that starts a sign-in with `provider`: the auth routes' `GET\n * {basePath}/{provider}/start?returnTo=`, which redirects to the provider\n * and, once the user consents, back to `returnTo`'s origin with the session\n * cookie set (or to the routes' `errorPath` with `?error=`). Navigate to it:\n * a link, or `location.assign`. Throws a `TypeError` for a provider id the\n * routes cannot have (lowercase letters, digits and hyphens).\n *\n * @example\n * <a href={signInUrl(\"google\", { apiUrl: API_URL })}>Sign in with Google</a>\n */\nexport function signInUrl(provider: string, options: SignInUrlOptions = {}): string {\n if (typeof provider !== \"string\" || !PROVIDER_ID.test(provider)) {\n throw new TypeError(\n `signInUrl: a provider id is lowercase letters, digits and hyphens; got ${String(provider)}`,\n );\n }\n const url = new URL(`${routesUrl(\"signInUrl\", options)}/${provider}/start`);\n const returnTo = options.returnTo ?? pageOrigin();\n if (returnTo !== undefined) {\n url.searchParams.set(\"returnTo\", returnTo);\n }\n return url.toString();\n}\n\n/** The error a route's refusal stands for: its `{ error, message }` body, else its HTTP status. */\nasync function refusalOf(response: Response): Promise<QuickdrawError> {\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = undefined;\n }\n const code = isRecord(body) && isErrorCode(body.error) ? body.error : undefined;\n const message =\n isRecord(body) && typeof body.message === \"string\"\n ? body.message\n : `The auth route answered ${String(response.status)}`;\n if (code !== undefined) {\n return new QuickdrawError(code, message);\n }\n return new QuickdrawError(response.status === 429 ? \"RATE_LIMITED\" : \"INTERNAL\", message);\n}\n\n/** POSTs to one session route with the cookie, the JSON content type and the stored token. */\nasync function postRoute(owner: string, route: string, target: AuthRoutesTarget): Promise<void> {\n const url = `${routesUrl(owner, target)}${route}`;\n const token = getAuthToken();\n let response: Response;\n try {\n response = await fetch(url, {\n method: \"POST\",\n credentials: \"include\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(token === null ? {} : { Authorization: `Bearer ${token}` }),\n },\n body: \"{}\",\n });\n } catch (error) {\n throw new QuickdrawError(\"INTERNAL\", `${owner}: ${url} could not be reached`, {\n cause: error instanceof Error ? error.message : String(error),\n });\n }\n if (!response.ok) {\n throw await refusalOf(response);\n }\n}\n\n/**\n * Signs out: `POST {basePath}/logout`, which revokes the request's session\n * and clears its cookie, sent with the session cookie and, when one is\n * stored, the bearer token; the stored token is forgotten either way.\n * Rejects with a `QuickdrawError` when the server cannot be reached or\n * refuses (`RATE_LIMITED`, say), so the app knows the session may still be\n * live. The connection keeps the user it signed in as until it connects\n * again: with cookie sessions, reconnect it (`connection.close()`, then\n * `open()`), or load the next page; a provider whose `auth` was the stored\n * token reconnects when the app clears it.\n *\n * @example\n * await signOut({ apiUrl: API_URL });\n */\nexport async function signOut(options: AuthRoutesTarget = {}): Promise<void> {\n try {\n await postRoute(\"signOut\", \"/logout\", options);\n } finally {\n clearAuthToken();\n }\n}\n\n/** A sign-in the API's auth routes serve, as `GET {basePath}/providers` lists it ({@link authProviders}). */\nexport interface AuthProviderInfo {\n /** Its id: `signInUrl(id)` starts it; a guest's is `POST {basePath}/guest`. */\n readonly id: string;\n /** A name for its button: `\"Google\"`, `\"Discord\"`, `\"Mock\"`, `\"Guest\"`, or an OAuth provider's own. */\n readonly name: string;\n /** `\"oauth\"` (a redirecting provider), `\"mock\"` (the development picker) or `\"guest\"`. */\n readonly kind: \"oauth\" | \"mock\" | \"guest\";\n}\n\nconst PROVIDER_KINDS: ReadonlySet<unknown> = new Set([\"oauth\", \"mock\", \"guest\"]);\n\n/** One entry of the routes' list, its known fields only (a newer server may add others). */\nfunction providerInfoOf(value: unknown): AuthProviderInfo | undefined {\n if (!isRecord(value) || typeof value.id !== \"string\" || !PROVIDER_ID.test(value.id)) {\n return undefined;\n }\n const kind = PROVIDER_KINDS.has(value.kind)\n ? (value.kind as AuthProviderInfo[\"kind\"])\n : undefined;\n return kind === undefined\n ? undefined\n : { id: value.id, name: typeof value.name === \"string\" ? value.name : value.id, kind };\n}\n\n/**\n * The sign-ins the API serves, in the order its routes list them: `GET\n * {basePath}/providers` (finding F9.1 of the owner's QA of the template), so\n * a login page renders a button only for a provider that is there, whatever\n * the web app was built with: a provider whose credentials the server lacks\n * is not listed, nor the mock where it is off. Rejects with a\n * `QuickdrawError` when the server cannot be reached or refuses.\n *\n * @example\n * const { data: providers = [] } = useQuery({\n * queryKey: [\"auth\", \"providers\"],\n * queryFn: () => authProviders({ apiUrl: API_URL }),\n * });\n */\nexport async function authProviders(\n options: AuthRoutesTarget = {},\n): Promise<readonly AuthProviderInfo[]> {\n const url = `${routesUrl(\"authProviders\", options)}/providers`;\n let response: Response;\n try {\n response = await fetch(url, { headers: { Accept: \"application/json\" } });\n } catch (error) {\n throw new QuickdrawError(\"INTERNAL\", `authProviders: ${url} could not be reached`, {\n cause: error instanceof Error ? error.message : String(error),\n });\n }\n if (!response.ok) {\n throw await refusalOf(response);\n }\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = undefined;\n }\n if (!isRecord(body) || !Array.isArray(body.providers)) {\n throw new QuickdrawError(\"INTERNAL\", `authProviders: ${url} answered no list of providers`);\n }\n return body.providers.flatMap((entry: unknown) => {\n const info = providerInfoOf(entry);\n return info === undefined ? [] : [info];\n });\n}\n\n/**\n * Signs the user out everywhere: `POST {basePath}/logout-all`, which\n * revokes every session of the user (the app's `onRevoke` can disconnect\n * their sockets) and clears this browser's cookie; the stored token is\n * forgotten either way. Rejects with `UNAUTHENTICATED` when the request\n * carries no live session, and with a `QuickdrawError` when the server\n * cannot be reached or refuses.\n */\nexport async function signOutEverywhere(options: AuthRoutesTarget = {}): Promise<void> {\n try {\n await postRoute(\"signOutEverywhere\", \"/logout-all\", options);\n } finally {\n clearAuthToken();\n }\n}\n","// `newId()` (finding F11.2 of the quickdraw-chat migration): an id for a row\n// the client creates and the server keeps, so that an optimistic item whose\n// call's outcome is unknown can be found by its scope's next load, and its\n// `retry()` cannot write a second row (`additions.ts`). A version 4 UUID,\n// from `crypto.randomUUID()` where the runtime has it, else made from\n// `crypto.getRandomValues()`: browsers give `randomUUID` to secure pages only\n// (https, localhost), and a dev server opened at its LAN address is plain\n// http, while `getRandomValues` is on every page, in Node, and in React\n// Native with a `getRandomValues` polyfill.\n//\n// React-free, and no dependency.\n\n/** The Web Crypto members `newId` uses. */\ninterface IdCrypto {\n readonly randomUUID?: () => string;\n readonly getRandomValues?: (array: Uint8Array) => Uint8Array;\n}\n\n/** Byte `index` of a version 4 UUID made from the random `byte`: the version in byte 6, the RFC 9562 variant in byte 8. */\nfunction uuidByte(byte: number, index: number): number {\n if (index === 6) {\n return 0x40 + (byte % 0x10);\n }\n return index === 8 ? 0x80 + (byte % 0x40) : byte;\n}\n\n/** 16 random bytes as a version 4 UUID. */\nfunction uuidOf(bytes: Uint8Array): string {\n const hex = Array.from(bytes, (byte, index) =>\n uuidByte(byte, index).toString(16).padStart(2, \"0\"),\n ).join(\"\");\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n\n/**\n * A new id for a row the client creates and the server keeps: a version 4\n * UUID, from `crypto.randomUUID()`, or from `crypto.getRandomValues()` where\n * that is missing (a page over plain http, which browsers do not give\n * `randomUUID`). Give it to an item an optimistic update adds and to the\n * create's input, so the scope's next load can find the item after a lost\n * answer and `retry()` cannot write twice. Throws a `TypeError` in a runtime\n * without Web Crypto.\n *\n * @example\n * create.mutate({ id: newId(), chatId, content });\n */\nexport function newId(): string {\n const { crypto } = globalThis as { readonly crypto?: IdCrypto };\n if (typeof crypto?.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n if (typeof crypto?.getRandomValues !== \"function\") {\n throw new TypeError(\"newId: this runtime has no crypto.getRandomValues\");\n }\n return uuidOf(crypto.getRandomValues(new Uint8Array(16)));\n}\n","\"use client\";\n\n// `usePresence(room)` (RFC 0003 section 12.5): the ids of the users in an\n// app room the connection's socket is in, kept current by the server's\n// `qd:presence` frames (`presence.ts`). A method joins the socket to the room\n// (`ctx.rooms.join`); until then, and after it leaves or the socket\n// disconnects, the list is empty.\n\nimport { useCallback, useSyncExternalStore } from \"react\";\nimport { serverStateOf } from \"../context\";\nimport { useLiveData } from \"./liveHooks\";\n\nconst NOBODY: readonly string[] = Object.freeze([]);\n\n/** The users in `room`, as the server last said: the same array until it changes. */\nexport function usePresence(room: string): readonly string[] {\n const { connection, live, awaiting } = useLiveData(\"usePresence\");\n const listen = useCallback(\n (listener: () => void) => live.presence.listen(room, listener),\n [live, room],\n );\n // Awaiting new credentials' hello, it shows nobody the last user's socket saw.\n const read = (): readonly string[] => (awaiting ? NOBODY : live.presence.users(room));\n // A server's socket is in no room: nobody, unless its state has a hello (a mock's session).\n const onServer = (): readonly string[] =>\n serverStateOf(connection).hello === null ? NOBODY : live.presence.users(room);\n return useSyncExternalStore(listen, read, onServer);\n}\n","\"use client\";\n\n// `useJoin(member, input, options)` (RFC 0003 sections 11 and 12.5): runs a\n// joining call (a method whose handler puts the calling socket in an app\n// room with `ctx.rooms.join`, a game's `watchWorld`, a board's `enterBoard`)\n// on every `qd:hello`, not on every render. An app room belongs to one\n// socket: a reconnect (a lost network, a `qd:rotate`, new credentials) is a\n// new socket in no room, which hears none of the room's events and whose\n// channel messages that require the room are dropped, while a query's cached\n// answer still looks fine and nothing reads it again. 4.1 apps re-joined from\n// `isConnected` effects by hand.\n//\n// - Each hello runs the call once more (`connection.onHello`), and so does a\n// new input (compared by value, as query keys are). A failure shows as\n// `error` until the next hello; `RATE_LIMITED` is tried again once its\n// backoff ends.\n// - `status` is the current socket's: `\"idle\"` while there is none to join\n// with (before the first hello, while reconnecting) or `enabled` is false,\n// `\"joining\"` while the call runs, then `\"joined\"` or `\"error\"`. `data` is\n// what the last successful call returned, kept through a reconnect.\n// - `retry()` runs the call again at once on the current socket, for a\n// join that was refused (finding F6.3 of the quickdraw-chat migration: a\n// refused first join could be retried only by toggling `enabled`). It\n// does nothing while there is no socket to join with or `enabled` is\n// false: the next hello joins anyway.\n// - A join answered after its hello was replaced (new credentials clear it\n// before the old socket closes, a reconnect brings another) is dropped:\n// its data and `onJoined` are another user's or another socket's.\n// - It never leaves a room: unmounting or disabling it stops the re-joins\n// only. Leave with a call of its own, or let the server's `onRoomLeave`\n// follow the socket's disconnect.\n//\n// It calls through `member.call`, so it works on a mock client's members\n// too, whose provider gives a hello per session.\n\nimport { hashKey } from \"@tanstack/react-query\";\nimport {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type Dispatch,\n type SetStateAction,\n} from \"react\";\nimport { QuickdrawError } from \"../protocol/errors\";\nimport { DEFAULT_BACKOFF_MS, retryAfterOf } from \"./backoff\";\nimport type { QuickdrawConnection } from \"./connection\";\nimport { useConnectionState, useQuickdrawContext } from \"./context\";\n\n/**\n * A member `useJoin` can run: any query or mutation member of the typed\n * client (`qd.game.watchWorld`), or of a mock client.\n */\nexport interface JoinMember<Input, Output> {\n call(input: Input): Promise<Output>;\n}\n\n/** Where a join stands on the current socket. */\nexport type JoinStatus = \"idle\" | \"joining\" | \"joined\" | \"error\";\n\n/** Options of {@link useJoin}. */\nexport interface UseJoinOptions<Output> {\n /**\n * Joins only while true. Turning it false stops the re-joins (it does not\n * leave the room); turning it true again joins at once. Default `true`.\n */\n readonly enabled?: boolean;\n /** Called with what each successful join returned: once per socket. */\n readonly onJoined?: (data: Output) => void;\n}\n\n/** What {@link useJoin} returns. */\nexport interface UseJoinResult<Output> {\n /**\n * The current socket's join: `\"idle\"` while there is no socket to join\n * with (before the first hello, while reconnecting) or `enabled` is false,\n * `\"joining\"` while the call runs, `\"joined\"` once it succeeded, `\"error\"`\n * when it failed (see `error`).\n */\n readonly status: JoinStatus;\n /** `status === \"joined\"`. */\n readonly isJoined: boolean;\n /** What the last successful call returned, on this socket or an earlier one. */\n readonly data: Output | undefined;\n /** Why the call failed on the current socket; `null` otherwise. */\n readonly error: QuickdrawError | null;\n /**\n * Runs the call again now on the current socket: after a refusal the user\n * can act on (the room was full, a grant was missing). Does nothing while\n * there is no socket to join with (before the first hello, while\n * reconnecting) or `enabled` is false, since the next hello joins anyway.\n */\n readonly retry: () => void;\n}\n\n/** The last join: the socket it ran on, and how it went. */\ninterface JoinState<Output> {\n readonly socketId: string | undefined;\n readonly status: Exclude<JoinStatus, \"idle\">;\n readonly data: Output | undefined;\n readonly error: QuickdrawError | null;\n}\n\nfunction failureOf(error: unknown): QuickdrawError {\n return error instanceof QuickdrawError\n ? error\n : new QuickdrawError(\"INTERNAL\", error instanceof Error ? error.message : String(error));\n}\n\n/** What one run of `useJoin`'s effect reads of its latest render. */\ninterface Latest<Input, Output> {\n readonly input: Input;\n readonly onJoined: ((data: Output) => void) | undefined;\n}\n\n/**\n * Joins on every hello of `connection` from now on, with `RATE_LIMITED`\n * tried again after its backoff on the socket it was refused on; `join`\n * joins at once (`retry`), and `stop` ends it all.\n */\nfunction startJoins<Input, Output>(\n connection: QuickdrawConnection,\n member: JoinMember<Input, Output>,\n latest: { readonly current: Latest<Input, Output> },\n setState: Dispatch<SetStateAction<JoinState<Output> | undefined>>,\n): { readonly join: () => void; readonly stop: () => void } {\n let attempt = 0;\n let stopped = false;\n let retry: ReturnType<typeof setTimeout> | undefined;\n const join = (): void => {\n clearTimeout(retry);\n attempt += 1;\n const mine = attempt;\n const socketId = connection.socket.id;\n // New credentials clear the hello at once, before the old socket closes: a reply that\n // arrives then is the last user's join, never this hook's data or onJoined.\n const { hello } = connection.getState();\n const current = (): boolean =>\n !stopped && mine === attempt && connection.getState().hello === hello;\n setState((previous) => ({ socketId, status: \"joining\", data: previous?.data, error: null }));\n member.call(latest.current.input).then(\n (data) => {\n if (current()) {\n setState({ socketId, status: \"joined\", data, error: null });\n latest.current.onJoined?.(data);\n }\n },\n (error: unknown) => {\n if (!current()) {\n return;\n }\n const failure = failureOf(error);\n setState((previous) => ({\n socketId,\n status: \"error\",\n data: previous?.data,\n error: failure,\n }));\n if (failure.code === \"RATE_LIMITED\") {\n retry = setTimeout(\n () => {\n // Only on the socket it was refused on: a new one's hello joins anyway.\n if (current() && connection.socket.id === socketId) {\n join();\n }\n },\n retryAfterOf(failure) ?? DEFAULT_BACKOFF_MS,\n );\n }\n },\n );\n };\n const stopHellos = connection.onHello(join);\n return {\n join,\n stop: () => {\n stopped = true;\n clearTimeout(retry);\n stopHellos();\n },\n };\n}\n\n/**\n * Runs `member.call(input)` on every `qd:hello` of the provider's connection\n * (the first one, every reconnect, new credentials) and when `input`\n * changes by value: the joining call of an app room, which a new socket must\n * make again. Not on re-renders.\n *\n * @example\n * // the socket is in the board's room on every connection, so its events and channel reach it\n * const board = useJoin(qd.task.enterBoard, { projectId });\n * if (board.status === \"error\") return <p>{board.error?.message}</p>;\n */\nexport function useJoin<Input, Output>(\n member: JoinMember<Input, Output>,\n input: Input,\n options: UseJoinOptions<Output> = {},\n): UseJoinResult<Output> {\n const { connection } = useQuickdrawContext(\"useJoin\");\n const connected = useConnectionState(connection).status === \"connected\";\n const enabled = options.enabled !== false;\n const key = hashKey([input]);\n const latest = useRef<Latest<Input, Output>>({ input, onJoined: options.onJoined });\n useEffect(() => {\n latest.current = { input, onJoined: options.onJoined };\n });\n const [state, setState] = useState<JoinState<Output> | undefined>(undefined);\n // The join of the effect running now, for `retry`; none while disabled.\n const running = useRef<(() => void) | undefined>(undefined);\n useEffect(() => {\n if (!enabled) {\n return undefined;\n }\n const joins = startJoins(connection, member, latest, setState);\n running.current = joins.join;\n return () => {\n joins.stop();\n if (running.current === joins.join) {\n running.current = undefined;\n }\n };\n }, [connection, member, enabled, key]);\n const retry = useCallback((): void => {\n const { status: socket, hello } = connection.getState();\n if (socket === \"connected\" && hello !== null) {\n running.current?.();\n }\n }, [connection]);\n const shown =\n enabled && connected && state !== undefined && state.socketId === connection.socket.id\n ? state\n : undefined;\n const status = shown?.status ?? \"idle\";\n const error = shown?.error ?? null;\n const data = state?.data;\n return useMemo(\n () => ({ status, isJoined: status === \"joined\", data, error, retry }),\n [status, data, error, retry],\n );\n}\n","\"use client\";\n\n// A screen that serves every admin service from its metadata (RFC 0003\n// section 12.4): the route names the service's key, `useAdminServices(qd)`\n// lists them, and `adminMeta` names the fields at run time. Over a union of\n// keys, `qd[key].admin` is a union of members typed per entity, whose sort\n// fields and data cannot be given names read from `adminMeta`, so such a\n// screen needed a cast (finding F3.7). `adminOf(qd, key)` gives one shape\n// for every service instead: the same members, typed by field name.\n\nimport type { QueryClient, UseMutationResult, UseQueryResult } from \"@tanstack/react-query\";\nimport type { SortDirection } from \"../contract/collections\";\nimport type { AdminMethodName, AdminServiceMeta } from \"../contract/kits/admin\";\nimport type { AdminPage, AdminSubscribers } from \"../contract/kits/adminSchemas\";\nimport type { FilterValue } from \"../contract/kits/crudList\";\nimport type { QuickdrawError } from \"../protocol/errors\";\nimport { adminNamespaceOf, type AdminKeysOf } from \"./admin\";\nimport type { MutationCallOptions, QueryCallOptions } from \"./clientTypes\";\nimport type { MethodMutationOptions, MethodQueryOptions } from \"./hooks\";\nimport type { MethodQueryKey } from \"./keys\";\n\n/** A row as a metadata-driven admin screen handles it: its fields by name. */\nexport type AdminRow = Readonly<Record<string, unknown>> & { readonly id: string };\n\n/** What a metadata-driven screen passes `adminList`: field names read from `adminMeta`. */\nexport interface AdminListRequest {\n /** The page, from 1. Default 1. */\n readonly page?: number;\n /** The rows per page: default 20, at most 100. */\n readonly pageSize?: number;\n /** Equality per field `adminMeta` marks `filterable`. */\n readonly filter?: Readonly<Record<string, FilterValue>>;\n /** One field `adminMeta` marks `sortable`; the kit's default order without it. */\n readonly sort?: { readonly field: string; readonly direction?: SortDirection };\n}\n\n/** The values an admin write sets, by field name. */\nexport type AdminWriteData = Readonly<Record<string, unknown>>;\n\n/** A query member of an admin screen: the typed client's, typed by field name. */\nexport interface AdminQueryMember<Input, Output> {\n useQuery<Data = Output>(\n input: Input,\n options?: MethodQueryOptions<Output, Data, Input>,\n ): UseQueryResult<Data, QuickdrawError>;\n call(input: Input, options?: QueryCallOptions): Promise<Output>;\n key(input: Input): MethodQueryKey<Input>;\n prefetch(queryClient: QueryClient, input: Input): Promise<void>;\n}\n\n/** A mutation member of an admin screen: the typed client's, typed by field name. */\nexport interface AdminMutationMember<Input, Output> {\n useMutation<Context = unknown>(\n options?: MethodMutationOptions<Output, Input, Context>,\n ): UseMutationResult<Output, QuickdrawError, Input, Context>;\n call(input: Input, options?: MutationCallOptions): Promise<Output>;\n}\n\n/**\n * The admin kit's members of one service as a screen driven by `adminMeta`\n * calls them: rows and inputs by field name, the same for every service.\n * `adminMeta` and `adminList` are always there; a method the contract does\n * not expose (`admin.contract({ expose })`) is absent.\n */\nexport interface AdminScreen {\n readonly adminMeta: AdminQueryMember<undefined, AdminServiceMeta>;\n readonly adminList: AdminQueryMember<AdminListRequest | undefined, AdminPage<AdminRow>>;\n readonly adminGet?: AdminQueryMember<{ readonly id: string }, AdminRow>;\n readonly adminCreate?: AdminMutationMember<{ readonly data: AdminWriteData }, AdminRow>;\n readonly adminUpdate?: AdminMutationMember<\n { readonly id: string; readonly data: AdminWriteData },\n AdminRow\n >;\n readonly adminDelete?: AdminMutationMember<{ readonly id: string }, null>;\n readonly adminSubscribers?: AdminQueryMember<{ readonly id: string }, AdminSubscribers>;\n readonly adminReemit?: AdminMutationMember<{ readonly id: string }, AdminSubscribers>;\n}\n\nconst SCREENS = new WeakMap<object, AdminScreen>();\n\n/**\n * `client[key].admin` as one shape for every service: the same members,\n * keyed by what the kit made them for and typed by field name, for a screen\n * that reads the fields from `adminMeta` at run time. Throws a `TypeError`\n * for a key whose contract has no admin kit, or exposes no `adminMeta` or\n * `adminList`. The same object for the same service every time, so its\n * hooks can be called on every render.\n *\n * @example\n * const admin = adminOf(qd, key); // key: AdminKeysOf<typeof qd>, from the route\n * const { data: meta } = admin.adminMeta.useQuery(undefined);\n * const { data: page } = admin.adminList.useQuery({ page, sort: { field: \"createdAt\", direction: \"desc\" } });\n */\nexport function adminOf<Client extends object>(\n client: Client,\n key: AdminKeysOf<Client>,\n): AdminScreen {\n const namespace = adminNamespaceOf(client, key);\n if (namespace === undefined) {\n throw new TypeError(`adminOf: \"${key}\" is not a service of this client with the admin kit`);\n }\n const known = SCREENS.get(namespace);\n if (known !== undefined) {\n return known;\n }\n const missing = ([\"adminMeta\", \"adminList\"] as const).filter(\n (kind: AdminMethodName) => namespace.byKind[kind] === undefined,\n );\n if (missing.length > 0) {\n throw new TypeError(\n `adminOf: \"${key}\" exposes no ${missing.join(\" or \")}, which an admin screen reads`,\n );\n }\n const screen = Object.freeze({ ...namespace.byKind }) as unknown as AdminScreen;\n SCREENS.set(namespace, screen);\n return screen;\n}\n"],"mappings":"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","names":["useCallback","useCallback"]}
1
+ {"version":3,"sources":["../../src/client/live/usePresence.ts","../../src/client/join.ts","../../src/client/adminScreen.ts"],"sourcesContent":["\"use client\";\n\n// `usePresence(room)` (RFC 0003 section 12.5): the ids of the users in an\n// app room the connection's socket is in, kept current by the server's\n// `qd:presence` frames (`presence.ts`). A method joins the socket to the room\n// (`ctx.rooms.join`); until then, and after it leaves or the socket\n// disconnects, the list is empty.\n\nimport { useCallback, useSyncExternalStore } from \"react\";\nimport { serverStateOf } from \"../context\";\nimport { useLiveData } from \"./liveHooks\";\n\nconst NOBODY: readonly string[] = Object.freeze([]);\n\n/** The users in `room`, as the server last said: the same array until it changes. */\nexport function usePresence(room: string): readonly string[] {\n const { connection, live, awaiting } = useLiveData(\"usePresence\");\n const listen = useCallback(\n (listener: () => void) => live.presence.listen(room, listener),\n [live, room],\n );\n // Awaiting new credentials' hello, it shows nobody the last user's socket saw.\n const read = (): readonly string[] => (awaiting ? NOBODY : live.presence.users(room));\n // A server's socket is in no room: nobody, unless its state has a hello (a mock's session).\n const onServer = (): readonly string[] =>\n serverStateOf(connection).hello === null ? NOBODY : live.presence.users(room);\n return useSyncExternalStore(listen, read, onServer);\n}\n","\"use client\";\n\n// `useJoin(member, input, options)` (RFC 0003 sections 11 and 12.5): runs a\n// joining call (a method whose handler puts the calling socket in an app\n// room with `ctx.rooms.join`, a game's `watchWorld`, a board's `enterBoard`)\n// on every `qd:hello`, not on every render. An app room belongs to one\n// socket: a reconnect (a lost network, a `qd:rotate`, new credentials) is a\n// new socket in no room, which hears none of the room's events and whose\n// channel messages that require the room are dropped, while a query's cached\n// answer still looks fine and nothing reads it again. 4.1 apps re-joined from\n// `isConnected` effects by hand.\n//\n// - Each hello runs the call once more (`connection.onHello`), and so does a\n// new input (compared by value, as query keys are). A failure shows as\n// `error` until the next hello; `RATE_LIMITED` is tried again once its\n// backoff ends.\n// - `status` is the current socket's: `\"idle\"` while there is none to join\n// with (before the first hello, while reconnecting) or `enabled` is false,\n// `\"joining\"` while the call runs, then `\"joined\"` or `\"error\"`. `data` is\n// what the last successful call returned, kept through a reconnect.\n// - `retry()` runs the call again at once on the current socket, for a\n// join that was refused (finding F6.3 of the quickdraw-chat migration: a\n// refused first join could be retried only by toggling `enabled`). It\n// does nothing while there is no socket to join with or `enabled` is\n// false: the next hello joins anyway.\n// - A join answered after its hello was replaced (new credentials clear it\n// before the old socket closes, a reconnect brings another) is dropped:\n// its data and `onJoined` are another user's or another socket's.\n// - It never leaves a room: unmounting or disabling it stops the re-joins\n// only. Leave with a call of its own, or let the server's `onRoomLeave`\n// follow the socket's disconnect.\n//\n// It calls through `member.call`, so it works on a mock client's members\n// too, whose provider gives a hello per session.\n\nimport { hashKey } from \"@tanstack/react-query\";\nimport {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type Dispatch,\n type SetStateAction,\n} from \"react\";\nimport { QuickdrawError } from \"../protocol/errors\";\nimport { DEFAULT_BACKOFF_MS, retryAfterOf } from \"./backoff\";\nimport type { QuickdrawConnection } from \"./connection\";\nimport { useConnectionState, useQuickdrawContext } from \"./context\";\n\n/**\n * A member `useJoin` can run: any query or mutation member of the typed\n * client (`qd.game.watchWorld`), or of a mock client.\n */\nexport interface JoinMember<Input, Output> {\n call(input: Input): Promise<Output>;\n}\n\n/** Where a join stands on the current socket. */\nexport type JoinStatus = \"idle\" | \"joining\" | \"joined\" | \"error\";\n\n/** Options of {@link useJoin}. */\nexport interface UseJoinOptions<Output> {\n /**\n * Joins only while true. Turning it false stops the re-joins (it does not\n * leave the room); turning it true again joins at once. Default `true`.\n */\n readonly enabled?: boolean;\n /** Called with what each successful join returned: once per socket. */\n readonly onJoined?: (data: Output) => void;\n}\n\n/** What {@link useJoin} returns. */\nexport interface UseJoinResult<Output> {\n /**\n * The current socket's join: `\"idle\"` while there is no socket to join\n * with (before the first hello, while reconnecting) or `enabled` is false,\n * `\"joining\"` while the call runs, `\"joined\"` once it succeeded, `\"error\"`\n * when it failed (see `error`).\n */\n readonly status: JoinStatus;\n /** `status === \"joined\"`. */\n readonly isJoined: boolean;\n /** What the last successful call returned, on this socket or an earlier one. */\n readonly data: Output | undefined;\n /** Why the call failed on the current socket; `null` otherwise. */\n readonly error: QuickdrawError | null;\n /**\n * Runs the call again now on the current socket: after a refusal the user\n * can act on (the room was full, a grant was missing). Does nothing while\n * there is no socket to join with (before the first hello, while\n * reconnecting) or `enabled` is false, since the next hello joins anyway.\n */\n readonly retry: () => void;\n}\n\n/** The last join: the socket it ran on, and how it went. */\ninterface JoinState<Output> {\n readonly socketId: string | undefined;\n readonly status: Exclude<JoinStatus, \"idle\">;\n readonly data: Output | undefined;\n readonly error: QuickdrawError | null;\n}\n\nfunction failureOf(error: unknown): QuickdrawError {\n return error instanceof QuickdrawError\n ? error\n : new QuickdrawError(\"INTERNAL\", error instanceof Error ? error.message : String(error));\n}\n\n/** What one run of `useJoin`'s effect reads of its latest render. */\ninterface Latest<Input, Output> {\n readonly input: Input;\n readonly onJoined: ((data: Output) => void) | undefined;\n}\n\n/**\n * Joins on every hello of `connection` from now on, with `RATE_LIMITED`\n * tried again after its backoff on the socket it was refused on; `join`\n * joins at once (`retry`), and `stop` ends it all.\n */\nfunction startJoins<Input, Output>(\n connection: QuickdrawConnection,\n member: JoinMember<Input, Output>,\n latest: { readonly current: Latest<Input, Output> },\n setState: Dispatch<SetStateAction<JoinState<Output> | undefined>>,\n): { readonly join: () => void; readonly stop: () => void } {\n let attempt = 0;\n let stopped = false;\n let retry: ReturnType<typeof setTimeout> | undefined;\n const join = (): void => {\n clearTimeout(retry);\n attempt += 1;\n const mine = attempt;\n const socketId = connection.socket.id;\n // New credentials clear the hello at once, before the old socket closes: a reply that\n // arrives then is the last user's join, never this hook's data or onJoined.\n const { hello } = connection.getState();\n const current = (): boolean =>\n !stopped && mine === attempt && connection.getState().hello === hello;\n setState((previous) => ({ socketId, status: \"joining\", data: previous?.data, error: null }));\n member.call(latest.current.input).then(\n (data) => {\n if (current()) {\n setState({ socketId, status: \"joined\", data, error: null });\n latest.current.onJoined?.(data);\n }\n },\n (error: unknown) => {\n if (!current()) {\n return;\n }\n const failure = failureOf(error);\n setState((previous) => ({\n socketId,\n status: \"error\",\n data: previous?.data,\n error: failure,\n }));\n if (failure.code === \"RATE_LIMITED\") {\n retry = setTimeout(\n () => {\n // Only on the socket it was refused on: a new one's hello joins anyway.\n if (current() && connection.socket.id === socketId) {\n join();\n }\n },\n retryAfterOf(failure) ?? DEFAULT_BACKOFF_MS,\n );\n }\n },\n );\n };\n const stopHellos = connection.onHello(join);\n return {\n join,\n stop: () => {\n stopped = true;\n clearTimeout(retry);\n stopHellos();\n },\n };\n}\n\n/**\n * Runs `member.call(input)` on every `qd:hello` of the provider's connection\n * (the first one, every reconnect, new credentials) and when `input`\n * changes by value: the joining call of an app room, which a new socket must\n * make again. Not on re-renders.\n *\n * @example\n * // the socket is in the board's room on every connection, so its events and channel reach it\n * const board = useJoin(qd.task.enterBoard, { projectId });\n * if (board.status === \"error\") return <p>{board.error?.message}</p>;\n */\nexport function useJoin<Input, Output>(\n member: JoinMember<Input, Output>,\n input: Input,\n options: UseJoinOptions<Output> = {},\n): UseJoinResult<Output> {\n const { connection } = useQuickdrawContext(\"useJoin\");\n const connected = useConnectionState(connection).status === \"connected\";\n const enabled = options.enabled !== false;\n const key = hashKey([input]);\n const latest = useRef<Latest<Input, Output>>({ input, onJoined: options.onJoined });\n useEffect(() => {\n latest.current = { input, onJoined: options.onJoined };\n });\n const [state, setState] = useState<JoinState<Output> | undefined>(undefined);\n // The join of the effect running now, for `retry`; none while disabled.\n const running = useRef<(() => void) | undefined>(undefined);\n useEffect(() => {\n if (!enabled) {\n return undefined;\n }\n const joins = startJoins(connection, member, latest, setState);\n running.current = joins.join;\n return () => {\n joins.stop();\n if (running.current === joins.join) {\n running.current = undefined;\n }\n };\n }, [connection, member, enabled, key]);\n const retry = useCallback((): void => {\n const { status: socket, hello } = connection.getState();\n if (socket === \"connected\" && hello !== null) {\n running.current?.();\n }\n }, [connection]);\n const shown =\n enabled && connected && state !== undefined && state.socketId === connection.socket.id\n ? state\n : undefined;\n const status = shown?.status ?? \"idle\";\n const error = shown?.error ?? null;\n const data = state?.data;\n return useMemo(\n () => ({ status, isJoined: status === \"joined\", data, error, retry }),\n [status, data, error, retry],\n );\n}\n","\"use client\";\n\n// A screen that serves every admin service from its metadata (RFC 0003\n// section 12.4): the route names the service's key, `useAdminServices(qd)`\n// lists them, and `adminMeta` names the fields at run time. Over a union of\n// keys, `qd[key].admin` is a union of members typed per entity, whose sort\n// fields and data cannot be given names read from `adminMeta`, so such a\n// screen needed a cast (finding F3.7). `adminOf(qd, key)` gives one shape\n// for every service instead: the same members, typed by field name.\n\nimport type {\n QueryClient,\n Updater,\n UseMutationResult,\n UseQueryResult,\n} from \"@tanstack/react-query\";\nimport type { SortDirection } from \"../contract/collections\";\nimport type { AdminMethodName, AdminServiceMeta } from \"../contract/kits/admin\";\nimport type { AdminPage, AdminSubscribers } from \"../contract/kits/adminSchemas\";\nimport type { FilterValue } from \"../contract/kits/crudList\";\nimport type { QuickdrawError } from \"../protocol/errors\";\nimport { adminNamespaceOf, type AdminKeysOf } from \"./admin\";\nimport type { MutationCallOptions, QueryCallOptions } from \"./clientTypes\";\nimport type { MethodMutationOptions, MethodQueryOptions } from \"./hooks\";\nimport type { MethodQueryKey } from \"./keys\";\n\n/** A row as a metadata-driven admin screen handles it: its fields by name. */\nexport type AdminRow = Readonly<Record<string, unknown>> & { readonly id: string };\n\n/** What a metadata-driven screen passes `adminList`: field names read from `adminMeta`. */\nexport interface AdminListRequest {\n /** The page, from 1. Default 1. */\n readonly page?: number;\n /** The rows per page: default 20, at most 100. */\n readonly pageSize?: number;\n /** Equality per field `adminMeta` marks `filterable`. */\n readonly filter?: Readonly<Record<string, FilterValue>>;\n /** One field `adminMeta` marks `sortable`; the kit's default order without it. */\n readonly sort?: { readonly field: string; readonly direction?: SortDirection };\n}\n\n/** The values an admin write sets, by field name. */\nexport type AdminWriteData = Readonly<Record<string, unknown>>;\n\n/** A query member of an admin screen: the typed client's, typed by field name. */\nexport interface AdminQueryMember<Input, Output> {\n useQuery<Data = Output>(\n input: Input,\n options?: MethodQueryOptions<Output, Data, Input>,\n ): UseQueryResult<Data, QuickdrawError>;\n call(input: Input, options?: QueryCallOptions): Promise<Output>;\n key(input: Input): MethodQueryKey<Input>;\n prefetch(queryClient: QueryClient, input: Input): Promise<void>;\n /**\n * Writes the cached result for `input`, as the typed client's `setData`\n * does: `updater` is the new result, or a function of the cached one\n * (`undefined` when nothing is cached; returning `undefined` writes\n * nothing), and it returns what the cache then holds. The admin kit's\n * reads are not live, so this is how a screen shows the row a write\n * returned: `admin.adminGet?.setData({ id: row.id }, row)`.\n */\n setData(\n input: Input,\n updater: Updater<Output | undefined, Output | undefined>,\n ): Output | undefined;\n}\n\n/** A mutation member of an admin screen: the typed client's, typed by field name. */\nexport interface AdminMutationMember<Input, Output> {\n useMutation<Context = unknown>(\n options?: MethodMutationOptions<Output, Input, Context>,\n ): UseMutationResult<Output, QuickdrawError, Input, Context>;\n call(input: Input, options?: MutationCallOptions): Promise<Output>;\n}\n\n/**\n * The admin kit's members of one service as a screen driven by `adminMeta`\n * calls them: rows and inputs by field name, the same for every service.\n * `adminMeta` and `adminList` are always there; a method the contract does\n * not expose (`admin.contract({ expose })`) is absent.\n */\nexport interface AdminScreen {\n readonly adminMeta: AdminQueryMember<undefined, AdminServiceMeta>;\n readonly adminList: AdminQueryMember<AdminListRequest | undefined, AdminPage<AdminRow>>;\n readonly adminGet?: AdminQueryMember<{ readonly id: string }, AdminRow>;\n readonly adminCreate?: AdminMutationMember<{ readonly data: AdminWriteData }, AdminRow>;\n readonly adminUpdate?: AdminMutationMember<\n { readonly id: string; readonly data: AdminWriteData },\n AdminRow\n >;\n readonly adminDelete?: AdminMutationMember<{ readonly id: string }, null>;\n readonly adminSubscribers?: AdminQueryMember<{ readonly id: string }, AdminSubscribers>;\n readonly adminReemit?: AdminMutationMember<{ readonly id: string }, AdminSubscribers>;\n}\n\nconst SCREENS = new WeakMap<object, AdminScreen>();\n\n/**\n * `client[key].admin` as one shape for every service: the same members,\n * keyed by what the kit made them for and typed by field name, for a screen\n * that reads the fields from `adminMeta` at run time. Throws a `TypeError`\n * for a key whose contract has no admin kit, or exposes no `adminMeta` or\n * `adminList`. The same object for the same service every time, so its\n * hooks can be called on every render.\n *\n * @example\n * const admin = adminOf(qd, key); // key: AdminKeysOf<typeof qd>, from the route\n * const { data: meta } = admin.adminMeta.useQuery(undefined);\n * const { data: page } = admin.adminList.useQuery({ page, sort: { field: \"createdAt\", direction: \"desc\" } });\n */\nexport function adminOf<Client extends object>(\n client: Client,\n key: AdminKeysOf<Client>,\n): AdminScreen {\n const namespace = adminNamespaceOf(client, key);\n if (namespace === undefined) {\n throw new TypeError(`adminOf: \"${key}\" is not a service of this client with the admin kit`);\n }\n const known = SCREENS.get(namespace);\n if (known !== undefined) {\n return known;\n }\n const missing = ([\"adminMeta\", \"adminList\"] as const).filter(\n (kind: AdminMethodName) => namespace.byKind[kind] === undefined,\n );\n if (missing.length > 0) {\n throw new TypeError(\n `adminOf: \"${key}\" exposes no ${missing.join(\" or \")}, which an admin screen reads`,\n );\n }\n const screen = Object.freeze({ ...namespace.byKind }) as unknown as AdminScreen;\n SCREENS.set(namespace, screen);\n return screen;\n}\n"],"mappings":"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","names":["useCallback","useCallback"]}
@@ -1,9 +1,9 @@
1
1
  import { QueryClient, UseMutationOptions, UseQueryOptions, UseQueryResult, Updater, UseMutationResult, QueryKey } from '@tanstack/react-query';
2
- import { J as AnyContract, az as StreamName, am as IsScopedStream, ay as StreamItemOf, aa as ChannelName, a9 as ChannelInputOf, ag as EventName, ah as EventPayloadOf, ap as MethodName, aq as MethodOf, ar as OutputOf, al as InputOf, ae as ContractMap, ao as KindOf, af as EntityOf, ac as CollectionName, an as ItemOf, aw as ScopeOf, aA as ViewName, ak as IndexRowOf } from './infer-BmoZsL9a.js';
2
+ import { J as AnyContract, az as StreamName, am as IsScopedStream, ay as StreamItemOf, aa as ChannelName, a9 as ChannelInputOf, ag as EventName, ah as EventPayloadOf, ap as MethodName, aq as MethodOf, ar as OutputOf, al as InputOf, ae as ContractMap, ao as KindOf, af as EntityOf, ac as CollectionName, an as ItemOf, aw as ScopeOf, aA as ViewName, ak as IndexRowOf } from './infer-Br62cnyW.js';
3
3
  import { Q as QuickdrawError } from './errors-S-bxEIBC.js';
4
4
  import { M as MethodQueryKey } from './keys-CqKkljVN.js';
5
- import { i as SearchPage } from './adminSchemas-D4bFKsrb.js';
6
- import { a5 as Revision } from './envelope-DzWyIXjj.js';
5
+ import { i as SearchPage } from './adminSchemas-29cGgrg5.js';
6
+ import { as as Revision } from './envelope-BMBaxppV.js';
7
7
 
8
8
  /** A row as additions keep it: any object with a string `id`. */
9
9
  type AddedRow = Readonly<Record<string, unknown>> & {
@@ -1,6 +1,6 @@
1
- import { C as CLIENT_EVENTS, a9 as ServerToClientEvents, E as ClientToServerEvents, B as ChangedFrame, n as ProtocolMismatch, k as HelloFrame, A as AuthenticationRefused } from './envelope-DzWyIXjj.js';
2
- import { k as MethodKind } from './infer-BmoZsL9a.js';
3
- import { ManagerOptions, SocketOptions, Socket } from 'socket.io-client';
1
+ import { C as CLIENT_EVENTS, aw as ServerToClientEvents, I as ClientToServerEvents, F as ChangedFrame, m as HelloFrame, q as ProtocolMismatch, A as AuthenticationRefused, l as AuthenticationUnavailable } from './envelope-BMBaxppV.js';
2
+ import { k as MethodKind } from './infer-Br62cnyW.js';
3
+ import { Socket, ManagerOptions, SocketOptions } from 'socket.io-client';
4
4
 
5
5
  /** The kinds of work that back off separately: method calls by kind, and subscription events. */
6
6
  type BackoffKind = MethodKind | "subscription";
@@ -127,14 +127,20 @@ interface QuickdrawConnectionOptions {
127
127
  * Where a connection is:
128
128
  *
129
129
  * - `idle`: not opened, or closed by the app;
130
- * - `connecting`: opening, or reopening after it was lost (Socket.IO retries);
130
+ * - `connecting`: opening, or reopening after it was lost (Socket.IO
131
+ * retries) or after the server could not authenticate it (`INTERNAL`:
132
+ * the connection retries, with backoff);
131
133
  * - `connected`;
132
134
  * - `disconnected`: the server ended it, and it does not retry;
133
135
  * - `refused`: the server refused the handshake (see `refusal`).
134
136
  */
135
137
  type ConnectionStatus = "idle" | "connecting" | "connected" | "disconnected" | "refused";
136
- /** Why the server refused a handshake: another protocol, failed authentication, or something else. */
137
- type ConnectionRefusal = ProtocolMismatch | AuthenticationRefused | {
138
+ /**
139
+ * Why the server refused a handshake: another protocol, refused
140
+ * credentials, credentials it could not check (`INTERNAL`, only once the
141
+ * connection stopped retrying), or something else.
142
+ */
143
+ type ConnectionRefusal = ProtocolMismatch | AuthenticationRefused | AuthenticationUnavailable | {
138
144
  readonly code: "REFUSED";
139
145
  readonly message: string;
140
146
  };
@@ -143,9 +149,10 @@ interface ConnectionState {
143
149
  readonly status: ConnectionStatus;
144
150
  /**
145
151
  * True while the connection is `connecting` again after it was connected
146
- * with its current credentials: the socket dropped, or the server asked it
147
- * to rotate. Queries keep running meanwhile (their calls wait in the send
148
- * buffer); `setAuth` and `close` end it.
152
+ * with its current credentials: the socket dropped, the server asked it to
153
+ * rotate, or the server could not authenticate a reconnect (`INTERNAL`).
154
+ * Queries keep running meanwhile (their calls wait in the send buffer);
155
+ * `setAuth` and `close` end it.
149
156
  */
150
157
  readonly reconnecting: boolean;
151
158
  /**
@@ -1,11 +1,11 @@
1
1
  import { IncomingMessage, ServerResponse, Server as Server$1 } from 'node:http';
2
- import { a as DispatcherBaseOptions, H as HttpRouter, b as HttpTransportOptions, P as PrincipalOfServices, D as Dispatcher } from './http-DotQ5bj8.js';
3
- import { P as Principal } from './types-CYliTP65.js';
4
- import { r as AppRooms, t as StreamSubscriptions, A as AnyService, u as RoomLeaveHandler } from './caller-BT-Mv4bD.js';
5
- import { S as ServerAuth, a as ServiceGrants } from './auth-DXiSJZaf.js';
2
+ import { a as DispatcherBaseOptions, H as HttpRouter, b as HttpTransportOptions, P as PrincipalOfServices, D as Dispatcher } from './http-YzLAar3w.js';
3
+ import { P as Principal } from './types-CLRCQGnO.js';
4
+ import { q as AppRooms, r as StreamSubscriptions, A as AnyService, t as RoomLeaveHandler, H as HostLeaveHandler } from './caller-BTwGl7vm.js';
5
+ import { S as ServerAuth, a as ServiceGrants } from './auth-DtdYjrZ9.js';
6
6
  import { Server, DefaultEventsMap, Socket, ServerOptions as ServerOptions$1 } from 'socket.io';
7
- import { g as PROTOCOL_VERSION } from './envelope-DzWyIXjj.js';
8
- import { A as AccessLevel } from './infer-BmoZsL9a.js';
7
+ import { g as PROTOCOL_VERSION } from './envelope-BMBaxppV.js';
8
+ import { A as AccessLevel } from './infer-Br62cnyW.js';
9
9
  import { L as Logger } from './logger-Yz93KzdV.js';
10
10
 
11
11
  /** One live collection subscription, as `socket.data.collections[room]` records it. */
@@ -231,6 +231,13 @@ interface RateLimitOptions {
231
231
  * the global limiter — channels enforce their own per-socket token buckets.
232
232
  */
233
233
  excludePrefixes?: string[];
234
+ /**
235
+ * Passes an event uncounted when it returns true, given the event's name
236
+ * and arguments. `createServer`'s socket limiter passes a peer link's bye
237
+ * (`qd:peer` whose `sig` is `{ bye: true }`), which only ends what the
238
+ * server holds for the pair and has its own bucket.
239
+ */
240
+ skip?: (eventName: string, args: readonly unknown[]) => boolean;
234
241
  /**
235
242
  * Custom key generator for grouping requests.
236
243
  * Default uses socket.id.
@@ -275,7 +282,7 @@ interface RateLimiter {
275
282
  /**
276
283
  * Configuration options.
277
284
  */
278
- options: Required<Pick<RateLimitOptions, "windowMs" | "maxRequests" | "excludeEvents" | "excludePrefixes">>;
285
+ options: Required<Pick<RateLimitOptions, "windowMs" | "maxRequests" | "excludeEvents" | "excludePrefixes" | "skip">>;
279
286
  }
280
287
  /**
281
288
  * Create a rate limiter instance.
@@ -444,8 +451,9 @@ interface ServerOnlyOptions<P extends Principal = Principal> {
444
451
  * their own per-socket token buckets), `qd:cancel`, the entity and
445
452
  * collection subscription events (`qd:sub`, `qd:unsub`, `qd:col:sub`,
446
453
  * `qd:col:unsub`, `qd:col:items`), the topic watches (`qd:watch`,
447
- * `qd:unwatch`) and the stream subscriptions (`qd:stream:sub`,
448
- * `qd:stream:unsub`) are never counted; the ones that read run in a
454
+ * `qd:unwatch`), the stream subscriptions (`qd:stream:sub`,
455
+ * `qd:stream:unsub`) and peer link relay messages (`qd:relay`, which keep
456
+ * their own buckets) are never counted; the ones that read run in a
449
457
  * per-socket lane instead (`limits.subscriptions`: 8 at once, 64 waiting,
450
458
  * then `RATE_LIMITED`).
451
459
  */
@@ -511,6 +519,26 @@ interface ServerOnlyOptions<P extends Principal = Principal> {
511
519
  * },
512
520
  */
513
521
  readonly onRoomLeave?: RoomLeaveHandler<P>;
522
+ /**
523
+ * Called once when the host of an app room leaves it (host mode): its own
524
+ * leave, a removal or a disconnect. Every service's own `onHostLeave` runs
525
+ * beside it, as for `onRoomLeave`. The room has no host until a method
526
+ * sets one again.
527
+ */
528
+ readonly onHostLeave?: HostLeaveHandler<P>;
529
+ /**
530
+ * Serve peer links (RFC 0003 section 12.5): two sockets of one app room
531
+ * open WebRTC data channels between them (`createPeerLink` on the
532
+ * client), the server forwarding their signals (`qd:peer`) and relaying
533
+ * their messages (`qd:relay`) when no direct path forms. It authorizes
534
+ * both by room membership and never sees what a direct link carries, so a
535
+ * service trusts nothing a peer reports. `qd:hello` then lists `"peer"` in
536
+ * `features` and the relay's limits in `limits.peers`. Default `false`:
537
+ * clients open no links, and the server forwards nothing between them.
538
+ * Host mode (a contract's `serve: "host"` channels and streams) runs over
539
+ * peer links: a server whose services declare any needs `peers: true`.
540
+ */
541
+ readonly peers?: boolean;
514
542
  }
515
543
  /**
516
544
  * Options of {@link createServer}: the dispatcher's (`services`, `db`,
@@ -1,9 +1,9 @@
1
- import { f as ServerEventName, a9 as ServerToClientEvents, a1 as EventFrame, S as SERVER_EVENTS, aa as StreamFrame, k as HelloFrame } from './envelope-DzWyIXjj.js';
2
- import { A as AnyService, d as Caller } from './caller-BT-Mv4bD.js';
3
- import { S as ServerOptions, Q as QuickdrawServer } from './createServer-Cp-Wk500.js';
4
- import { P as PrincipalOfServices, C as ContractOfServices } from './http-DotQ5bj8.js';
5
- import { J as AnyContract, ag as EventName, ah as EventPayloadOf, az as StreamName, ay as StreamItemOf } from './infer-BmoZsL9a.js';
6
- import { C as CallRecord } from './metrics-DW80Exve.js';
1
+ import { f as ServerEventName, aw as ServerToClientEvents, a4 as EventFrame, S as SERVER_EVENTS, ax as StreamFrame, m as HelloFrame } from './envelope-BMBaxppV.js';
2
+ import { A as AnyService, d as Caller } from './caller-BTwGl7vm.js';
3
+ import { S as ServerOptions, Q as QuickdrawServer } from './createServer-Bx5HOcYK.js';
4
+ import { P as PrincipalOfServices, M as McpContextOfServices, C as ContractOfServices } from './http-YzLAar3w.js';
5
+ import { J as AnyContract, ag as EventName, ah as EventPayloadOf, az as StreamName, ay as StreamItemOf } from './infer-Br62cnyW.js';
6
+ import { C as CallRecord } from './metrics-CTVpw0gu.js';
7
7
  import { Socket } from 'socket.io-client';
8
8
 
9
9
  /** The payload of a v5 server event, by its name. */
@@ -123,6 +123,30 @@ declare function waitForEvent<T = unknown>(socket: Socket, event: string, timeou
123
123
  * prints warnings and errors only.
124
124
  */
125
125
  type TestAppOptions<S extends readonly AnyService[]> = ServerOptions<S> & Pick<InstrumentedOptions, "strictWarnings">;
126
+ /**
127
+ * How `app.as(principal, options)` calls: as which transport, and with which
128
+ * `ctx.mcp`, so a guard that reads either is tested in process. `M` is the
129
+ * type of `ctx.mcp`: the app's `QuickdrawTypes["mcp"]`, or `McpContext` when
130
+ * it declares none.
131
+ */
132
+ interface TestCallerOptions<M> {
133
+ /**
134
+ * The transport the calls arrive as, which handlers read as
135
+ * `ctx.transport`: `"internal"` (the default), `"http"` or `"mcp"`. The
136
+ * calls have no connection, so no concurrency lane caps them. A socket's
137
+ * call is `app.connect`'s, over a real socket.
138
+ */
139
+ readonly transport?: "internal" | "http" | "mcp";
140
+ /**
141
+ * The calls' `ctx.mcp`: the fields the MCP bridge's `context` hook would
142
+ * give them, typed by the app's `QuickdrawTypes["mcp"]`. With
143
+ * `transport: "mcp"` they are an MCP tool call's; with the default
144
+ * transport, the calls an MCP call makes through `ctx.services`. Bound
145
+ * arguments and the registry's `principal` and `context` hooks belong to
146
+ * the MCP registry: test those through `registry.callMethod`.
147
+ */
148
+ readonly mcp?: M;
149
+ }
126
150
  /** A socket connected to the test app. */
127
151
  interface TestConnection<S extends readonly AnyService[] = readonly AnyService[]> {
128
152
  /** The raw Socket.IO client socket, for frames `call` does not cover. */
@@ -156,9 +180,12 @@ interface TestApp<S extends readonly AnyService[] = readonly AnyService[]> {
156
180
  * carries no `serviceAccess` gets its grants from the server's
157
181
  * `auth.loadServiceAccess` at every call, as an HTTP call's and a
158
182
  * socket's principal do, so in-process and socket calls authorize alike;
159
- * one that carries grants (even `{}`) keeps them.
183
+ * one that carries grants (even `{}`) keeps them. Its calls arrive with
184
+ * transport `"internal"` and no `ctx.mcp`, unless `options` names others:
185
+ * `app.as(agent, { transport: "mcp", mcp: { scopes: [] } })` calls as an
186
+ * MCP tool call would.
160
187
  */
161
- as(principal: PrincipalOfServices<S> | null): Caller<ContractOfServices<S>>;
188
+ as(principal: PrincipalOfServices<S> | null, options?: TestCallerOptions<McpContextOfServices<S>>): Caller<ContractOfServices<S>>;
162
189
  /**
163
190
  * Connects a real v5 socket acting as `principal` (`null` for anonymous);
164
191
  * resolves once the server said hello, rejects when it refused the socket.
@@ -191,4 +218,4 @@ interface TestApp<S extends readonly AnyService[] = readonly AnyService[]> {
191
218
  */
192
219
  declare function createTestApp<const S extends readonly AnyService[]>(options: TestAppOptions<S>): Promise<TestApp<S>>;
193
220
 
194
- export { type EventQuery as E, type FrameMatch as F, type RecordedFrame as R, type ServerFrameOf as S, type TestApp as T, type TestAppOptions as a, type TestConnection as b, createTestApp as c, type FrameQuery as d, eventFrames as e, type FrameRecorder as f, emitWithAck as g, streamFrames as s, waitForEvent as w };
221
+ export { type EventQuery as E, type FrameMatch as F, type RecordedFrame as R, type ServerFrameOf as S, type TestApp as T, type TestAppOptions as a, type TestCallerOptions as b, createTestApp as c, type TestConnection as d, eventFrames as e, type FrameQuery as f, type FrameRecorder as g, emitWithAck as h, streamFrames as s, waitForEvent as w };