@fitzzero/quickdraw-core 5.1.1 → 5.1.2
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 +533 -180
- package/dist/{admin-DKwkIh5i.d.ts → admin-DWWrT1h6.d.ts} +2 -2
- package/dist/{adminSchemas-2DUA_hZ8.d.ts → adminSchemas-D4bFKsrb.d.ts} +6 -4
- package/dist/{auth-BhWqdyb-.d.ts → auth-DXiSJZaf.d.ts} +42 -16
- package/dist/{caller-CXTFswBO.d.ts → caller-BT-Mv4bD.d.ts} +117 -22
- package/dist/{chunk-RT6OILOI.js → chunk-4D2DOGAW.js} +2 -14
- package/dist/chunk-4D2DOGAW.js.map +1 -0
- package/dist/chunk-5CGO426C.js +15 -0
- package/dist/chunk-5CGO426C.js.map +1 -0
- package/dist/{chunk-RZTP4IEW.js → chunk-6K5BY47X.js} +4594 -3481
- package/dist/chunk-6K5BY47X.js.map +1 -0
- package/dist/{chunk-VSQBHPWH.js → chunk-753VJIUP.js} +13 -15
- package/dist/chunk-753VJIUP.js.map +1 -0
- package/dist/{chunk-ZIVPHYRG.js → chunk-7QCUX4TK.js} +4 -4
- package/dist/chunk-7QCUX4TK.js.map +1 -0
- package/dist/chunk-AK23XSWD.js +16 -0
- package/dist/chunk-AK23XSWD.js.map +1 -0
- package/dist/{chunk-SHEGLZEP.js → chunk-AONAFOYU.js} +14 -5
- package/dist/{chunk-SHEGLZEP.js.map → chunk-AONAFOYU.js.map} +1 -1
- package/dist/chunk-DKE6QO26.js +136 -0
- package/dist/chunk-DKE6QO26.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-CDKG4TFA.js → chunk-GKH3LJ7N.js} +1 -1
- package/dist/chunk-GKH3LJ7N.js.map +1 -0
- package/dist/{chunk-OECZPRPW.js → chunk-GSIPUNJL.js} +49 -9
- package/dist/chunk-GSIPUNJL.js.map +1 -0
- package/dist/{chunk-DD4SKCNO.js → chunk-LCC7HNFP.js} +12 -1
- package/dist/chunk-LCC7HNFP.js.map +1 -0
- package/dist/{chunk-2PWMUVC7.js → chunk-MBH6IDSM.js} +480 -12
- package/dist/chunk-MBH6IDSM.js.map +1 -0
- package/dist/{chunk-QCHKWIRS.js → chunk-NMXYENH4.js} +10 -10
- package/dist/{chunk-QCHKWIRS.js.map → chunk-NMXYENH4.js.map} +1 -1
- 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-REX5YPHX.js → chunk-RJ5I2VO3.js} +74 -19
- package/dist/chunk-RJ5I2VO3.js.map +1 -0
- package/dist/{chunk-QEKVYG5F.js → chunk-RL4VKLBX.js} +1 -1
- package/dist/{chunk-QEKVYG5F.js.map → chunk-RL4VKLBX.js.map} +1 -1
- package/dist/chunk-XH2IZVLT.js +62 -0
- package/dist/chunk-XH2IZVLT.js.map +1 -0
- package/dist/{chunk-4DIQHASK.js → chunk-Z5ILULF2.js} +26 -7
- package/dist/chunk-Z5ILULF2.js.map +1 -0
- package/dist/cli/quickdraw-docs.js +5 -3
- package/dist/cli/quickdraw-docs.js.map +1 -1
- package/dist/client/index.d.ts +30 -18
- package/dist/client/index.js +8 -6
- package/dist/client/index.js.map +1 -1
- package/dist/{clientTypes-Bc-G8n-U.d.ts → clientTypes-hWcPUVS9.d.ts} +3 -3
- package/dist/{connection-Cr1BU07W.d.ts → connection-CgijdGpT.d.ts} +8 -4
- package/dist/{createServer-CP9eDHZZ.d.ts → createServer-Cp-Wk500.d.ts} +36 -14
- package/dist/{createTestApp-UKVGEmOf.d.ts → createTestApp-BB_PkqQ7.d.ts} +16 -10
- package/dist/{envelope-Cp3YcVxC.d.ts → envelope-DzWyIXjj.d.ts} +18 -4
- package/dist/{http-Djj2Inuu.d.ts → http-DotQ5bj8.d.ts} +68 -17
- package/dist/index.d.ts +7 -7
- package/dist/index.js +37 -13
- package/dist/index.js.map +1 -1
- package/dist/{infer-Teo1xpsd.d.ts → infer-BmoZsL9a.d.ts} +43 -10
- package/dist/logger-Yz93KzdV.d.ts +30 -0
- package/dist/{metrics-BdBu5o8G.d.ts → metrics-DW80Exve.d.ts} +2 -2
- package/dist/origins-kvpxnVcW.d.ts +8 -0
- package/dist/prisma/index.d.ts +5 -5
- package/dist/prisma/index.js +130 -67
- package/dist/prisma/index.js.map +1 -1
- package/dist/{rateLimit-ZMIZJUZF.js → rateLimit-5RKBFLUR.js} +2 -2
- package/dist/server/auth/index.d.ts +82 -33
- package/dist/server/auth/index.js +273 -268
- 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 +147 -67
- package/dist/server/index.js +520 -381
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/index.d.ts +63 -17
- package/dist/server/mcp/index.js +256 -54
- 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-UDLn1049.d.ts} +3 -3
- package/dist/testing/client.d.ts +14 -14
- package/dist/testing/client.js +9 -7
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts +12 -12
- package/dist/testing/index.js +21 -12
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock.d.ts +5 -5
- package/dist/testing/mock.js +8 -6
- package/dist/{types-Cu0vmx7c.d.ts → types-CYliTP65.d.ts} +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-2PWMUVC7.js.map +0 -1
- package/dist/chunk-4DIQHASK.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-REX5YPHX.js.map +0 -1
- package/dist/chunk-RT6OILOI.js.map +0 -1
- package/dist/chunk-RZTP4IEW.js.map +0 -1
- package/dist/chunk-TBMKJ26R.js.map +0 -1
- package/dist/chunk-VSQBHPWH.js.map +0 -1
- package/dist/chunk-YSWH776Y.js +0 -13
- package/dist/chunk-YSWH776Y.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 → rateLimit-5RKBFLUR.js.map} +0 -0
package/dist/client/index.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
export { BreadcrumbItem, JWTPayload, NavItem, ServerCallOptions, ServerCaller, ServerCallerHeaders, ServerCallerOptions, ServerMethod, ServerMutation, ServerQuery, buildBreadcrumbs, createServerCaller, findNavItemByHref, findParentNavItem, formatCurrency, formatDate, formatDateTime, formatNumber, formatPercent, parseJWTPayload, routeRequiresAuth, truncate } from '../utils/index.js';
|
|
2
2
|
import { C as CollectionQueryKey, M as MethodQueryKey } from '../keys-CqKkljVN.js';
|
|
3
3
|
export { E as EntityQueryKey, K as KEY_ROOT, b as MethodKeyPrefix, S as ServiceKeyPrefix, c as collectionKey, e as entityKey, m as methodKey, a as methodKeyPrefix, s as serviceKeyPrefix } from '../keys-CqKkljVN.js';
|
|
4
|
-
import { Q as QuickdrawConnection, a as QuickdrawConnectionOptions, C as ConnectionAuth, b as ConnectionStatus, c as ConnectionRefusal } from '../connection-
|
|
5
|
-
export { B as BackoffKind, g as BackoffWindows, e as ConnectionState, D as DEFAULT_BACKOFF_MS, h as DEFAULT_SUBSCRIPTION_LANE, J as JoinWait, L as LaneCallback, f as QuickdrawSocket, S as SocketClientOptions, i as SubscriptionEvent, j as SubscriptionLane, T as TopicWatch, d as createQuickdrawConnection } from '../connection-
|
|
6
|
-
import { k as MethodKind, O as OrderBy, C as CollectionDef, ae as ContractMap, A as AccessLevel, z as SortDirection } from '../infer-
|
|
7
|
-
import { ad as Version, y as CallSuccess, w as CallNotModified, n as ProtocolMismatch, a5 as Revision, F as CollectionDelta, G as CollectionFrame, a7 as RevokeReason, k as HelloFrame } from '../envelope-
|
|
4
|
+
import { Q as QuickdrawConnection, a as QuickdrawConnectionOptions, C as ConnectionAuth, b as ConnectionStatus, c as ConnectionRefusal } from '../connection-CgijdGpT.js';
|
|
5
|
+
export { B as BackoffKind, g as BackoffWindows, e as ConnectionState, D as DEFAULT_BACKOFF_MS, h as DEFAULT_SUBSCRIPTION_LANE, J as JoinWait, L as LaneCallback, f as QuickdrawSocket, S as SocketClientOptions, i as SubscriptionEvent, j as SubscriptionLane, T as TopicWatch, d as createQuickdrawConnection } from '../connection-CgijdGpT.js';
|
|
6
|
+
import { k as MethodKind, O as OrderBy, C as CollectionDef, ae as ContractMap, A as AccessLevel, z as SortDirection } from '../infer-BmoZsL9a.js';
|
|
7
|
+
import { ad as Version, y as CallSuccess, w as CallNotModified, n as ProtocolMismatch, a5 as Revision, F as CollectionDelta, G as CollectionFrame, a7 as RevokeReason, k as HelloFrame } from '../envelope-DzWyIXjj.js';
|
|
8
8
|
import { QueryClient, QueryKey, Query, UseQueryResult, UseMutationResult } from '@tanstack/react-query';
|
|
9
|
-
import { Q as QuickdrawClient, M as MethodQueryOptions, a as QueryCallOptions, b as MethodMutationOptions, c as MutationCallOptions } from '../clientTypes-
|
|
10
|
-
export { A as AddItemOptions, h as AdminMembers, u as ChannelMember, C as CollectionMember, E as EntityMembers, v as EventMember, G as GlobalStreamMember, L as LiveMembers, i as MethodMember, j as MutationMember, k as MutationVariables, O as OnRefused, d as OptimisticCache, l as OptimisticCacheOf, e as OptimisticUpdate, f as OverlayStore, g as OverlayView, m as QueryMember, n as QuickdrawInvalidate, w as RealtimeMembers, R as RefusedAddition, p as RefusedItem, x as ScopedStreamMember, H as SearchMember, I as SearchMemberOf, S as ServiceClient, y as StreamMember, z as UseChannelResult, U as UseCollectionOptions, q as UseCollectionResult, r as UseEntitiesResult, s as UseEntityOptions, t as UseEntityResult, B as UseEventOptions, J as UseSearchOptions, K as UseSearchResult, D as UseStreamOptions, F as UseStreamResult, o as overlaysOf } from '../clientTypes-
|
|
9
|
+
import { Q as QuickdrawClient, M as MethodQueryOptions, a as QueryCallOptions, b as MethodMutationOptions, c as MutationCallOptions } from '../clientTypes-hWcPUVS9.js';
|
|
10
|
+
export { A as AddItemOptions, h as AdminMembers, u as ChannelMember, C as CollectionMember, E as EntityMembers, v as EventMember, G as GlobalStreamMember, L as LiveMembers, i as MethodMember, j as MutationMember, k as MutationVariables, O as OnRefused, d as OptimisticCache, l as OptimisticCacheOf, e as OptimisticUpdate, f as OverlayStore, g as OverlayView, m as QueryMember, n as QuickdrawInvalidate, w as RealtimeMembers, R as RefusedAddition, p as RefusedItem, x as ScopedStreamMember, H as SearchMember, I as SearchMemberOf, S as ServiceClient, y as StreamMember, z as UseChannelResult, U as UseCollectionOptions, q as UseCollectionResult, r as UseEntitiesResult, s as UseEntityOptions, t as UseEntityResult, B as UseEventOptions, J as UseSearchOptions, K as UseSearchResult, D as UseStreamOptions, F as UseStreamResult, o as overlaysOf } from '../clientTypes-hWcPUVS9.js';
|
|
11
11
|
import { Q as QuickdrawError } from '../errors-S-bxEIBC.js';
|
|
12
|
-
import { n as AdminServiceMeta, F as FilterValue, y as AdminPage, z as AdminSubscribers } from '../adminSchemas-
|
|
12
|
+
import { n as AdminServiceMeta, F as FilterValue, y as AdminPage, z as AdminSubscribers } from '../adminSchemas-D4bFKsrb.js';
|
|
13
13
|
import * as React from 'react';
|
|
14
14
|
import 'socket.io-client';
|
|
15
15
|
|
|
@@ -270,9 +270,12 @@ interface InvalidationCoordinator {
|
|
|
270
270
|
*/
|
|
271
271
|
declare function createInvalidationCoordinator(queryClient: QueryClient, options?: CoordinatorOptions): InvalidationCoordinator;
|
|
272
272
|
|
|
273
|
-
/** What a new `qd:hello
|
|
273
|
+
/** What a new `qd:hello`, or the end of the session, meant for the cache. */
|
|
274
274
|
interface HelloChange {
|
|
275
|
-
/**
|
|
275
|
+
/**
|
|
276
|
+
* It names another user than the cache was loaded for, or the server ended
|
|
277
|
+
* the session: the cache was emptied.
|
|
278
|
+
*/
|
|
276
279
|
readonly switched: boolean;
|
|
277
280
|
/** No hello was held before it: the connection's first, or the first on new credentials. */
|
|
278
281
|
readonly first: boolean;
|
|
@@ -917,10 +920,16 @@ interface QuickdrawStatus {
|
|
|
917
920
|
readonly isConnected: boolean;
|
|
918
921
|
/**
|
|
919
922
|
* True once the server's hello on the current credentials has arrived:
|
|
920
|
-
* `userId` is then final, `null` meaning anonymous. False before it,
|
|
921
|
-
*
|
|
922
|
-
*
|
|
923
|
-
*
|
|
923
|
+
* `userId` is then final, `null` meaning anonymous. False before it, again
|
|
924
|
+
* from new credentials (`auth` changed) until their hello, and again once
|
|
925
|
+
* the server ended the session or refused a connect for it (`status`
|
|
926
|
+
* `"disconnected"`, or `"refused"` after the handshake's error code
|
|
927
|
+
* `UNAUTHENTICATED` or `FORBIDDEN`): the hello, the grants and everything
|
|
928
|
+
* quickdraw cached are gone. Another protocol (`refusal.code`
|
|
929
|
+
* `"PROTOCOL_MISMATCH"`) keeps them. It stays true while the connection
|
|
930
|
+
* reconnects with the same credentials. Gate on it, not on `isConnected`;
|
|
931
|
+
* while it is false, a stopped `status` tells "signed out" from "not
|
|
932
|
+
* known yet".
|
|
924
933
|
*/
|
|
925
934
|
readonly isKnown: boolean;
|
|
926
935
|
/**
|
|
@@ -933,13 +942,15 @@ interface QuickdrawStatus {
|
|
|
933
942
|
readonly hello: HelloFrame | null;
|
|
934
943
|
/**
|
|
935
944
|
* The user the connection acts for, from the server's hello: `null` while
|
|
936
|
-
* anonymous,
|
|
937
|
-
* (`isKnown`
|
|
945
|
+
* anonymous, before the hello on the current credentials arrives, and
|
|
946
|
+
* once the server ended the session (`isKnown` and `status` tell them
|
|
947
|
+
* apart).
|
|
938
948
|
*/
|
|
939
949
|
readonly userId: string | null;
|
|
940
950
|
/**
|
|
941
951
|
* The user's service grants: from the server's hello, then from each
|
|
942
|
-
* `qd:access` push; `null` before the hello on the current credentials
|
|
952
|
+
* `qd:access` push; `null` before the hello on the current credentials,
|
|
953
|
+
* and once the server ended the session.
|
|
943
954
|
*/
|
|
944
955
|
readonly serviceAccess: Readonly<Record<string, AccessLevel>> | null;
|
|
945
956
|
/** Why the server refused the connection, while `status` is `refused`. */
|
|
@@ -970,8 +981,9 @@ declare function QuickdrawProvider<Contracts extends ContractMap>(props: Quickdr
|
|
|
970
981
|
* `RATE_LIMITED`.
|
|
971
982
|
*
|
|
972
983
|
* @example
|
|
973
|
-
* const { isKnown, userId } = useQuickdraw();
|
|
974
|
-
*
|
|
984
|
+
* const { status, isKnown, userId } = useQuickdraw();
|
|
985
|
+
* // stopped with nobody known: the server ended or refused the session
|
|
986
|
+
* if (!isKnown) return status === "disconnected" || status === "refused" ? <SignIn /> : <Spinner />;
|
|
975
987
|
* return userId === null ? <SignIn /> : <App />;
|
|
976
988
|
*/
|
|
977
989
|
declare function useQuickdraw(): QuickdrawStatus;
|
package/dist/client/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
shouldRetry,
|
|
11
11
|
useLiveData,
|
|
12
12
|
useQuickdraw
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-7QCUX4TK.js";
|
|
14
14
|
import {
|
|
15
15
|
DEFAULT_BACKOFF_MS,
|
|
16
16
|
DEFAULT_INVALIDATION_WINDOW_MS,
|
|
@@ -40,13 +40,15 @@ import {
|
|
|
40
40
|
useAdminServices,
|
|
41
41
|
useConnectionState,
|
|
42
42
|
useQuickdrawContext
|
|
43
|
-
} from "../chunk-
|
|
44
|
-
import "../chunk-
|
|
45
|
-
import "../chunk-
|
|
46
|
-
import "../chunk-
|
|
43
|
+
} from "../chunk-GSIPUNJL.js";
|
|
44
|
+
import "../chunk-GKH3LJ7N.js";
|
|
45
|
+
import "../chunk-753VJIUP.js";
|
|
46
|
+
import "../chunk-5CGO426C.js";
|
|
47
|
+
import "../chunk-4D2DOGAW.js";
|
|
48
|
+
import "../chunk-AK23XSWD.js";
|
|
47
49
|
import "../chunk-47AEXVDM.js";
|
|
48
50
|
import "../chunk-Z2LZ7IZ7.js";
|
|
49
|
-
import "../chunk-
|
|
51
|
+
import "../chunk-DKE6QO26.js";
|
|
50
52
|
import {
|
|
51
53
|
buildBreadcrumbs,
|
|
52
54
|
createServerCaller,
|
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/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,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-BmoZsL9a.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 { a5 as Revision } from './envelope-
|
|
5
|
+
import { i as SearchPage } from './adminSchemas-D4bFKsrb.js';
|
|
6
|
+
import { a5 as Revision } from './envelope-DzWyIXjj.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,5 +1,5 @@
|
|
|
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-
|
|
2
|
-
import { k as MethodKind } from './infer-
|
|
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
3
|
import { ManagerOptions, SocketOptions, Socket } from 'socket.io-client';
|
|
4
4
|
|
|
5
5
|
/** The kinds of work that back off separately: method calls by kind, and subscription events. */
|
|
@@ -148,12 +148,16 @@ interface ConnectionState {
|
|
|
148
148
|
* buffer); `setAuth` and `close` end it.
|
|
149
149
|
*/
|
|
150
150
|
readonly reconnecting: boolean;
|
|
151
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* The server's `qd:hello` on the current credentials, or `null` before it
|
|
153
|
+
* arrives, and again once the server ended the session (its disconnect, or
|
|
154
|
+
* a connect or reconnect refused with `UNAUTHENTICATED` or `FORBIDDEN`).
|
|
155
|
+
*/
|
|
152
156
|
readonly hello: HelloFrame | null;
|
|
153
157
|
/**
|
|
154
158
|
* The principal's service grants: from the server's `qd:hello`, then from
|
|
155
159
|
* every `qd:access` push; `null` before the hello on the current
|
|
156
|
-
* credentials arrives.
|
|
160
|
+
* credentials arrives, and once the server ended the session.
|
|
157
161
|
*/
|
|
158
162
|
readonly serviceAccess: HelloFrame["serviceAccess"] | null;
|
|
159
163
|
/** 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 { r as AppRooms, t as StreamSubscriptions, A as AnyService, u as RoomLeaveHandler } from './caller-
|
|
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-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';
|
|
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-DzWyIXjj.js';
|
|
8
|
+
import { A as AccessLevel } from './infer-BmoZsL9a.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
|
|
@@ -374,8 +380,13 @@ interface DisconnectUserOptions {
|
|
|
374
380
|
readonly reason?: string;
|
|
375
381
|
}
|
|
376
382
|
|
|
377
|
-
/**
|
|
378
|
-
|
|
383
|
+
/**
|
|
384
|
+
* Socket.IO server options `createServer` passes through; it sets `parser`
|
|
385
|
+
* and `cors` itself, and refuses `connectionStateRecovery`: a recovered
|
|
386
|
+
* socket would come back with its principal and rooms without
|
|
387
|
+
* authenticating again.
|
|
388
|
+
*/
|
|
389
|
+
type SocketOptions = Partial<Omit<ServerOptions$1, "parser" | "cors" | "connectionStateRecovery">>;
|
|
379
390
|
/** Socket.IO's CORS setting for the handshake and polling requests. */
|
|
380
391
|
type SocketCors = Partial<ServerOptions$1>["cors"];
|
|
381
392
|
|
|
@@ -406,7 +417,13 @@ interface ServerOnlyOptions<P extends Principal = Principal> {
|
|
|
406
417
|
readonly auth?: ServerAuth<P>;
|
|
407
418
|
/** Socket.IO's CORS for its handshake. The app configures CORS for its own routes. */
|
|
408
419
|
readonly cors?: SocketCors;
|
|
409
|
-
/**
|
|
420
|
+
/**
|
|
421
|
+
* Other Socket.IO server options, such as `path` or `pingInterval`.
|
|
422
|
+
* `connectionStateRecovery` is refused: a recovered socket would come back
|
|
423
|
+
* with its principal and rooms without authenticating again, so a revoked
|
|
424
|
+
* session or a lowered grant would return; quickdraw clients resume by
|
|
425
|
+
* revision on every reconnect.
|
|
426
|
+
*/
|
|
410
427
|
readonly socket?: SocketOptions;
|
|
411
428
|
/**
|
|
412
429
|
* Use the stock Socket.IO parser, so events may carry binary. Default
|
|
@@ -459,8 +476,11 @@ interface ServerOnlyOptions<P extends Principal = Principal> {
|
|
|
459
476
|
* Behind a cluster adapter: where the cluster's shared state lives (the
|
|
460
477
|
* revision counter every node's flushes take their revision from, and
|
|
461
478
|
* users' last-seen times). By default the Socket.IO Redis adapter's own
|
|
462
|
-
* Valkey or Redis client, under keys prefixed `quickdraw
|
|
463
|
-
* `
|
|
479
|
+
* Valkey or Redis client, under keys prefixed `quickdraw:`. Its
|
|
480
|
+
* `keyPrefix` also names the deployment every server-to-server event
|
|
481
|
+
* carries (a node ignores another deployment's) and is
|
|
482
|
+
* `setupRedisAdapter`'s default key: each deployment sharing a Valkey
|
|
483
|
+
* needs its own. See `docs/deploying.md`.
|
|
464
484
|
*/
|
|
465
485
|
readonly cluster?: ClusterOptions;
|
|
466
486
|
/**
|
|
@@ -495,8 +515,10 @@ interface ServerOnlyOptions<P extends Principal = Principal> {
|
|
|
495
515
|
/**
|
|
496
516
|
* Options of {@link createServer}: the dispatcher's (`services`, `db`,
|
|
497
517
|
* `logger`, `limits`, `onCall` and the pipeline seams) and the server's own.
|
|
518
|
+
* The grants column is `auth.serviceAccessSource` here, which the server
|
|
519
|
+
* gives its dispatcher.
|
|
498
520
|
*/
|
|
499
|
-
type ServerOptions<S extends readonly AnyService[]> =
|
|
521
|
+
type ServerOptions<S extends readonly AnyService[]> = DispatcherBaseOptions<S> & ServerOnlyOptions<PrincipalOfServices<S>>;
|
|
500
522
|
/** Options of `server.rotate`. */
|
|
501
523
|
interface RotateOptions {
|
|
502
524
|
/** Each client reconnects at a random moment within this many milliseconds. */
|
|
@@ -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-
|
|
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, 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';
|
|
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;
|
|
@@ -172,8 +175,11 @@ interface TestApp<S extends readonly AnyService[] = readonly AnyService[]> {
|
|
|
172
175
|
* 127.0.0.1, for tests that call them in process or over real sockets.
|
|
173
176
|
* `strictWarnings: true` (under vitest or jest) makes every development
|
|
174
177
|
* warning raised in the app's method calls throw where it is raised, so a
|
|
175
|
-
* test that causes one fails
|
|
176
|
-
*
|
|
178
|
+
* test that causes one fails, and one raised at start
|
|
179
|
+
* (`tiered-field-in-output`, `resolver-without-reads`,
|
|
180
|
+
* `untracked-access-model`, `channel-without-requires`) rejects this;
|
|
181
|
+
* warnings outside its calls (seeding through the tracked client, say) are
|
|
182
|
+
* logged, and nothing is strict after `close()`.
|
|
177
183
|
*
|
|
178
184
|
* @example
|
|
179
185
|
* const app = await createTestApp({ services: [taskService], db: testPrisma });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as AccessLevel } from './infer-
|
|
1
|
+
import { A as AccessLevel } from './infer-BmoZsL9a.js';
|
|
2
2
|
import { W as WireError } from './errors-S-bxEIBC.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -132,15 +132,28 @@ interface AuthenticationRefused {
|
|
|
132
132
|
}
|
|
133
133
|
/** True when a `connect_error`'s `data` says authenticating the connection failed. */
|
|
134
134
|
declare function isAuthenticationRefused(value: unknown): value is AuthenticationRefused;
|
|
135
|
+
/** The most rows one socket may hold entity subscriptions to at once, as `qd:hello` announces it. */
|
|
136
|
+
declare const MAX_ENTITY_IDS_PER_SOCKET = 10000;
|
|
137
|
+
/** The most collection scopes one socket may hold at once, as `qd:hello` announces it. */
|
|
138
|
+
declare const MAX_SCOPES_PER_SOCKET = 1000;
|
|
139
|
+
/** The most change topics one socket may watch at once, as `qd:hello` announces it. */
|
|
140
|
+
declare const MAX_TOPICS_PER_SOCKET = 1000;
|
|
135
141
|
/**
|
|
136
142
|
* Each socket's lane of subscription events (`qd:sub`, `qd:col:sub`,
|
|
137
|
-
* `qd:col:items`, `qd:watch`; RFC 0003 section 8.2),
|
|
143
|
+
* `qd:col:items`, `qd:watch`; RFC 0003 section 8.2), and how much one
|
|
144
|
+
* socket may hold at once, as `qd:hello` announces them.
|
|
138
145
|
*/
|
|
139
146
|
interface HelloSubscriptionLimits {
|
|
140
147
|
/** Subscription events one socket may have running at once. */
|
|
141
148
|
readonly maxInFlight: number;
|
|
142
149
|
/** Subscription events one socket may have waiting; past that one fails with `RATE_LIMITED`. */
|
|
143
150
|
readonly maxQueued: number;
|
|
151
|
+
/** Rows one socket may hold entity subscriptions to at once; one more is `CONFLICT`. Absent from servers up to 5.1.0, which have no such cap. */
|
|
152
|
+
readonly maxEntityIds?: number;
|
|
153
|
+
/** Collection scopes one socket may hold at once; one more is `CONFLICT`. Absent from servers up to 5.1.0, which have no such cap. */
|
|
154
|
+
readonly maxScopes?: number;
|
|
155
|
+
/** Change topics one socket may watch at once; one more is `CONFLICT`. Absent from servers up to 5.1.0, which have no such cap. */
|
|
156
|
+
readonly maxTopics?: number;
|
|
144
157
|
}
|
|
145
158
|
/** The limits a server announces in `qd:hello`, so a client can stay inside them. */
|
|
146
159
|
interface HelloLimits {
|
|
@@ -442,7 +455,8 @@ interface StreamSubscribe {
|
|
|
442
455
|
* seed is the latest items the server kept, or what the service computed
|
|
443
456
|
* for this subscriber (the current state the items change); a computed one
|
|
444
457
|
* the server had to wait for may also reflect items that arrived as frames
|
|
445
|
-
* meanwhile. A
|
|
458
|
+
* meanwhile. A subscribe an unsubscribe overtook answers an empty seed. A
|
|
459
|
+
* client replaces what it held with it.
|
|
446
460
|
*/
|
|
447
461
|
type StreamSubscribeReply<Item = unknown> = {
|
|
448
462
|
readonly ok: true;
|
|
@@ -587,4 +601,4 @@ declare function isCallEnvelope(value: unknown): value is CallEnvelope;
|
|
|
587
601
|
/** True when `value` has the shape of a `qd:cancel` frame. Unknown keys are ignored. */
|
|
588
602
|
declare function isCancel(value: unknown): value is CancelFrame;
|
|
589
603
|
|
|
590
|
-
export { type EntityUnsubscribe as $, type AuthenticationRefused as A, type ChangedFrame as B, CLIENT_EVENTS as C, type ChannelFrame as D, type ClientToServerEvents as E, type CollectionDelta as F, type CollectionFrame as G, type HandshakeAuth as H, type CollectionItemsReply as I, type CollectionItemsRequest as J, type CollectionResumed as K, type CollectionScopeRef as L, type CollectionSnapshot as M, type CollectionSubscribe as N, type CollectionSubscribeReply as O, PROTOCOL_MISMATCH as P, type QdHandshake as Q, RESERVED_ROOM_PREFIXES as R, SERVER_EVENTS as S, type EntityFrame as T, type EntityNotModified as U, type EntityPatch as V, type EntityRemove as W, type EntityResult as X, type EntityRow as Y, type EntitySubscribe as Z, type EntitySubscribeReply as _, SERVICE_TOPIC as a, type EntityUpdate as a0, type EventFrame as a1, type Failure as a2, type Ok as a3, type PresenceFrame as a4, type Revision as a5, type RevokedFrame as a6, type RevokeReason as a7, type RotateFrame as a8, type ServerToClientEvents as a9, type StreamFrame as aa, type StreamSubscribe as ab, type StreamSubscribeReply as ac, type Version as ad, type WatchFrame as ae, type WireIndexRow as af, collectionTopic as b, collectionRoom as c, type ClientEventName as d, entityRoom as e, type ServerEventName as f, PROTOCOL_VERSION as g, isProtocolMismatch as h, isAuthenticationRefused as i, isQdHandshake as j, type HelloFrame as k, type HelloLimits as l, type HelloSubscriptionLimits as m, type ProtocolMismatch as n, isCallEnvelope as o, isCancel as p, type AccessFrame as q, type CallEnvelope as r, streamRoom as s, topicRoom as t, userRoom as u, type CallId as v, type CallNotModified as w, type CallReply as x, type CallSuccess as y, type CancelFrame as z };
|
|
604
|
+
export { type EntityUnsubscribe as $, type AuthenticationRefused as A, type ChangedFrame as B, CLIENT_EVENTS as C, type ChannelFrame as D, type ClientToServerEvents as E, type CollectionDelta as F, type CollectionFrame as G, type HandshakeAuth as H, type CollectionItemsReply as I, type CollectionItemsRequest as J, type CollectionResumed as K, type CollectionScopeRef as L, type CollectionSnapshot as M, type CollectionSubscribe as N, type CollectionSubscribeReply as O, PROTOCOL_MISMATCH as P, type QdHandshake as Q, RESERVED_ROOM_PREFIXES as R, SERVER_EVENTS as S, type EntityFrame as T, type EntityNotModified as U, type EntityPatch as V, type EntityRemove as W, type EntityResult as X, type EntityRow as Y, type EntitySubscribe as Z, type EntitySubscribeReply as _, SERVICE_TOPIC as a, type EntityUpdate as a0, type EventFrame as a1, type Failure as a2, type Ok as a3, type PresenceFrame as a4, type Revision as a5, type RevokedFrame as a6, type RevokeReason as a7, type RotateFrame as a8, type ServerToClientEvents as a9, type StreamFrame as aa, type StreamSubscribe as ab, type StreamSubscribeReply as ac, type Version as ad, type WatchFrame as ae, type WireIndexRow as af, MAX_ENTITY_IDS_PER_SOCKET as ag, MAX_SCOPES_PER_SOCKET as ah, MAX_TOPICS_PER_SOCKET as ai, collectionTopic as b, collectionRoom as c, type ClientEventName as d, entityRoom as e, type ServerEventName as f, PROTOCOL_VERSION as g, isProtocolMismatch as h, isAuthenticationRefused as i, isQdHandshake as j, type HelloFrame as k, type HelloLimits as l, type HelloSubscriptionLimits as m, type ProtocolMismatch as n, isCallEnvelope as o, isCancel as p, type AccessFrame as q, type CallEnvelope as r, streamRoom as s, topicRoom as t, userRoom as u, type CallId as v, type CallNotModified as w, type CallReply as x, type CallSuccess as y, type CancelFrame as z };
|