@fitzzero/quickdraw-core 5.1.1 → 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.
- package/README.md +1308 -219
- package/dist/{admin-DKwkIh5i.d.ts → admin-CS7JB_UH.d.ts} +2 -2
- package/dist/{adminSchemas-2DUA_hZ8.d.ts → adminSchemas-29cGgrg5.d.ts} +6 -4
- package/dist/{auth-BhWqdyb-.d.ts → auth-DtdYjrZ9.d.ts} +51 -21
- package/dist/{caller-CXTFswBO.d.ts → caller-BTwGl7vm.d.ts} +296 -33
- package/dist/chunk-2FDSKLBO.js +82 -0
- package/dist/chunk-2FDSKLBO.js.map +1 -0
- package/dist/{chunk-RT6OILOI.js → chunk-4D2DOGAW.js} +2 -14
- package/dist/chunk-4D2DOGAW.js.map +1 -0
- package/dist/{chunk-OECZPRPW.js → chunk-64I6MYBC.js} +162 -775
- package/dist/chunk-64I6MYBC.js.map +1 -0
- package/dist/chunk-7473HK24.js +17 -0
- package/dist/chunk-7473HK24.js.map +1 -0
- package/dist/chunk-7Y5ROPXL.js +33 -0
- package/dist/chunk-7Y5ROPXL.js.map +1 -0
- package/dist/chunk-A2RBQD66.js +7 -0
- package/dist/chunk-A2RBQD66.js.map +1 -0
- package/dist/chunk-AK23XSWD.js +16 -0
- package/dist/chunk-AK23XSWD.js.map +1 -0
- package/dist/chunk-ASAMUGHG.js +651 -0
- package/dist/chunk-ASAMUGHG.js.map +1 -0
- package/dist/{chunk-B2P7AZ25.js → chunk-CYE4UBOB.js} +6 -4
- package/dist/chunk-CYE4UBOB.js.map +1 -0
- package/dist/chunk-DKE6QO26.js +136 -0
- package/dist/chunk-DKE6QO26.js.map +1 -0
- package/dist/chunk-DNP65TD5.js +832 -0
- package/dist/chunk-DNP65TD5.js.map +1 -0
- package/dist/{chunk-H23KEFBA.js → chunk-DUBU3BB3.js} +30 -1
- package/dist/chunk-DUBU3BB3.js.map +1 -0
- package/dist/chunk-GYLWEJHW.js +19 -0
- package/dist/chunk-GYLWEJHW.js.map +1 -0
- package/dist/{chunk-SHEGLZEP.js → chunk-I3JCFS7H.js} +34 -13
- package/dist/chunk-I3JCFS7H.js.map +1 -0
- package/dist/{chunk-X5CWPX7H.js → chunk-IIQ3WMJK.js} +13 -2
- package/dist/chunk-IIQ3WMJK.js.map +1 -0
- package/dist/{chunk-RZTP4IEW.js → chunk-JVTONQFI.js} +6339 -4226
- package/dist/chunk-JVTONQFI.js.map +1 -0
- package/dist/{chunk-QEKVYG5F.js → chunk-KKQANRLG.js} +2 -2
- package/dist/chunk-KKQANRLG.js.map +1 -0
- package/dist/{chunk-CDKG4TFA.js → chunk-MPQAT55F.js} +38 -205
- package/dist/chunk-MPQAT55F.js.map +1 -0
- package/dist/chunk-NO4M6PO5.js +101 -0
- package/dist/chunk-NO4M6PO5.js.map +1 -0
- package/dist/{chunk-TBMKJ26R.js → chunk-NW6U45HX.js} +3 -1
- package/dist/chunk-NW6U45HX.js.map +1 -0
- package/dist/chunk-NX2V6SIL.js +202 -0
- package/dist/chunk-NX2V6SIL.js.map +1 -0
- package/dist/{chunk-ZIVPHYRG.js → chunk-NZHA4TZD.js} +72 -175
- package/dist/chunk-NZHA4TZD.js.map +1 -0
- package/dist/{chunk-2PWMUVC7.js → chunk-OCZJ4EYV.js} +518 -27
- package/dist/chunk-OCZJ4EYV.js.map +1 -0
- package/dist/{chunk-REX5YPHX.js → chunk-PHCURHC7.js} +76 -21
- package/dist/chunk-PHCURHC7.js.map +1 -0
- package/dist/{chunk-4YO5H5ON.js → chunk-Q4OGQM5I.js} +4 -10
- package/dist/chunk-Q4OGQM5I.js.map +1 -0
- package/dist/chunk-QSU47XEX.js +13 -0
- package/dist/chunk-QSU47XEX.js.map +1 -0
- package/dist/{chunk-DD4SKCNO.js → chunk-RZXJTDH5.js} +13 -2
- package/dist/chunk-RZXJTDH5.js.map +1 -0
- package/dist/{chunk-Z2LZ7IZ7.js → chunk-SN3OZVEQ.js} +5 -31
- package/dist/chunk-SN3OZVEQ.js.map +1 -0
- package/dist/{chunk-4DIQHASK.js → chunk-SQ5VUG77.js} +28 -8
- package/dist/chunk-SQ5VUG77.js.map +1 -0
- package/dist/chunk-STH5DLQP.js +1192 -0
- package/dist/chunk-STH5DLQP.js.map +1 -0
- package/dist/{chunk-QCHKWIRS.js → chunk-VZFJTKIW.js} +13 -13
- package/dist/{chunk-QCHKWIRS.js.map → chunk-VZFJTKIW.js.map} +1 -1
- package/dist/{chunk-VSQBHPWH.js → chunk-X4ZU4QH5.js} +54 -22
- package/dist/chunk-X4ZU4QH5.js.map +1 -0
- package/dist/chunk-XD7SRDUY.js +188 -0
- package/dist/chunk-XD7SRDUY.js.map +1 -0
- package/dist/chunk-XH2IZVLT.js +62 -0
- package/dist/chunk-XH2IZVLT.js.map +1 -0
- package/dist/chunk-Z7I6SONO.js +825 -0
- package/dist/chunk-Z7I6SONO.js.map +1 -0
- package/dist/cli/quickdraw-docs.js +132 -112
- package/dist/cli/quickdraw-docs.js.map +1 -1
- package/dist/client/core.d.ts +387 -0
- package/dist/client/core.js +109 -0
- package/dist/client/index.d.ts +52 -203
- package/dist/client/index.js +59 -218
- package/dist/client/index.js.map +1 -1
- package/dist/{clientTypes-Bc-G8n-U.d.ts → clientTypes-CUUNJwWs.d.ts} +3 -3
- package/dist/{connection-Cr1BU07W.d.ts → connection-2EeQ_Et0.d.ts} +22 -11
- package/dist/{createServer-CP9eDHZZ.d.ts → createServer-Bx5HOcYK.d.ts} +67 -17
- package/dist/{createTestApp-UKVGEmOf.d.ts → createTestApp-DAMANT79.d.ts} +46 -13
- package/dist/{envelope-Cp3YcVxC.d.ts → envelope-BMBaxppV.d.ts} +216 -10
- package/dist/{http-Djj2Inuu.d.ts → http-YzLAar3w.d.ts} +76 -60
- package/dist/index.d.ts +8 -7
- package/dist/index.js +82 -25
- package/dist/index.js.map +1 -1
- package/dist/{infer-Teo1xpsd.d.ts → infer-Br62cnyW.d.ts} +82 -11
- package/dist/logger-Yz93KzdV.d.ts +30 -0
- package/dist/{metrics-BdBu5o8G.d.ts → metrics-CTVpw0gu.d.ts} +2 -2
- package/dist/origins-kvpxnVcW.d.ts +8 -0
- package/dist/parser.js +2 -1
- package/dist/prisma/index.d.ts +14 -7
- package/dist/prisma/index.js +279 -70
- package/dist/prisma/index.js.map +1 -1
- package/dist/{rateLimit-ZMIZJUZF.js → rateLimit-5RKBFLUR.js} +2 -2
- package/dist/rateLimit-5RKBFLUR.js.map +1 -0
- package/dist/remote-N7OUAGOW.js +15 -0
- package/dist/remote-N7OUAGOW.js.map +1 -0
- package/dist/server/auth/index.d.ts +82 -33
- package/dist/server/auth/index.js +277 -270
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/express/index.d.ts +9 -2
- package/dist/server/express/index.js +1 -1
- package/dist/server/index.d.ts +268 -84
- package/dist/server/index.js +664 -418
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/index.d.ts +89 -27
- package/dist/server/mcp/index.js +281 -62
- package/dist/server/mcp/index.js.map +1 -1
- package/dist/server/otel.d.ts +3 -3
- package/dist/{sharing-DVa8W-yR.d.ts → sharing-BrsmlDv_.d.ts} +3 -3
- package/dist/testing/client.d.ts +14 -14
- package/dist/testing/client.js +19 -10
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts +50 -13
- package/dist/testing/index.js +113 -21
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock.d.ts +5 -5
- package/dist/testing/mock.js +17 -9
- package/dist/tracked-BR5b2O-U.d.ts +43 -0
- package/dist/{types-Cu0vmx7c.d.ts → types-CLRCQGnO.d.ts} +4 -3
- package/dist/types-tMagHbjy.d.ts +151 -0
- package/dist/utils/index.d.ts +16 -4
- package/dist/utils/index.js +6 -3
- package/package.json +5 -1
- package/dist/chunk-2PWMUVC7.js.map +0 -1
- package/dist/chunk-4DIQHASK.js.map +0 -1
- package/dist/chunk-4YO5H5ON.js.map +0 -1
- package/dist/chunk-B2P7AZ25.js.map +0 -1
- package/dist/chunk-CDKG4TFA.js.map +0 -1
- package/dist/chunk-DD4SKCNO.js.map +0 -1
- package/dist/chunk-H23KEFBA.js.map +0 -1
- package/dist/chunk-OECZPRPW.js.map +0 -1
- package/dist/chunk-QEKVYG5F.js.map +0 -1
- package/dist/chunk-REX5YPHX.js.map +0 -1
- package/dist/chunk-RT6OILOI.js.map +0 -1
- package/dist/chunk-RZTP4IEW.js.map +0 -1
- package/dist/chunk-SHEGLZEP.js.map +0 -1
- package/dist/chunk-TBMKJ26R.js.map +0 -1
- package/dist/chunk-VSQBHPWH.js.map +0 -1
- package/dist/chunk-X5CWPX7H.js.map +0 -1
- package/dist/chunk-YSWH776Y.js +0 -13
- package/dist/chunk-YSWH776Y.js.map +0 -1
- package/dist/chunk-Z2LZ7IZ7.js.map +0 -1
- package/dist/chunk-ZIVPHYRG.js.map +0 -1
- package/dist/logger-BY28acW-.d.ts +0 -16
- /package/dist/{rateLimit-ZMIZJUZF.js.map → client/core.js.map} +0 -0
package/dist/client/index.js.map
CHANGED
|
@@ -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-
|
|
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-
|
|
6
|
-
import {
|
|
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,
|
|
2
|
-
import { k as MethodKind } from './infer-
|
|
3
|
-
import { ManagerOptions, SocketOptions
|
|
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
|
|
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
|
-
/**
|
|
137
|
-
|
|
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,17 +149,22 @@ 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,
|
|
147
|
-
*
|
|
148
|
-
*
|
|
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
|
+
/**
|
|
159
|
+
* The server's `qd:hello` on the current credentials, or `null` before it
|
|
160
|
+
* arrives, and again once the server ended the session (its disconnect, or
|
|
161
|
+
* a connect or reconnect refused with `UNAUTHENTICATED` or `FORBIDDEN`).
|
|
162
|
+
*/
|
|
152
163
|
readonly hello: HelloFrame | null;
|
|
153
164
|
/**
|
|
154
165
|
* The principal's service grants: from the server's `qd:hello`, then from
|
|
155
166
|
* every `qd:access` push; `null` before the hello on the current
|
|
156
|
-
* credentials arrives.
|
|
167
|
+
* credentials arrives, and once the server ended the session.
|
|
157
168
|
*/
|
|
158
169
|
readonly serviceAccess: HelloFrame["serviceAccess"] | null;
|
|
159
170
|
/** Why the last handshake was refused, while `status` is `refused`. */
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { IncomingMessage, ServerResponse, Server as Server$1 } from 'node:http';
|
|
2
|
-
import { a as
|
|
3
|
-
import { P as Principal } from './types-
|
|
4
|
-
import {
|
|
5
|
-
import { S as ServerAuth, a as ServiceGrants } from './auth-
|
|
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-
|
|
8
|
-
import { A as AccessLevel } from './infer-
|
|
9
|
-
import { L as Logger } from './logger-
|
|
7
|
+
import { g as PROTOCOL_VERSION } from './envelope-BMBaxppV.js';
|
|
8
|
+
import { A as AccessLevel } from './infer-Br62cnyW.js';
|
|
9
|
+
import { L as Logger } from './logger-Yz93KzdV.js';
|
|
10
10
|
|
|
11
11
|
/** One live collection subscription, as `socket.data.collections[room]` records it. */
|
|
12
12
|
interface ScopeSubscription {
|
|
@@ -128,7 +128,13 @@ interface ClusterOptions {
|
|
|
128
128
|
* another adapter, revisions stay per process.
|
|
129
129
|
*/
|
|
130
130
|
readonly client?: ClusterClient | IoRedisLike;
|
|
131
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* The prefix of the cluster's keys (`{keyPrefix}:rev`,
|
|
133
|
+
* `{keyPrefix}:seen:{userId}`), and the deployment: every server-to-server
|
|
134
|
+
* event carries it, and a node ignores another deployment's events. It is
|
|
135
|
+
* `setupRedisAdapter`'s default key too. Each deployment that shares a
|
|
136
|
+
* Valkey needs its own. Default `"quickdraw"`; a non-empty string.
|
|
137
|
+
*/
|
|
132
138
|
readonly keyPrefix?: string;
|
|
133
139
|
/**
|
|
134
140
|
* How long a counter command may take before the node takes its revision
|
|
@@ -225,6 +231,13 @@ interface RateLimitOptions {
|
|
|
225
231
|
* the global limiter — channels enforce their own per-socket token buckets.
|
|
226
232
|
*/
|
|
227
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;
|
|
228
241
|
/**
|
|
229
242
|
* Custom key generator for grouping requests.
|
|
230
243
|
* Default uses socket.id.
|
|
@@ -269,7 +282,7 @@ interface RateLimiter {
|
|
|
269
282
|
/**
|
|
270
283
|
* Configuration options.
|
|
271
284
|
*/
|
|
272
|
-
options: Required<Pick<RateLimitOptions, "windowMs" | "maxRequests" | "excludeEvents" | "excludePrefixes">>;
|
|
285
|
+
options: Required<Pick<RateLimitOptions, "windowMs" | "maxRequests" | "excludeEvents" | "excludePrefixes" | "skip">>;
|
|
273
286
|
}
|
|
274
287
|
/**
|
|
275
288
|
* Create a rate limiter instance.
|
|
@@ -374,8 +387,13 @@ interface DisconnectUserOptions {
|
|
|
374
387
|
readonly reason?: string;
|
|
375
388
|
}
|
|
376
389
|
|
|
377
|
-
/**
|
|
378
|
-
|
|
390
|
+
/**
|
|
391
|
+
* Socket.IO server options `createServer` passes through; it sets `parser`
|
|
392
|
+
* and `cors` itself, and refuses `connectionStateRecovery`: a recovered
|
|
393
|
+
* socket would come back with its principal and rooms without
|
|
394
|
+
* authenticating again.
|
|
395
|
+
*/
|
|
396
|
+
type SocketOptions = Partial<Omit<ServerOptions$1, "parser" | "cors" | "connectionStateRecovery">>;
|
|
379
397
|
/** Socket.IO's CORS setting for the handshake and polling requests. */
|
|
380
398
|
type SocketCors = Partial<ServerOptions$1>["cors"];
|
|
381
399
|
|
|
@@ -406,7 +424,13 @@ interface ServerOnlyOptions<P extends Principal = Principal> {
|
|
|
406
424
|
readonly auth?: ServerAuth<P>;
|
|
407
425
|
/** Socket.IO's CORS for its handshake. The app configures CORS for its own routes. */
|
|
408
426
|
readonly cors?: SocketCors;
|
|
409
|
-
/**
|
|
427
|
+
/**
|
|
428
|
+
* Other Socket.IO server options, such as `path` or `pingInterval`.
|
|
429
|
+
* `connectionStateRecovery` is refused: a recovered socket would come back
|
|
430
|
+
* with its principal and rooms without authenticating again, so a revoked
|
|
431
|
+
* session or a lowered grant would return; quickdraw clients resume by
|
|
432
|
+
* revision on every reconnect.
|
|
433
|
+
*/
|
|
410
434
|
readonly socket?: SocketOptions;
|
|
411
435
|
/**
|
|
412
436
|
* Use the stock Socket.IO parser, so events may carry binary. Default
|
|
@@ -427,8 +451,9 @@ interface ServerOnlyOptions<P extends Principal = Principal> {
|
|
|
427
451
|
* their own per-socket token buckets), `qd:cancel`, the entity and
|
|
428
452
|
* collection subscription events (`qd:sub`, `qd:unsub`, `qd:col:sub`,
|
|
429
453
|
* `qd:col:unsub`, `qd:col:items`), the topic watches (`qd:watch`,
|
|
430
|
-
* `qd:unwatch`)
|
|
431
|
-
* `qd:stream:unsub`)
|
|
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
|
|
432
457
|
* per-socket lane instead (`limits.subscriptions`: 8 at once, 64 waiting,
|
|
433
458
|
* then `RATE_LIMITED`).
|
|
434
459
|
*/
|
|
@@ -459,8 +484,11 @@ interface ServerOnlyOptions<P extends Principal = Principal> {
|
|
|
459
484
|
* Behind a cluster adapter: where the cluster's shared state lives (the
|
|
460
485
|
* revision counter every node's flushes take their revision from, and
|
|
461
486
|
* users' last-seen times). By default the Socket.IO Redis adapter's own
|
|
462
|
-
* Valkey or Redis client, under keys prefixed `quickdraw
|
|
463
|
-
* `
|
|
487
|
+
* Valkey or Redis client, under keys prefixed `quickdraw:`. Its
|
|
488
|
+
* `keyPrefix` also names the deployment every server-to-server event
|
|
489
|
+
* carries (a node ignores another deployment's) and is
|
|
490
|
+
* `setupRedisAdapter`'s default key: each deployment sharing a Valkey
|
|
491
|
+
* needs its own. See `docs/deploying.md`.
|
|
464
492
|
*/
|
|
465
493
|
readonly cluster?: ClusterOptions;
|
|
466
494
|
/**
|
|
@@ -491,12 +519,34 @@ interface ServerOnlyOptions<P extends Principal = Principal> {
|
|
|
491
519
|
* },
|
|
492
520
|
*/
|
|
493
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;
|
|
494
542
|
}
|
|
495
543
|
/**
|
|
496
544
|
* Options of {@link createServer}: the dispatcher's (`services`, `db`,
|
|
497
545
|
* `logger`, `limits`, `onCall` and the pipeline seams) and the server's own.
|
|
546
|
+
* The grants column is `auth.serviceAccessSource` here, which the server
|
|
547
|
+
* gives its dispatcher.
|
|
498
548
|
*/
|
|
499
|
-
type ServerOptions<S extends readonly AnyService[]> =
|
|
549
|
+
type ServerOptions<S extends readonly AnyService[]> = DispatcherBaseOptions<S> & ServerOnlyOptions<PrincipalOfServices<S>>;
|
|
500
550
|
/** Options of `server.rotate`. */
|
|
501
551
|
interface RotateOptions {
|
|
502
552
|
/** Each client reconnects at a random moment within this many milliseconds. */
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { f as ServerEventName,
|
|
2
|
-
import { A as AnyService, d as Caller } from './caller-
|
|
3
|
-
import { S as ServerOptions, Q as QuickdrawServer } from './createServer-
|
|
4
|
-
import { P as PrincipalOfServices, C as ContractOfServices } from './http-
|
|
5
|
-
import { J as AnyContract, ag as EventName, ah as EventPayloadOf, az as StreamName, ay as StreamItemOf } from './infer-
|
|
6
|
-
import { C as CallRecord } from './metrics-
|
|
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. */
|
|
@@ -95,8 +95,11 @@ interface InstrumentedOptions {
|
|
|
95
95
|
* Under vitest or jest (`VITEST` or `JEST_WORKER_ID` set), throw every
|
|
96
96
|
* development warning raised in this app's method calls (N+1 statements,
|
|
97
97
|
* unbounded reads, oversized replies, nested writes) as a `DevWarningError`
|
|
98
|
-
* where it is raised, so the test that caused it fails.
|
|
99
|
-
*
|
|
98
|
+
* where it is raised, so the test that caused it fails. A warning raised
|
|
99
|
+
* at start (`tiered-field-in-output`, `resolver-without-reads`,
|
|
100
|
+
* `untracked-access-model`, `channel-without-requires`) fails the app's
|
|
101
|
+
* start. Warnings outside its
|
|
102
|
+
* calls, such as an ambient write while seeding, are logged; `close()`
|
|
100
103
|
* ends it. Default `false`: warnings are logged once.
|
|
101
104
|
*/
|
|
102
105
|
readonly strictWarnings?: boolean;
|
|
@@ -120,6 +123,30 @@ declare function waitForEvent<T = unknown>(socket: Socket, event: string, timeou
|
|
|
120
123
|
* prints warnings and errors only.
|
|
121
124
|
*/
|
|
122
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
|
+
}
|
|
123
150
|
/** A socket connected to the test app. */
|
|
124
151
|
interface TestConnection<S extends readonly AnyService[] = readonly AnyService[]> {
|
|
125
152
|
/** The raw Socket.IO client socket, for frames `call` does not cover. */
|
|
@@ -153,9 +180,12 @@ interface TestApp<S extends readonly AnyService[] = readonly AnyService[]> {
|
|
|
153
180
|
* carries no `serviceAccess` gets its grants from the server's
|
|
154
181
|
* `auth.loadServiceAccess` at every call, as an HTTP call's and a
|
|
155
182
|
* socket's principal do, so in-process and socket calls authorize alike;
|
|
156
|
-
* 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.
|
|
157
187
|
*/
|
|
158
|
-
as(principal: PrincipalOfServices<S> | null): Caller<ContractOfServices<S>>;
|
|
188
|
+
as(principal: PrincipalOfServices<S> | null, options?: TestCallerOptions<McpContextOfServices<S>>): Caller<ContractOfServices<S>>;
|
|
159
189
|
/**
|
|
160
190
|
* Connects a real v5 socket acting as `principal` (`null` for anonymous);
|
|
161
191
|
* resolves once the server said hello, rejects when it refused the socket.
|
|
@@ -172,8 +202,11 @@ interface TestApp<S extends readonly AnyService[] = readonly AnyService[]> {
|
|
|
172
202
|
* 127.0.0.1, for tests that call them in process or over real sockets.
|
|
173
203
|
* `strictWarnings: true` (under vitest or jest) makes every development
|
|
174
204
|
* warning raised in the app's method calls throw where it is raised, so a
|
|
175
|
-
* test that causes one fails
|
|
176
|
-
*
|
|
205
|
+
* test that causes one fails, and one raised at start
|
|
206
|
+
* (`tiered-field-in-output`, `resolver-without-reads`,
|
|
207
|
+
* `untracked-access-model`, `channel-without-requires`) rejects this;
|
|
208
|
+
* warnings outside its calls (seeding through the tracked client, say) are
|
|
209
|
+
* logged, and nothing is strict after `close()`.
|
|
177
210
|
*
|
|
178
211
|
* @example
|
|
179
212
|
* const app = await createTestApp({ services: [taskService], db: testPrisma });
|
|
@@ -185,4 +218,4 @@ interface TestApp<S extends readonly AnyService[] = readonly AnyService[]> {
|
|
|
185
218
|
*/
|
|
186
219
|
declare function createTestApp<const S extends readonly AnyService[]>(options: TestAppOptions<S>): Promise<TestApp<S>>;
|
|
187
220
|
|
|
188
|
-
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
|
|
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 };
|