@fitzzero/quickdraw-core 5.0.0-rc.2 → 5.0.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +136 -15
  2. package/dist/{admin-Bl71J4IG.d.ts → admin-BiVNyWdI.d.ts} +1 -1
  3. package/dist/{auth-DyCIKmTQ.d.ts → auth-DlFtykbn.d.ts} +2 -2
  4. package/dist/{caller-Cx5wuao-.d.ts → caller-D4cgj-Xl.d.ts} +120 -14
  5. package/dist/{chunk-UJQJM7EG.js → chunk-3ONXZP2X.js} +3 -2
  6. package/dist/chunk-3ONXZP2X.js.map +1 -0
  7. package/dist/{chunk-SSDLYYQV.js → chunk-5CXPEEWF.js} +2 -2
  8. package/dist/{chunk-SSDLYYQV.js.map → chunk-5CXPEEWF.js.map} +1 -1
  9. package/dist/{chunk-5ADGYGC5.js → chunk-B4IOFSM6.js} +2 -2
  10. package/dist/{chunk-NRM4XAWR.js → chunk-CNECM3VC.js} +2 -2
  11. package/dist/{chunk-5EZNDES3.js → chunk-CWPBMY7P.js} +2 -2
  12. package/dist/{chunk-5EZNDES3.js.map → chunk-CWPBMY7P.js.map} +1 -1
  13. package/dist/{chunk-GXLJA7AH.js → chunk-DEGCLMTU.js} +6 -5
  14. package/dist/chunk-DEGCLMTU.js.map +1 -0
  15. package/dist/{chunk-U32GOJRO.js → chunk-GBM7QP5Z.js} +3 -3
  16. package/dist/chunk-GBM7QP5Z.js.map +1 -0
  17. package/dist/{chunk-EI57GEMD.js → chunk-H23KEFBA.js} +1 -1
  18. package/dist/{chunk-EI57GEMD.js.map → chunk-H23KEFBA.js.map} +1 -1
  19. package/dist/{chunk-7E7STZEO.js → chunk-HVJ4KIYX.js} +1 -1
  20. package/dist/{chunk-7E7STZEO.js.map → chunk-HVJ4KIYX.js.map} +1 -1
  21. package/dist/{chunk-DXTJNJM5.js → chunk-ISNQKSHQ.js} +3 -3
  22. package/dist/{chunk-I2OMV4AD.js → chunk-TNTZBUQX.js} +224 -60
  23. package/dist/chunk-TNTZBUQX.js.map +1 -0
  24. package/dist/cli/quickdraw-docs.js +3 -2
  25. package/dist/cli/quickdraw-docs.js.map +1 -1
  26. package/dist/client/index.d.ts +4 -4
  27. package/dist/client/index.js +3 -3
  28. package/dist/{clientTypes-B62bWHua.d.ts → clientTypes-P_VIjnyz.d.ts} +2 -2
  29. package/dist/{createServer-P61KBWro.d.ts → createServer-BwhO8YlA.d.ts} +35 -6
  30. package/dist/{createTestApp-BBm3TakW.d.ts → createTestApp-CBESgORq.d.ts} +10 -6
  31. package/dist/{envelope-CoiZY9KU.d.ts → envelope-CKQEC7e-.d.ts} +6 -4
  32. package/dist/{http-89lENH2D.d.ts → http-B5hNMuVY.d.ts} +17 -10
  33. package/dist/index.d.ts +5 -5
  34. package/dist/index.js +8 -4
  35. package/dist/index.js.map +1 -1
  36. package/dist/{infer-Cwq49ME5.d.ts → infer-Dlplloxi.d.ts} +32 -3
  37. package/dist/{metrics-Ny1dFbyY.d.ts → metrics-BBUPqcsT.d.ts} +2 -2
  38. package/dist/prisma/index.d.ts +4 -4
  39. package/dist/prisma/index.js +3 -3
  40. package/dist/server/auth/index.d.ts +98 -12
  41. package/dist/server/auth/index.js +99 -12
  42. package/dist/server/auth/index.js.map +1 -1
  43. package/dist/server/index.d.ts +65 -27
  44. package/dist/server/index.js +90 -21
  45. package/dist/server/index.js.map +1 -1
  46. package/dist/server/mcp/index.d.ts +22 -11
  47. package/dist/server/mcp/index.js +11 -6
  48. package/dist/server/mcp/index.js.map +1 -1
  49. package/dist/server/otel.d.ts +3 -3
  50. package/dist/{sharing-BHyufgVl.d.ts → sharing-RrW03F9j.d.ts} +2 -2
  51. package/dist/testing/client.d.ts +11 -11
  52. package/dist/testing/client.js +3 -3
  53. package/dist/testing/index.d.ts +29 -15
  54. package/dist/testing/index.js +28 -10
  55. package/dist/testing/index.js.map +1 -1
  56. package/dist/{types-BDmssndG.d.ts → types-B3N6xKkl.d.ts} +1 -1
  57. package/dist/utils/index.d.ts +1 -1
  58. package/package.json +1 -1
  59. package/dist/chunk-GXLJA7AH.js.map +0 -1
  60. package/dist/chunk-I2OMV4AD.js.map +0 -1
  61. package/dist/chunk-U32GOJRO.js.map +0 -1
  62. package/dist/chunk-UJQJM7EG.js.map +0 -1
  63. /package/dist/{chunk-5ADGYGC5.js.map → chunk-B4IOFSM6.js.map} +0 -0
  64. /package/dist/{chunk-NRM4XAWR.js.map → chunk-CNECM3VC.js.map} +0 -0
  65. /package/dist/{chunk-DXTJNJM5.js.map → chunk-ISNQKSHQ.js.map} +0 -0
package/README.md CHANGED
@@ -716,7 +716,10 @@ export const taskService = qd.defineService(task, {
716
716
  (`qd:access`).
717
717
  - Policies: `owner(field)`, `jsonAcl(field, { owner? })`,
718
718
  `members({ model, entry, user, level, levels? })`, `inherit({ from, via })`,
719
- `anyOf(...)` and `resolver({ levelsFor, where? })`. Their column names are
719
+ `anyOf(...)`, `resolver({ levelsFor, where? })` and `everyone(level)`
720
+ (every signed-in user has `level` on every row, reading nothing: public
721
+ profiles are `anyOf(owner("id"), everyone("Read"))`; unlike `rowless: true`
722
+ on a method, it covers subscriptions and lists too). Their column names are
720
723
  checked against the Prisma client's models at compile time. A lookup is one
721
724
  batched query per table, memoized for the call, so checking 60 ids costs
722
725
  what checking one does. `entry` access needs a policy; a service without
@@ -766,7 +769,11 @@ tracked client as `db`; the server finds the rest on it.
766
769
  (`writes: ["taskLabel"]`); the `no-foreign-write` lint rule checks it.
767
770
  - Jobs, scripts and webhooks wrap their writes in `qd.run(fn)`, which
768
771
  flushes before it returns. A write made outside any unit of work flushes
769
- on its own on the next tick, with a development warning.
772
+ on its own on the next tick, with a development warning. Inside a
773
+ handler `qd.run` joins the handler's unit; background work the reply does
774
+ not wait for (a push sent after a message, pruning what it reports dead)
775
+ runs in `qd.run(fn, { detached: true })`, a unit of its own that flushes
776
+ when `fn` settles (catch what the promise rejects with: nothing awaits it).
770
777
 
771
778
  <!-- example: apps/api/src/jobs/overdue.ts#run -->
772
779
 
@@ -828,14 +835,16 @@ export const taskService = qd.defineService(task, {
828
835
  // a write to a subtask sends its parent again
829
836
  affects: [{ service: task, id: "parentTaskId" }],
830
837
  project: {
831
- // a relation count: read with select, built by a pure, synchronous map
838
+ // a relation count: read with select, built by a pure, synchronous map. Read the relation's
839
+ // ids, which Prisma fetches for the rows read only; its _count aggregates the whole TaskLabel
840
+ // table (a GROUP BY over every row) on every snapshot and flush
832
841
  card: {
833
- select: { title: true, status: true, _count: { select: { labels: true } } },
834
- map: (row: { id: string; title: string; status: string; _count: { labels: number } }) => ({
842
+ select: { title: true, status: true, labels: { select: { id: true } } },
843
+ map: (row: { id: string; title: string; status: string; labels: { id: string }[] }) => ({
835
844
  id: row.id,
836
845
  title: row.title,
837
846
  status: row.status,
838
- labelCount: row._count.labels,
847
+ labelCount: row.labels.length,
839
848
  }),
840
849
  },
841
850
  },
@@ -848,13 +857,19 @@ export const taskService = qd.defineService(task, {
848
857
  handler: ({ input, db }) =>
849
858
  db.task.findUniqueOrThrow({
850
859
  where: { id: input.id },
851
- select: { id: true, title: true, status: true, _count: { select: { labels: true } } },
860
+ select: { id: true, title: true, status: true, labels: { select: { id: true } } },
852
861
  }),
853
862
  },
854
863
  },
855
864
  });
856
865
  ```
857
866
 
867
+ - A count over a relation reads the relation's ids (`labels: { select: { id: true } }`)
868
+ and counts them in `map`: Prisma reads them for the rows read only (a
869
+ second SQL query, `WHERE "taskId" IN (...)`). Its `_count` compiles to a
870
+ `GROUP BY` over the whole relation table, joined in, on every snapshot,
871
+ page and flush that reads the projection. For a relation too large to read
872
+ (a chat's members), keep a counter column that the writes maintain.
858
873
  - A projection's keys come from its schema's JSON Schema (Zod 4.2 or later),
859
874
  or from `project: { <name>: { keys } }`; a service whose projection has
860
875
  neither fails when it is defined. A handler returning a projection returns
@@ -959,6 +974,11 @@ export const taskService = qd.defineService(task, {
959
974
  ends in `id`; `limit` (default 100) and `maxLimit` (default 500) size its
960
975
  pages; `where` is an equality filter on membership; `access` is the level
961
976
  needed on the anchor (default `Read`).
977
+ - A junction write adds or removes its entry in the one scope it links.
978
+ When the item reads the junction (a chat's `memberCount`), declare
979
+ `via({ model, entry, scope, refreshEntry: true })`: every junction write
980
+ then also sends the entry again, `updated`, to each scope that still
981
+ holds it, for one more read of the item per flush.
962
982
  - `qd:col:sub { s, c, scope }` authorizes the scope through its anchor's
963
983
  policy, then answers a page and joins the scope's room; flushes send `qd:c`
964
984
  deltas to it (`added`, `updated`, `patched`, `removed`, or `reset` for more
@@ -1105,7 +1125,9 @@ createMcpStdioServer({ registry, name: "my-app", version: "1.0.0" });
1105
1125
 
1106
1126
  - **stdio** speaks JSON-RPC (MCP protocol version 2024-11-05). One process is
1107
1127
  one session: its queries share one concurrency lane, and
1108
- `notifications/cancelled` cancels a call. Start its module through
1128
+ `notifications/cancelled` cancels a call. When stdin ends, the calls still
1129
+ running finish and their replies are written before `closed` resolves
1130
+ (`server.close()` cancels them instead). Start its module through
1109
1131
  `bootstrapMcpServer(new URL("./mcp-server.js", import.meta.url))`, which
1110
1132
  sends console output to stderr so only the protocol reaches stdout.
1111
1133
  - **HTTP**: `GET /mcp/tools` and `POST /mcp/invoke`, which takes
@@ -1115,6 +1137,11 @@ createMcpStdioServer({ registry, name: "my-app", version: "1.0.0" });
1115
1137
  `"public"` methods, and custom tools that declare `access: "public"`; any
1116
1138
  other tool answers `UNAUTHENTICATED` before it runs. A failed call reaches
1117
1139
  the agent as a tool error carrying the code.
1140
+ - The tool list is the registry's, the same for every caller: `tools/list`
1141
+ and `GET /mcp/tools` are not filtered by the principal, so an agent sees
1142
+ tools it may not call (and its call is refused). Keep a tool out of the
1143
+ list with `exclude` or `include`, per registry, and serve agents of
1144
+ different reach from separate registries.
1118
1145
 
1119
1146
  ## The client
1120
1147
 
@@ -1789,6 +1816,15 @@ export function AdminTasks() {
1789
1816
  configurations written for 4.x still type); `id` and the
1790
1817
  timestamps come first and are not editable; `acl`, `serviceAccess` and
1791
1818
  `service_access` are hidden.
1819
+ - Grants: on a users service, `admin.handlers(user, { grants: true })`
1820
+ shows and writes the entity's `serviceAccess` (a `json` field the entity
1821
+ schema checks), so the admin screen edits grants through `adminUpdate`.
1822
+ Only a caller whose own service-wide grant on that service is `Admin`
1823
+ reads or writes it, whatever `access` gives the method: anyone else gets
1824
+ rows without it and `FORBIDDEN` for a write, filter or sort naming it.
1825
+ The write is tracked, so with `auth.serviceAccessSource` naming the column
1826
+ the user's open sockets get the new grants at once, on every node. Such
1827
+ an Admin can grant any service, themself included.
1792
1828
  - `adminSubscribers({ id })` counts the sockets subscribed to a row per
1793
1829
  access level (`{ id, count, levels, complete }`; behind a Redis adapter
1794
1830
  the counts are this server's and `complete` is `false`), and
@@ -1835,7 +1871,11 @@ export const task = defineContract("taskService", {
1835
1871
  requires: { room: (cursor) => `board:${cursor.projectId}` },
1836
1872
  },
1837
1873
  },
1838
- events: { cursorMoved: { payload: cursorSchema } },
1874
+ events: {
1875
+ cursorMoved: { payload: cursorSchema },
1876
+ // a user's last socket left a board: `onRoomLeave` sends it
1877
+ leftBoard: { payload: z.object({ projectId: z.string(), userId: z.string() }) },
1878
+ },
1839
1879
  });
1840
1880
  ```
1841
1881
 
@@ -1939,14 +1979,69 @@ export function TaskRoom({
1939
1979
  `ctx.rooms.join(room)` and `leave` put the calling socket in an app room
1940
1980
  (calls without a socket get `false`; names starting with `qd:` or `user:`
1941
1981
  are refused with `VALIDATION`; at most 100 per socket; a method that shares
1942
- its runs, `share`, may not join or leave: `INTERNAL`), and the room's
1943
- sockets get `qd:presence` frames: the list on joining, then who joins and
1944
- who leaves. `usePresence(room)` shows them.
1982
+ its runs, `share`, may not join or leave with its caller's socket:
1983
+ `INTERNAL`), and the room's sockets get `qd:presence` frames: the list on
1984
+ joining, then who joins and who leaves. `usePresence(room)` shows them.
1945
1985
  - Events: `ctx.rooms.emit(room, contract, event, payload)` and
1946
1986
  `emitToUser(userId, ...)` check the payload first (`INTERNAL`, nothing
1947
1987
  sent, when it fails), then send the validated payload as
1948
1988
  `qd:event [service, event, payload]`; `useEvent` hears them.
1949
1989
 
1990
+ Code that is not a handler (a game loop, a timer, a job) reaches rooms
1991
+ through `qd.rooms` (also `server.rooms`), and `onRoomLeave` hears sockets
1992
+ leave:
1993
+
1994
+ <!-- example: apps/api/src/services/kits/rooms.ts#rooms -->
1995
+
1996
+ ```ts
1997
+ import type { RoomLeaveHandler } from "@fitzzero/quickdraw-core/server";
1998
+
1999
+ const boardRoom = (projectId: string): string => `board:${projectId}`;
2000
+
2001
+ // a timer or a game loop's tick, outside any handler: every socket in the room, on every node
2002
+ export function showCursor(projectId: string, taskId: string, x: number): void {
2003
+ qd.rooms.emit(boardRoom(projectId), task, "cursorMoved", { projectId, taskId, x });
2004
+ }
2005
+
2006
+ // a member removed from the project: their sockets leave its board on every node, so they hear
2007
+ // nothing more of it and its cursor channel drops their messages (also ctx.rooms.leave)
2008
+ export async function removeFromBoard(projectId: string, userId: string): Promise<void> {
2009
+ await qd.rooms.leave(boardRoom(projectId), { userId });
2010
+ }
2011
+
2012
+ // qd.createServer({ ..., onRoomLeave }): once per socket that leaves app rooms, in a unit of its own
2013
+ export const onRoomLeave: RoomLeaveHandler<AppPrincipal> = ({ principal, rooms }) => {
2014
+ for (const { room, last } of rooms) {
2015
+ // last: no socket of the user is in the room any more (a second tab keeps it false)
2016
+ if (principal !== null && last && room.startsWith("board:")) {
2017
+ const projectId = room.slice("board:".length);
2018
+ qd.rooms.emit(room, task, "leftBoard", { projectId, userId: principal.userId });
2019
+ }
2020
+ }
2021
+ };
2022
+ ```
2023
+
2024
+ - `qd.rooms.emit` and `emitToUser` are `ctx.rooms`' own, from anywhere,
2025
+ reaching every node behind a cluster adapter.
2026
+ - `leave(room, { userId })` (on `qd.rooms`, and on `ctx.rooms` beside the
2027
+ calling socket's own `leave(room)`) takes every socket of the user out of
2028
+ an app room, on every node: they hear nothing more of it, a channel that
2029
+ `requires` the room drops their messages, each gets
2030
+ `qd:presence { room, users: [] }`, and the room hears `left`. Behind a
2031
+ cluster it is broadcast and answered, so await it before sending what the
2032
+ user must not receive. Joining again is the app's to refuse.
2033
+ - `createServer({ onRoomLeave })` runs once per socket that leaves app
2034
+ rooms, on the node that held it: its own `leave` (`reason: "leave"`), a
2035
+ removal (`"removed"`), or a disconnect, which leaves every app room it was
2036
+ in (`"disconnect"`; `qd:rotate` reconnects with a new socket). Each room
2037
+ comes with `last`: true when no socket of that user is in the room any
2038
+ more, on any node, which is what a game's `playerLeft` waits for. It runs
2039
+ in a unit of work of its own (never the unit of the handler that left), a
2040
+ throw is logged, and `close()` waits for it. Behind a cluster `last` is
2041
+ decided by asking every node once the socket left, so two sockets of one
2042
+ user leaving two nodes at the same moment may both see the other (a
2043
+ removal never misses: each node reports its own last socket).
2044
+
1950
2045
  ### Auth routes kit
1951
2046
 
1952
2047
  Sign-in for Google, Discord, a development mock and guests, as one Express
@@ -1981,7 +2076,8 @@ app.set("trust proxy", 1);
1981
2076
  app.use(
1982
2077
  createAuthRoutes({
1983
2078
  providers: [
1984
- google({ clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET }),
2079
+ // nothing without its credentials: the routes skip it
2080
+ google.optional({ clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET }),
1985
2081
  discord({ clientId: env.DISCORD_CLIENT_ID, clientSecret: env.DISCORD_CLIENT_SECRET }),
1986
2082
  // served only while isMockOAuthEnabled()
1987
2083
  mock({ listUsers: listSeededUsers }),
@@ -2030,7 +2126,7 @@ nothing is cached:
2030
2126
  | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
2031
2127
  | `GET /{provider}/start?returnTo=<url>` | 302 to the provider |
2032
2128
  | `GET /{provider}/callback` | 302 to `{origin}{successPath}` with the session cookie, or to `{origin}{errorPath}?error=state`, `denied` or `failed` |
2033
- | `POST /guest` | `createUser(body)`, then `{ userId }` with the session cookie |
2129
+ | `POST /guest` | `createUser(body)`, then `{ userId, name? }` with the session cookie (and `token` with `guest({ token: true })`) |
2034
2130
  | `GET /me` | `{ userId }`, or 401 |
2035
2131
  | `POST /logout` | 204: revokes the session, clears the cookie |
2036
2132
  | `POST /logout-all` | 204: revokes every session of the user; 401 without a live session |
@@ -2079,6 +2175,27 @@ nothing is cached:
2079
2175
  `allowMissingOrigin: true` is set for native clients that keep cookies.
2080
2176
  Bearer tokens need no Origin, and HTTP calls are guarded by their JSON
2081
2177
  content type instead.
2178
+ - `socketAuth({ devCredentials })` signs a socket in by the user id its
2179
+ handshake names (`auth: { userId }`, no token), as the function answers
2180
+ (the principal, or `null` to refuse): for a game editor or load-test bots
2181
+ in development. `socketAuth` throws when it is given while `NODE_ENV` is
2182
+ `production`, and refuses such a handshake there anyway; pass it only
2183
+ behind the app's own flag.
2184
+ - `google.optional(...)` and `discord.optional(...)` build nothing when
2185
+ neither credential is set (only one is a misconfiguration, refused), and
2186
+ `providers` skips `undefined`, `null` and `false` entries, so an
2187
+ environment without a provider's app leaves it out in place.
2188
+ - A guest's `createUser` may return `{ userId, name }` when the name it gave
2189
+ differs from the one asked (a numbered one after a collision), and the
2190
+ route answers it; `guest({ createUser, token: true })` also answers the
2191
+ session's token, for clients that keep no cookies (a game engine, a page
2192
+ in a third-party iframe), at the cost of the token being readable by the
2193
+ page's scripts.
2194
+ - `requireSession({ sessions, jwtSecret })` guards the app's own REST
2195
+ routes: the credential is read as `/me` reads it, the JWT verified once
2196
+ and the session checked in the store, then `req.userId` and
2197
+ `req.sessionId` are set; otherwise 401 `{ error: "UNAUTHENTICATED", message }`.
2198
+ 4.1's `createRequireAuth` stays for token-keyed sessions.
2082
2199
  - Rate limits: the sign-in routes share `createAuthLimiter({ max: 60 })` (60
2083
2200
  requests per 15 minutes per IP) and the session routes
2084
2201
  `createAuthStatusLimiter()` (120); pass `rateLimit: { signIn, session }`
@@ -2236,7 +2353,11 @@ it("lets the owner rename, members read, and nobody else in", async () => {
2236
2353
 
2237
2354
  `"deny"` (the default for everyone `allow` does not name) means
2238
2355
  `UNAUTHENTICATED` without a principal and `FORBIDDEN` with one. Mutations run
2239
- for real, once per allowed principal: give inputs that can run again.
2356
+ for real, once per allowed principal: give inputs that can run again, or
2357
+ `input` as a function of the cell (`({ name, principal }) => input`, sync or
2358
+ async), called before each cell's call, that makes a row of its own (a task
2359
+ to delete, an unused name), so no cell depends on the order of the
2360
+ principals.
2240
2361
 
2241
2362
  ### Performance budgets
2242
2363
 
@@ -1,4 +1,4 @@
1
- import { e as StandardSchemaWithJSON, x as SortDirection, d as StandardSchemaV1, I as InferInput, c as InferOutput, L as RowSchema, Q as QueryDef, P as ProjectionList, p as MutationDef, A as AccessLevel, G as AnyContract, al as MethodName } from './infer-Cwq49ME5.js';
1
+ import { e as StandardSchemaWithJSON, x as SortDirection, d as StandardSchemaV1, I as InferInput, c as InferOutput, L as RowSchema, Q as QueryDef, P as ProjectionList, p as MutationDef, A as AccessLevel, G as AnyContract, al as MethodName } from './infer-Dlplloxi.js';
2
2
 
3
3
  /** A schema a kit generates: validation, and its JSON Schema. */
4
4
  type KitSchema<Input, Output = Input> = StandardSchemaWithJSON<Input, Output>;
@@ -1,7 +1,7 @@
1
1
  import { IncomingHttpHeaders, IncomingMessage } from 'node:http';
2
2
  import { Socket } from 'socket.io';
3
- import { A as AccessLevel } from './infer-Cwq49ME5.js';
4
- import { P as Principal, M as MaybePromise } from './types-BDmssndG.js';
3
+ import { A as AccessLevel } from './infer-Dlplloxi.js';
4
+ import { P as Principal, M as MaybePromise } from './types-B3N6xKkl.js';
5
5
 
6
6
  /** A principal's service-wide grants by service name: `{ taskService: "Admin" }`. */
7
7
  type ServiceGrants = Readonly<Record<string, AccessLevel>>;
@@ -1,7 +1,7 @@
1
- import { ad as Version, a5 as Revision } from './envelope-CoiZY9KU.js';
1
+ import { ad as Version, a5 as Revision } from './envelope-CKQEC7e-.js';
2
2
  import { L as Logger } from './logger-BY28acW-.js';
3
- import { A as AccessLevel, G as AnyContract, w as CollectionWhere, O as OrderBy, k as MethodKind, d as StandardSchemaV1, ac as EventName, ad as EventPayloadOf, a6 as ChannelName, a7 as ChannelPayloadOf, av as StreamName, ai as IsScopedStream, au as StreamItemOf, aa as ContractMap, al as MethodName, ah as InputOf, an as OutputOf } from './infer-Cwq49ME5.js';
4
- import { P as Principal, Q as QuickdrawTypes, M as MaybePromise, c as PrincipalOf, a as McpContext, T as Transport, C as ContextExtensionOf, b as McpContextOf, D as DbOf } from './types-BDmssndG.js';
3
+ import { A as AccessLevel, G as AnyContract, w as CollectionWhere, O as OrderBy, k as MethodKind, d as StandardSchemaV1, ac as EventName, ad as EventPayloadOf, a6 as ChannelName, a7 as ChannelPayloadOf, av as StreamName, ai as IsScopedStream, au as StreamItemOf, aa as ContractMap, al as MethodName, ah as InputOf, an as OutputOf } from './infer-Dlplloxi.js';
4
+ import { P as Principal, Q as QuickdrawTypes, M as MaybePromise, c as PrincipalOf, a as McpContext, T as Transport, C as ContextExtensionOf, b as McpContextOf, D as DbOf } from './types-B3N6xKkl.js';
5
5
 
6
6
  /** What a development warning is about. */
7
7
  type DevWarningKind = "n-plus-one" | "unbounded-read" | "oversized-response" | "nested-write" | "ambient-write" | "batch-read" | "batch-create-many" | "repeated-call";
@@ -194,7 +194,7 @@ interface ForeignColumns {
194
194
  readonly columns: string;
195
195
  }
196
196
  /** The builder a policy came from. */
197
- type PolicyKind = "owner" | "jsonAcl" | "members" | "inherit" | "anyOf" | "resolver";
197
+ type PolicyKind = "owner" | "jsonAcl" | "members" | "inherit" | "anyOf" | "resolver" | "everyone";
198
198
  /**
199
199
  * A service's access policy, from one of the builders. `Columns` names the
200
200
  * columns of the service's own model it reads and `Foreign` the other models
@@ -299,6 +299,8 @@ type CollectionScope = {
299
299
  readonly entry: string;
300
300
  /** The junction column holding the scope value. */
301
301
  readonly scope: string;
302
+ /** A junction write sends its entry again to every scope that still holds it (`via`'s `refreshEntry`). */
303
+ readonly refreshEntry: boolean;
302
304
  };
303
305
  /** One collection of a service: its contract declaration and the service's option, checked. */
304
306
  interface ServiceCollection {
@@ -574,12 +576,55 @@ interface Presence {
574
576
  /** The ids of the users with a socket in `room`, each once. */
575
577
  users(room: string): Promise<string[]>;
576
578
  }
579
+ /** Whose sockets `rooms.leave(room, target)` takes out of a room: every socket of one user. */
580
+ interface RoomTarget {
581
+ readonly userId: string;
582
+ }
583
+ /**
584
+ * App rooms from code that is not a handler (a game loop, a job, a webhook):
585
+ * `qd.rooms`, `server.rooms` and `dispatcher.rooms`, the half of `ctx.rooms`
586
+ * that needs no calling socket. Behind a cluster adapter each reaches every
587
+ * node.
588
+ *
589
+ * @example
590
+ * // a game loop's tick, outside any handler
591
+ * qd.rooms.emit(WORLD_ROOM, gameContract, "death", { id: playerId });
592
+ * // a moderator removed a member: their sockets stop hearing the room
593
+ * await qd.rooms.leave(chatRoom(chatId), { userId });
594
+ */
595
+ interface ServerRooms {
596
+ /**
597
+ * Sends the contract's event to every socket in `room` as `qd:event`, on
598
+ * every node. The payload is checked against the event's schema first: a
599
+ * payload that fails it throws `INTERNAL` and nothing is sent. Without a
600
+ * server it does nothing more.
601
+ */
602
+ emit<C extends AnyContract, E extends EventName<C>>(room: string, contract: C, event: E, payload: EventPayloadOf<C, E>): void;
603
+ /** `emit` to every socket of one user (their `user:{userId}` room), on every node. */
604
+ emitToUser<C extends AnyContract, E extends EventName<C>>(userId: string, contract: C, event: E, payload: EventPayloadOf<C, E>): void;
605
+ /**
606
+ * Takes every socket of `target.userId` out of the app room `room`, on
607
+ * every node: they stop receiving the room's events and `qd:presence`
608
+ * frames, and a channel that `requires: { room }` drops their messages.
609
+ * Each socket taken out gets `qd:presence { room, users: [] }`, as after
610
+ * its own leave, and `onRoomLeave` hears of it with reason `"removed"`.
611
+ * Behind a cluster adapter the request is broadcast and answered: it
612
+ * resolves once every node took the user's sockets out (at most
613
+ * `cluster.timeoutMs`), so await it before emitting what the user must not
614
+ * receive. Room names are checked as `join` checks them (`VALIDATION`). A
615
+ * user with no socket in the room is nothing to do; joining again is the
616
+ * app's to refuse.
617
+ */
618
+ leave(room: string, target: RoomTarget): Promise<void>;
619
+ }
577
620
  /**
578
621
  * `ctx.rooms` (RFC 0003 sections 3, 12.5 and 15): app-defined rooms the
579
622
  * calling socket joins and leaves, and typed room events. It replaces 4.1's
580
623
  * `emitToRoom` and `emitToUserRoom` (`legacy-src/server/BaseService.ts:378-441`).
624
+ * Everything that needs no calling socket is also on `qd.rooms`
625
+ * ({@link ServerRooms}).
581
626
  */
582
- interface ContextRooms {
627
+ interface ContextRooms extends ServerRooms {
583
628
  /**
584
629
  * Puts the calling socket in an app-defined room (a lobby), so it receives
585
630
  * the room's events and `qd:presence` frames. Returns `false`, doing
@@ -592,16 +637,49 @@ interface ContextRooms {
592
637
  join(room: string): boolean;
593
638
  /** Takes the calling socket out of an app room; `false` when it was not in it, or the call has no socket. */
594
639
  leave(room: string): boolean;
640
+ /** See {@link ServerRooms.leave}: every socket of the user, on every node, from any call. */
641
+ leave(room: string, target: RoomTarget): Promise<void>;
642
+ }
643
+ /** Why a socket left app rooms, as `onRoomLeave` hears it. */
644
+ type RoomLeaveReason =
645
+ /** The socket's own `ctx.rooms.leave(room)`. */
646
+ "leave"
647
+ /** `rooms.leave(room, { userId })` took its user out. */
648
+ | "removed"
649
+ /** The socket disconnected (a closed tab, a lost network, `qd:rotate`, the server closing): every app room it was in. */
650
+ | "disconnect";
651
+ /** One app room a socket left. */
652
+ interface RoomLeft {
653
+ readonly room: string;
595
654
  /**
596
- * Sends the contract's event to every socket in `room` as `qd:event`. The
597
- * payload is checked against the event's schema first: a payload that
598
- * fails it throws `INTERNAL` and nothing is sent. Without a server it does
599
- * nothing more.
655
+ * True when no socket of the user is in the room any more, on any node:
656
+ * the user is gone from it, not just one of their sockets (another tab
657
+ * keeps it false). Always true for an anonymous socket. Behind a cluster
658
+ * adapter it is decided by asking every node once this socket left, as
659
+ * the room's `left` presence frame is; when they cannot be asked it is
660
+ * true. For a removal every node reports its own last socket of the user
661
+ * as last, so it is never missed and may come from more than one node.
600
662
  */
601
- emit<C extends AnyContract, E extends EventName<C>>(room: string, contract: C, event: E, payload: EventPayloadOf<C, E>): void;
602
- /** `emit` to every socket of one user (their `user:{userId}` room). */
603
- emitToUser<C extends AnyContract, E extends EventName<C>>(userId: string, contract: C, event: E, payload: EventPayloadOf<C, E>): void;
663
+ readonly last: boolean;
604
664
  }
665
+ /** What `onRoomLeave` receives: one socket leaving one or more app rooms. */
666
+ interface RoomLeave<P = Principal> {
667
+ /** The socket's principal; `null` for an anonymous socket (a spectator). */
668
+ readonly principal: P | null;
669
+ /** The socket that left. */
670
+ readonly socketId: string;
671
+ readonly reason: RoomLeaveReason;
672
+ /** The app rooms it left: one for a leave or a removal, every one it was in for a disconnect. */
673
+ readonly rooms: readonly RoomLeft[];
674
+ }
675
+ /**
676
+ * `createServer`'s `onRoomLeave`: called once per socket that leaves app
677
+ * rooms, on the node that holds the socket, in a unit of work of its own
678
+ * (detached: never the unit of the handler whose `ctx.rooms.leave` caused
679
+ * it), after the room heard the socket go. `ctx` is a `qd.run` context. An
680
+ * error it throws is logged; a server's `close()` waits for it.
681
+ */
682
+ type RoomLeaveHandler<P = Principal> = (leave: RoomLeave<P>, ctx: RunContext) => void | PromiseLike<void>;
605
683
  /** The arguments of a stream's `push`: `(scope, item)` for a scoped stream, `(item)` for a global one. */
606
684
  type StreamPushArgs<C extends AnyContract, K extends StreamName<C>> = IsScopedStream<C, K> extends true ? [scope: string, item: StreamItemOf<C, K>] : [item: StreamItemOf<C, K>];
607
685
  /** The arguments of a stream's `pushMany`: `(scope, items)` for a scoped stream, `(items)` for a global one. */
@@ -826,6 +904,26 @@ interface RunContext {
826
904
  readonly log: Logger;
827
905
  readonly principal: null;
828
906
  }
907
+ /** Options of `qd.run(fn, options)` and `dispatcher.run(fn, options)`. */
908
+ interface RunOptions {
909
+ /**
910
+ * Run `fn` in a unit of work of its own even inside a method call or a
911
+ * transaction: for background work a handler starts and does not await
912
+ * (a push sent after the reply, pruning what it reports dead). Its writes
913
+ * flush once `fn` settles, on their own, instead of joining the handler's
914
+ * unit, which may have flushed long before (they would then flush as
915
+ * ambient writes, with a development warning). A failed flush is logged.
916
+ * Default `false`: inside an open unit of work or transaction, `fn` joins
917
+ * it.
918
+ *
919
+ * @example
920
+ * // in a handler, not awaited: the reply does not wait for the push
921
+ * void qd.run(() => sendPushes(db, message), { detached: true }).catch((error) => {
922
+ * ctx.log.error("Pushing the message failed", { error: String(error) });
923
+ * });
924
+ */
925
+ readonly detached?: boolean;
926
+ }
829
927
 
830
928
  /**
831
929
  * The `fields` of a write whose changed fields are unknown, such as a
@@ -881,6 +979,14 @@ interface UnitOfWorkScope {
881
979
  * dispatcher's own calls throw. Without them, the attached dispatcher's.
882
980
  */
883
981
  readonly warnings?: DevWarnings;
982
+ /**
983
+ * A unit of its own even where another unit or a transaction is open (a
984
+ * handler's background work, `qd.run(fn, { detached: true })`): its writes
985
+ * flush when it does, never with the unit around it, and its statements
986
+ * are not counted there. Default `false`: a unit begun inside an open
987
+ * one joins it.
988
+ */
989
+ readonly detached?: boolean;
884
990
  }
885
991
  /**
886
992
  * The writes of one handler run. The dispatcher calls `run` once, with the
@@ -924,7 +1030,7 @@ interface UnitOfWorkFactory {
924
1030
  }
925
1031
 
926
1032
  /** What a sink learns about a flush besides its writes: where they came from, and the flush's revision. */
927
- interface FlushInfo extends Omit<UnitOfWorkScope, "sink" | "warnings"> {
1033
+ interface FlushInfo extends Omit<UnitOfWorkScope, "sink" | "warnings" | "detached"> {
928
1034
  /**
929
1035
  * The flush's revision (RFC 0003 section 5.3, step 2), taken once for the
930
1036
  * whole batch before any sink runs, so before any row is read.
@@ -972,4 +1078,4 @@ type CallerFor<T extends QuickdrawTypes> = T extends {
972
1078
  readonly contracts: infer Contracts extends ContractMap;
973
1079
  } ? Caller<Contracts[keyof Contracts]> : Caller<AnyContract>;
974
1080
 
975
- export { DEFAULT_BULK_THRESHOLD as $, type AnyService as A, type ShareMode as B, type CountArgs as C, DevWarningError as D, type ChannelOptions as E, type FindManyArgs as F, type ChannelsRequired as G, type HandlerContext as H, type BaseContext as I, type CallerFor as J, type AccessPolicy as K, type AnyAccessPolicy as L, type ModelName as M, type PolicyColumns as N, type PolicyForeign as O, type Presence as P, type PolicyTools as Q, type RowLevels as R, type StorageAdapter as S, type RowAccess as T, type UnitOfWorkFactory as U, type AffectsLink as V, type WriteRecord as W, type ProjectedOutput as X, type Projection as Y, type TierGroup as Z, type Tiers as _, type StorageRow as a, type CollectionScope as a0, type ServiceCollection as a1, type ContextRooms as a2, type ContextServices as a3, type TouchOptions as a4, type ChannelAccess as a5, type ChannelContext as a6, type ChannelHandler as a7, type ChannelImplementation as a8, type ServiceChannel as a9, type ServiceStream as aa, type StreamPushArgs as ab, type StreamPushManyArgs as ac, type CallOptions as ad, type MethodCaller as ae, type ServiceCaller as af, type ForeignColumns as ag, type ForeignColumnsOf as ah, type MembershipRead as ai, type ParentLink as aj, type PolicyKind as ak, type PolicyReads as al, type RowLevel as am, type AccessForm as an, type AccessRequest as ao, type AuthenticatedAccess as ap, type EntryAccess as aq, type IdKeyOf as ar, type IdSelector as as, type ScopeAccess as at, type ServiceAccess as au, ANY_FIELD as av, type UnitOfWork as aw, type UnitOfWorkScope as ax, formatDevWarning as ay, N_PLUS_ONE_STATEMENTS as az, type StorageWhere as b, type StatementCount as c, type FlushSink as d, type FlushInfo as e, type Service as f, type DevWarning as g, type DevWarningKind as h, type Caller as i, type ServiceMethod as j, type AccessFilter as k, type RunContext as l, type StreamHandle as m, type AnyContext as n, type AccessEngine as o, type AppRooms as p, type StreamSubscriptions as q, type PolicyFor as r, storageOf as s, type RowForms as t, type AccessFor as u, type ModelColumn as v, type WatchAccess as w, type PublicAccess as x, type HandlerArgs as y, type CustomAccess as z };
1081
+ export { type Projection as $, type AnyService as A, type PublicAccess as B, type CountArgs as C, DevWarningError as D, type HandlerArgs as E, type FindManyArgs as F, type CustomAccess as G, type HandlerContext as H, type ShareMode as I, type ChannelOptions as J, type ChannelsRequired as K, type BaseContext as L, type ModelName as M, type CallerFor as N, type AccessPolicy as O, type Presence as P, type AnyAccessPolicy as Q, type RowLevels as R, type StorageAdapter as S, type PolicyColumns as T, type UnitOfWorkFactory as U, type PolicyForeign as V, type WriteRecord as W, type PolicyTools as X, type RowAccess as Y, type AffectsLink as Z, type ProjectedOutput as _, type StorageRow as a, type TierGroup as a0, type Tiers as a1, DEFAULT_BULK_THRESHOLD as a2, type CollectionScope as a3, type ServiceCollection as a4, type ContextRooms as a5, type ContextServices as a6, type TouchOptions as a7, type ChannelAccess as a8, type ChannelContext as a9, type ScopeAccess as aA, type ServiceAccess as aB, ANY_FIELD as aC, type UnitOfWork as aD, type UnitOfWorkScope as aE, formatDevWarning as aF, N_PLUS_ONE_STATEMENTS as aG, type ChannelHandler as aa, type ChannelImplementation as ab, type RoomLeave as ac, type RoomLeaveReason as ad, type RoomLeft as ae, type RoomTarget as af, type ServiceChannel as ag, type ServiceStream as ah, type StreamPushArgs as ai, type StreamPushManyArgs as aj, type CallOptions as ak, type MethodCaller as al, type ServiceCaller as am, type ForeignColumns as an, type ForeignColumnsOf as ao, type MembershipRead as ap, type ParentLink as aq, type PolicyKind as ar, type PolicyReads as as, type RowLevel as at, type AccessForm as au, type AccessRequest as av, type AuthenticatedAccess as aw, type EntryAccess as ax, type IdKeyOf as ay, type IdSelector as az, type StorageWhere as b, type StatementCount as c, type FlushSink as d, type FlushInfo as e, type Service as f, type DevWarning as g, type DevWarningKind as h, type Caller as i, type ServiceMethod as j, type AccessFilter as k, type RunContext as l, type RunOptions as m, type ServerRooms as n, type StreamHandle as o, type AnyContext as p, type AccessEngine as q, type AppRooms as r, storageOf as s, type StreamSubscriptions as t, type RoomLeaveHandler as u, type PolicyFor as v, type RowForms as w, type AccessFor as x, type ModelColumn as y, type WatchAccess as z };
@@ -6,7 +6,8 @@ function via(junction) {
6
6
  kind: "via",
7
7
  model: junction.model,
8
8
  entry: junction.entry,
9
- scope: junction.scope
9
+ scope: junction.scope,
10
+ ...junction.refreshEntry === void 0 ? {} : { refreshEntry: junction.refreshEntry }
10
11
  });
11
12
  }
12
13
 
@@ -27,4 +28,4 @@ export {
27
28
  CHANNEL_DEFAULT_RATE,
28
29
  isScopedStream
29
30
  };
30
- //# sourceMappingURL=chunk-UJQJM7EG.js.map
31
+ //# sourceMappingURL=chunk-3ONXZP2X.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/contract/collections.ts","../src/contract/realtime.ts"],"sourcesContent":["// Collection declarations for a contract (RFC 0003 section 7.1). A collection\n// is the rows of one service grouped by a scope value. Membership is declared\n// (a scope column or a junction table, plus an equality filter), never\n// computed by a function, so the framework knows which writes can move a row.\n// This replaces 4.1's `defineCollection({ resolveScopeId, checkScopeAccess,\n// snapshot, toItem })`.\n\nimport type { AccessLevel } from \"./access\";\n\n/** Page size of a collection snapshot when the collection sets no `limit`. */\nexport const DEFAULT_COLLECTION_LIMIT = 100;\n\n/** Largest page a client may ask for when the collection sets no `maxLimit`. */\nexport const DEFAULT_COLLECTION_MAX_LIMIT = 500;\n\n/**\n * A scope that comes from a junction table instead of a column on the row:\n * the rows of `model` link an entry (`entry` holds the row's id) to a scope\n * value (`scope`). For example, a chat appears in each member's list through\n * `via({ model: \"chatMember\", entry: \"chatId\", scope: \"userId\" })`.\n */\nexport interface ViaScope<\n Model extends string = string,\n Entry extends string = string,\n Scope extends string = string,\n> {\n readonly kind: \"via\";\n readonly model: Model;\n readonly entry: Entry;\n readonly scope: Scope;\n /**\n * The item is computed from the junction (a member count): every junction\n * write sends its entry again, whole, to each scope that still holds it.\n * Without it a junction write only adds or removes the entry in the scope\n * it links, so a count over the junction goes stale everywhere else.\n */\n readonly refreshEntry?: boolean;\n}\n\n/** What `via(...)` takes: the junction model, its entry and scope columns, and `refreshEntry`. */\nexport interface ViaOptions<\n Model extends string = string,\n Entry extends string = string,\n Scope extends string = string,\n> {\n /** The junction model, as the database client names it: `\"chatMember\"`. */\n readonly model: Model;\n /** The junction column holding the entry's id: `\"chatId\"`. */\n readonly entry: Entry;\n /** The junction column holding the scope value: `\"userId\"`. */\n readonly scope: Scope;\n /**\n * Set when the item reads the junction, as a `memberCount` mapped from a\n * relation count does: a junction create, update or delete then sends the\n * entry again, as `updated`, to every scope that still holds it (one read\n * of the entry's item per flush). Default `false`: a junction write adds\n * or removes the entry for the scope it links, and nothing else.\n */\n readonly refreshEntry?: boolean;\n}\n\n/**\n * Declares a collection scope that comes from a junction table.\n *\n * @example\n * myChats: {\n * scope: via({ model: \"chatMember\", entry: \"chatId\", scope: \"userId\", refreshEntry: true }),\n * item: \"listItem\",\n * order: [[\"id\", \"asc\"]],\n * }\n */\nexport function via<\n const Model extends string,\n const Entry extends string,\n const Scope extends string,\n>(junction: ViaOptions<Model, Entry, Scope>): ViaScope<Model, Entry, Scope> {\n return Object.freeze({\n kind: \"via\",\n model: junction.model,\n entry: junction.entry,\n scope: junction.scope,\n ...(junction.refreshEntry === undefined ? {} : { refreshEntry: junction.refreshEntry }),\n });\n}\n\nexport type SortDirection = \"asc\" | \"desc\";\n\n/**\n * Sort columns for the keyset cursor, ending in `id` so every row has a\n * unique position: `[[\"ordinal\", \"asc\"], [\"id\", \"asc\"]]`.\n */\nexport type OrderBy<Column extends string = string> = readonly [\n ...(readonly [Column, SortDirection])[],\n readonly [\"id\", SortDirection],\n];\n\n/** Who a view is evaluated for, on the client. */\nexport interface Viewer {\n readonly userId: string;\n}\n\n/**\n * A view: a named pure predicate evaluated on the client over the index rows\n * of a scope (RFC 0003 section 7.5). `row` holds only `id` and the\n * collection's `index` fields, which the type enforces.\n */\nexport type ViewPredicate<Row> = (row: Row, who: Viewer) => boolean;\n\n/** The equality filter that decides membership, for example `{ archived: false }`. */\nexport interface CollectionWhere {\n readonly [column: string]: string | number | boolean | null;\n}\n\n/** Any collection of any contract, as `defineContract` stores it. */\nexport interface CollectionDef {\n /** A string column of the entity, or `via(...)` for a junction table. */\n readonly scope: string | ViaScope;\n /** The projection each item is sent as: `\"entity\"` or a named projection. */\n readonly item: string;\n /** Sort columns for the keyset cursor; the last one is `id`. */\n readonly order: OrderBy;\n /** Equality filter that decides membership. */\n readonly where?: CollectionWhere | undefined;\n /** Page size. Default {@link DEFAULT_COLLECTION_LIMIT}. */\n readonly limit?: number | undefined;\n /** Largest page a client may ask for. Default {@link DEFAULT_COLLECTION_MAX_LIMIT}. */\n readonly maxLimit?: number | undefined;\n /** Item fields sent for every member of the scope (RFC 0003 section 7.4). */\n readonly index?: readonly string[] | undefined;\n /** Named predicates over index rows. Requires `index`. */\n readonly views?: { readonly [view: string]: ViewPredicate<never> } | undefined;\n /** Level required on the scope's anchor row. Default `\"Read\"`, through the anchor's policy. */\n readonly access?: AccessLevel | undefined;\n}\n","// Streams, channels and typed room events in a contract (RFC 0003 section\n// 12.5). A stream is server-to-client append-only data a subscriber receives\n// with a seed of recent items; a channel is fire-and-forget client-to-server\n// input (cursors, typing) behind a per-socket token bucket; an event is a\n// typed custom frame a handler sends to a room. 4.1 declared channels on the\n// service class (`legacy-src/server/BaseService.ts:915-942`) and typed room\n// events through the augmentable `QuickdrawEventMap`; here all three live in\n// the contract, so the client is typed from it.\n//\n// Methods, collections, streams, channels and events share one namespace:\n// the client exposes each as `qd.<service>.<name>`.\n\nimport type { AccessLevel } from \"./access\";\nimport type { AnyContract } from \"./defineContract\";\nimport type { StandardSchemaV1 } from \"./standardSchema\";\n\n/** The `scope` of a stream with one feed for the whole service: the default. */\nexport const GLOBAL_STREAM = \"global\";\n\n/** The most items a stream may keep per scope as the seed for new subscribers. */\nexport const STREAM_MAX_SEED = 1000;\n\n/** A channel's sustained rate when it declares none: messages per second per socket. */\nexport const CHANNEL_DEFAULT_RATE = 30;\n\n/**\n * Who may subscribe to a stream (RFC 0003 section 4.1's forms, as data). A\n * scoped stream's scope value is the row an `entry` or `scope` form is about:\n * `{ entry: L }` needs level `L` on the row of this service whose id is the\n * scope, `{ scope: L, of }` level `L` on that row of `of`'s service. A\n * stream that declares none is closed: no client may subscribe.\n */\nexport type StreamAccess =\n | \"public\"\n | \"authenticated\"\n | {\n readonly service: AccessLevel;\n readonly entry?: undefined;\n readonly scope?: undefined;\n readonly of?: undefined;\n }\n | {\n readonly entry: AccessLevel;\n readonly service?: AccessLevel;\n readonly scope?: undefined;\n readonly of?: undefined;\n }\n | {\n readonly scope: AccessLevel;\n readonly of: AnyContract;\n readonly service?: undefined;\n readonly entry?: undefined;\n };\n\n/** A server-to-client stream of append-only items (RFC 0003 section 12.5). */\nexport interface StreamDef<Item extends StandardSchemaV1 = StandardSchemaV1> {\n /** One item's schema. Pushed items are checked against it before they are sent. */\n readonly item: Item;\n /**\n * What a scope value is (`\"taskId\"`): the stream has one feed per scope.\n * `\"global\"`, the default, makes one feed for the whole service.\n */\n readonly scope?: string;\n /**\n * How many of the latest items the server keeps per scope, in memory on\n * each process, and sends a new subscriber first. Default 0; at most 1,000.\n */\n readonly seed?: number;\n /** Send items volatile: dropped for a client whose connection is backed up. Default `false`. */\n readonly volatile?: boolean;\n /** Who may subscribe. Without it the stream is closed. */\n readonly access?: StreamAccess;\n}\n\n/**\n * Where a channel requirement finds its row id or scope value in the parsed\n * payload: the name of a key holding a string, or a function of the payload.\n */\nexport type PayloadSelector = string | ((payload: never) => string | null | undefined);\n\n/**\n * The app room a channel requirement names: the room itself (`\"world\"`, a\n * game's one world), or a function of the parsed payload that returns it\n * (`(payload) => \\`lobby:${payload.lobbyId}\\``). Unlike a\n * {@link PayloadSelector}, a string here is the room's name, not a payload\n * key. A name starting with `qd:` or `user:` is never an app room: a literal\n * one is refused when the contract is defined, and a computed one drops the\n * message.\n */\nexport type RoomSelector = string | ((payload: never) => string | null | undefined);\n\n/**\n * What a channel message requires of the socket that sends it: a live\n * subscription (`qd:sub`) to the row of this service `entity` names, or\n * (`qd:col:sub`) to the scope of `collection` that `scope` names; or that\n * the socket is in the app room `room` names, which a method called over\n * that same socket joined with `ctx.rooms.join` (a room another socket of\n * the user joined does not count, and a reconnected socket is in none until\n * it joins again). Each is checked in memory against the sending socket's\n * own records, on the node it is connected to. A message whose payload names\n * none is dropped.\n */\nexport type ChannelRequires =\n | {\n readonly entity: PayloadSelector;\n readonly collection?: undefined;\n readonly scope?: undefined;\n readonly room?: undefined;\n }\n | {\n readonly collection: string;\n readonly scope: PayloadSelector;\n readonly entity?: undefined;\n readonly room?: undefined;\n }\n | {\n readonly room: RoomSelector;\n readonly entity?: undefined;\n readonly collection?: undefined;\n readonly scope?: undefined;\n };\n\n/** A client-to-server fire-and-forget channel (RFC 0003 section 12.5). */\nexport interface ChannelDef<Payload extends StandardSchemaV1 = StandardSchemaV1> {\n /** One message's schema. A message that fails it is dropped. */\n readonly payload: Payload;\n /** Messages one socket may send per second, sustained. Default 30. */\n readonly ratePerSecond?: number;\n /** How many messages one socket may send at once. Default twice `ratePerSecond`. */\n readonly burst?: number;\n /** What the sender must already be subscribed to, or the app room it must be in. */\n readonly requires?: ChannelRequires;\n}\n\n/** A typed custom room event, delivered as `qd:event` (RFC 0003 section 8.3). */\nexport interface EventDef<Payload extends StandardSchemaV1 = StandardSchemaV1> {\n readonly payload: Payload;\n}\n\n/** True when a stream has one feed per scope value; false for a global stream. */\nexport function isScopedStream(def: Pick<StreamDef, \"scope\">): boolean {\n return def.scope !== undefined && def.scope !== GLOBAL_STREAM;\n}\n"],"mappings":";AAUO,IAAM,2BAA2B;AAGjC,IAAM,+BAA+B;AA0DrC,SAAS,IAId,UAA0E;AAC1E,SAAO,OAAO,OAAO;AAAA,IACnB,MAAM;AAAA,IACN,OAAO,SAAS;AAAA,IAChB,OAAO,SAAS;AAAA,IAChB,OAAO,SAAS;AAAA,IAChB,GAAI,SAAS,iBAAiB,SAAY,CAAC,IAAI,EAAE,cAAc,SAAS,aAAa;AAAA,EACvF,CAAC;AACH;;;AClEO,IAAM,gBAAgB;AAGtB,IAAM,kBAAkB;AAGxB,IAAM,uBAAuB;AAqH7B,SAAS,eAAe,KAAwC;AACrE,SAAO,IAAI,UAAU,UAAa,IAAI,UAAU;AAClD;","names":[]}
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-V3KNCTXJ.js";
4
4
 
5
5
  // src/version.ts
6
- var QUICKDRAW_VERSION = "5.0.0-rc.2";
6
+ var QUICKDRAW_VERSION = "5.0.0-rc.3";
7
7
 
8
8
  // src/contract/names.ts
9
9
  function entityRoom(service, id, level) {
@@ -114,4 +114,4 @@ export {
114
114
  MAX_SUBSCRIBE_IDS,
115
115
  MAX_SCOPE_LENGTH
116
116
  };
117
- //# sourceMappingURL=chunk-SSDLYYQV.js.map
117
+ //# sourceMappingURL=chunk-5CXPEEWF.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts","../src/contract/names.ts","../src/protocol/version.ts"],"sourcesContent":["/** The published version of `@fitzzero/quickdraw-core`. Kept in step with package.json. */\nexport const QUICKDRAW_VERSION = \"5.0.0-rc.2\";\n","// Room and event names for protocol v5 (RFC 0003 sections 6, 7 and 8). Ported\n// from the 4.1 room helpers (`legacy-src/shared/types.ts:364-405`), which were\n// plain template strings so shared code could name a room without holding a\n// service instance. The 5.0 rooms live under a `qd:` prefix so they never\n// collide with an app's own rooms; the per-user room keeps its 4.1 name.\n\nimport type { AccessLevel } from \"./access\";\n\n/**\n * The room for one entity at one access tier: `qd:e:{service}:{id}@{level}`.\n * A subscriber joins the room for its own level, so a frame is built once and\n * stripped once per occupied tier.\n */\nexport function entityRoom(service: string, id: string, level: AccessLevel): string {\n return `qd:e:${service}:${id}@${level}`;\n}\n\n/** The room for one scope of a collection: `qd:c:{service}:{collection}:{scope}`. */\nexport function collectionRoom(service: string, collection: string, scope: string): string {\n return `qd:c:${service}:${collection}:${scope}`;\n}\n\n/** The room for a watched topic of a service: `qd:t:{service}:{topic}`. */\nexport function topicRoom(service: string, topic: string): string {\n return `qd:t:${service}:${topic}`;\n}\n\n/**\n * The change topic of a whole service (RFC 0003 section 11.3): every flush\n * that changes one of its rows or collection scopes signals it.\n */\nexport const SERVICE_TOPIC = \"service\";\n\n/** The change topic of one scope of a collection: `{collection}:{scope}` (RFC 0003 section 11.3). */\nexport function collectionTopic(collection: string, scope: string): string {\n return `${collection}:${scope}`;\n}\n\n/**\n * The room of a stream's feed (RFC 0003 section 12.5):\n * `qd:s:{service}:{stream}:{scope}` for one scope of a scoped stream, and\n * `qd:s:{service}:{stream}` for a global stream. Stream names hold no colon.\n */\nexport function streamRoom(service: string, stream: string, scope?: string): string {\n return scope === undefined ? `qd:s:${service}:${stream}` : `qd:s:${service}:${stream}:${scope}`;\n}\n\n/** The room every authenticated socket of a user joins: `user:{userId}`, as in 4.1. */\nexport function userRoom(userId: string): string {\n return `user:${userId}`;\n}\n\n/**\n * The prefixes of the rooms the framework manages: entity, collection, topic\n * and stream rooms (`qd:`) and user rooms (`user:`). `ctx.rooms.join` refuses\n * them; a socket enters those rooms only through their authorized paths.\n */\nexport const RESERVED_ROOM_PREFIXES: readonly string[] = Object.freeze([\"qd:\", \"user:\"]);\n\n/**\n * The reserved prefix `room` starts with (`qd:` or `user:`), or `undefined`\n * when an app room may have that name.\n */\nexport function reservedRoomPrefix(room: string): string | undefined {\n return RESERVED_ROOM_PREFIXES.find((prefix) => room.startsWith(prefix));\n}\n\n/** Events a client sends to the server (RFC 0003 section 8.2). */\nexport const CLIENT_EVENTS = Object.freeze({\n /** A method call: `{ id, s, m, i, v? }`, answered by ack. */\n call: \"qd:call\",\n /** Cancels an in-flight call: `{ id }`. */\n cancel: \"qd:cancel\",\n /** Subscribes to entities by id, with the revisions held. */\n sub: \"qd:sub\",\n /** Unsubscribes from entities. */\n unsub: \"qd:unsub\",\n /** Subscribes to a collection scope, or resumes it from a revision. */\n collectionSub: \"qd:col:sub\",\n /** Unsubscribes from a collection scope. */\n collectionUnsub: \"qd:col:unsub\",\n /** Loads collection items by id. */\n collectionItems: \"qd:col:items\",\n /** Joins a watched topic: `{ s, topic }`. */\n watch: \"qd:watch\",\n /** Leaves a watched topic. */\n unwatch: \"qd:unwatch\",\n /** Subscribes to a stream and receives its seed. */\n streamSub: \"qd:stream:sub\",\n /** Unsubscribes from a stream. */\n streamUnsub: \"qd:stream:unsub\",\n /** A fire-and-forget channel message, sent volatile: `[s, channel, payload]`. */\n channel: \"qd:ch\",\n} as const);\n\n/** Events the server sends to a client (RFC 0003 section 8.3). */\nexport const SERVER_EVENTS = Object.freeze({\n /** The handshake reply: `{ protocol, server, limits, features, userId, serviceAccess }`. */\n hello: \"qd:hello\",\n /** Entity frames: full update, patch or removal. */\n entity: \"qd:e\",\n /** Collection deltas, batched per flush. */\n collection: \"qd:c\",\n /** A watched topic changed; the client invalidates its queries. */\n changed: \"qd:changed\",\n /** Access to a subscription was revoked. */\n revoked: \"qd:revoked\",\n /** Stream items. */\n stream: \"qd:stream\",\n /** Typed custom room events declared in a contract's `events`. */\n event: \"qd:event\",\n /** Who is in an app room the socket is in: the list on joining, then each user who joins or leaves. */\n presence: \"qd:presence\",\n /** Reconnect within a jitter window. */\n rotate: \"qd:rotate\",\n /** The user's service grants changed. */\n access: \"qd:access\",\n} as const);\n\n/** The name of an event a client sends. */\nexport type ClientEventName = (typeof CLIENT_EVENTS)[keyof typeof CLIENT_EVENTS];\n\n/** The name of an event the server sends. */\nexport type ServerEventName = (typeof SERVER_EVENTS)[keyof typeof SERVER_EVENTS];\n","// The protocol version and the connection handshake (RFC 0003 section 8.1).\n// A v5 client connects with `auth: { token, qd: { protocol: 5, client } }`.\n// The server answers with `qd:hello`, or refuses a client that names another\n// protocol with `PROTOCOL_MISMATCH` in the `connect_error` data, and one whose\n// authentication failed with `UNAUTHENTICATED` there. A client that sends no\n// `auth.qd` is a 4.x client, which only the legacy shim serves (section 8.5).\n// There is no capability negotiation: the version decides.\n\nimport type { AccessLevel } from \"../contract/access\";\nimport { isRecord } from \"./guards\";\n\n/** The wire protocol this package speaks. */\nexport const PROTOCOL_VERSION = 5;\n\n/** `auth.qd`: what a v5 client sends when it connects. */\nexport interface QdHandshake {\n /** The protocol the client speaks. The server refuses any but its own. */\n readonly protocol: number;\n /** The client package's version, for logs. */\n readonly client: string;\n}\n\n/** The Socket.IO `auth` a v5 client connects with. */\nexport interface HandshakeAuth {\n /** The session token the server's `authenticate` reads. */\n readonly token?: string;\n /** The protocol the client speaks. A client without it is a 4.x client (RFC 0003 section 8.5). */\n readonly qd: QdHandshake;\n}\n\n/**\n * True when `value` has the shape of `auth.qd`, whatever protocol it names.\n * Compare `protocol` with {@link PROTOCOL_VERSION} to decide whether to serve it.\n */\nexport function isQdHandshake(value: unknown): value is QdHandshake {\n return isRecord(value) && Number.isInteger(value.protocol) && typeof value.client === \"string\";\n}\n\n/** The code in the `connect_error` data of a connection refused for its protocol. */\nexport const PROTOCOL_MISMATCH = \"PROTOCOL_MISMATCH\";\n\n/**\n * The `data` of the error a server refuses a connection with when the client\n * speaks another protocol, or none. It is not an `ErrorCode`: no call failed.\n */\nexport interface ProtocolMismatch {\n readonly code: typeof PROTOCOL_MISMATCH;\n /** The protocol the server speaks. */\n readonly expected: number;\n}\n\n/** True when a `connect_error`'s `data` says the server speaks another protocol. */\nexport function isProtocolMismatch(value: unknown): value is ProtocolMismatch {\n return isRecord(value) && value.code === PROTOCOL_MISMATCH && Number.isInteger(value.expected);\n}\n\n/**\n * The `data` of the error a server refuses a connection with when\n * authenticating it failed: the app's `authenticate` threw, so the\n * credentials are bad or could not be checked. A client tells it apart from\n * {@link ProtocolMismatch} by `code`; reconnecting needs other credentials.\n */\nexport interface AuthenticationRefused {\n readonly code: \"UNAUTHENTICATED\";\n}\n\n/** True when a `connect_error`'s `data` says authenticating the connection failed. */\nexport function isAuthenticationRefused(value: unknown): value is AuthenticationRefused {\n return isRecord(value) && value.code === \"UNAUTHENTICATED\";\n}\n\n/** The most ids one `qd:sub` may name (RFC 0003 section 6), as `qd:hello` announces it. */\nexport const MAX_SUBSCRIBE_IDS = 500;\n\n/** The longest collection scope value or change topic a frame may name, in characters. */\nexport const MAX_SCOPE_LENGTH = 256;\n\n/**\n * Each socket's lane of subscription events (`qd:sub`, `qd:col:sub`,\n * `qd:col:items`, `qd:watch`; RFC 0003 section 8.2), as `qd:hello` announces it.\n */\nexport interface HelloSubscriptionLimits {\n /** Subscription events one socket may have running at once. */\n readonly maxInFlight: number;\n /** Subscription events one socket may have waiting; past that one fails with `RATE_LIMITED`. */\n readonly maxQueued: number;\n}\n\n/** The limits a server announces in `qd:hello`, so a client can stay inside them. */\nexport interface HelloLimits {\n /** Queries one socket may have running at once; more wait in a queue (RFC 0003 section 9). */\n readonly maxInFlightQueries: number;\n /** Queries one socket may have waiting; past that a query fails with `RATE_LIMITED`. */\n readonly maxQueuedQueries: number;\n /** Ids one `qd:sub` may name (RFC 0003 section 6). */\n readonly maxSubscribeIds: number;\n /**\n * A call's default time limit in milliseconds, after which it fails with\n * `TIMEOUT`. A v5 client waits this long plus 2 s for an answer, so a slow\n * call ends with the server's `TIMEOUT` rather than its own.\n */\n readonly callTimeoutMs: number;\n /** The socket's lane of subscription events. */\n readonly subscriptions: HelloSubscriptionLimits;\n}\n\n/**\n * `qd:hello`: the server's answer to a v5 handshake, sent once per\n * connection, after the socket's listeners are in place. It says who the\n * socket acts for, so a client knows its user without another call.\n */\nexport interface HelloFrame {\n /** The protocol the server speaks. */\n readonly protocol: typeof PROTOCOL_VERSION;\n /** The server package's version. */\n readonly server: string;\n /** What a client must stay within. */\n readonly limits: HelloLimits;\n /** Names of optional server features that are on. Informational only. */\n readonly features: readonly string[];\n /** The user the socket acts for, or `null` when it is anonymous. */\n readonly userId: string | null;\n /**\n * The principal's service-wide grants when the socket connected, by\n * service name: `{ taskService: \"Admin\" }`. Empty for an anonymous socket\n * or a principal without grants. Later changes arrive as `qd:access`.\n */\n readonly serviceAccess: Readonly<Record<string, AccessLevel>>;\n}\n"],"mappings":";;;;;AACO,IAAM,oBAAoB;;;ACY1B,SAAS,WAAW,SAAiB,IAAY,OAA4B;AAClF,SAAO,QAAQ,OAAO,IAAI,EAAE,IAAI,KAAK;AACvC;AAGO,SAAS,eAAe,SAAiB,YAAoB,OAAuB;AACzF,SAAO,QAAQ,OAAO,IAAI,UAAU,IAAI,KAAK;AAC/C;AAGO,SAAS,UAAU,SAAiB,OAAuB;AAChE,SAAO,QAAQ,OAAO,IAAI,KAAK;AACjC;AAMO,IAAM,gBAAgB;AAGtB,SAAS,gBAAgB,YAAoB,OAAuB;AACzE,SAAO,GAAG,UAAU,IAAI,KAAK;AAC/B;AAOO,SAAS,WAAW,SAAiB,QAAgB,OAAwB;AAClF,SAAO,UAAU,SAAY,QAAQ,OAAO,IAAI,MAAM,KAAK,QAAQ,OAAO,IAAI,MAAM,IAAI,KAAK;AAC/F;AAGO,SAAS,SAAS,QAAwB;AAC/C,SAAO,QAAQ,MAAM;AACvB;AAOO,IAAM,yBAA4C,OAAO,OAAO,CAAC,OAAO,OAAO,CAAC;AAMhF,SAAS,mBAAmB,MAAkC;AACnE,SAAO,uBAAuB,KAAK,CAAC,WAAW,KAAK,WAAW,MAAM,CAAC;AACxE;AAGO,IAAM,gBAAgB,OAAO,OAAO;AAAA;AAAA,EAEzC,MAAM;AAAA;AAAA,EAEN,QAAQ;AAAA;AAAA,EAER,KAAK;AAAA;AAAA,EAEL,OAAO;AAAA;AAAA,EAEP,eAAe;AAAA;AAAA,EAEf,iBAAiB;AAAA;AAAA,EAEjB,iBAAiB;AAAA;AAAA,EAEjB,OAAO;AAAA;AAAA,EAEP,SAAS;AAAA;AAAA,EAET,WAAW;AAAA;AAAA,EAEX,aAAa;AAAA;AAAA,EAEb,SAAS;AACX,CAAU;AAGH,IAAM,gBAAgB,OAAO,OAAO;AAAA;AAAA,EAEzC,OAAO;AAAA;AAAA,EAEP,QAAQ;AAAA;AAAA,EAER,YAAY;AAAA;AAAA,EAEZ,SAAS;AAAA;AAAA,EAET,SAAS;AAAA;AAAA,EAET,QAAQ;AAAA;AAAA,EAER,OAAO;AAAA;AAAA,EAEP,UAAU;AAAA;AAAA,EAEV,QAAQ;AAAA;AAAA,EAER,QAAQ;AACV,CAAU;;;ACzGH,IAAM,mBAAmB;AAsBzB,SAAS,cAAc,OAAsC;AAClE,SAAO,SAAS,KAAK,KAAK,OAAO,UAAU,MAAM,QAAQ,KAAK,OAAO,MAAM,WAAW;AACxF;AAGO,IAAM,oBAAoB;AAa1B,SAAS,mBAAmB,OAA2C;AAC5E,SAAO,SAAS,KAAK,KAAK,MAAM,SAAS,qBAAqB,OAAO,UAAU,MAAM,QAAQ;AAC/F;AAaO,SAAS,wBAAwB,OAAgD;AACtF,SAAO,SAAS,KAAK,KAAK,MAAM,SAAS;AAC3C;AAGO,IAAM,oBAAoB;AAG1B,IAAM,mBAAmB;","names":[]}
1
+ {"version":3,"sources":["../src/version.ts","../src/contract/names.ts","../src/protocol/version.ts"],"sourcesContent":["/** The published version of `@fitzzero/quickdraw-core`. Kept in step with package.json. */\nexport const QUICKDRAW_VERSION = \"5.0.0-rc.3\";\n","// Room and event names for protocol v5 (RFC 0003 sections 6, 7 and 8). Ported\n// from the 4.1 room helpers (`legacy-src/shared/types.ts:364-405`), which were\n// plain template strings so shared code could name a room without holding a\n// service instance. The 5.0 rooms live under a `qd:` prefix so they never\n// collide with an app's own rooms; the per-user room keeps its 4.1 name.\n\nimport type { AccessLevel } from \"./access\";\n\n/**\n * The room for one entity at one access tier: `qd:e:{service}:{id}@{level}`.\n * A subscriber joins the room for its own level, so a frame is built once and\n * stripped once per occupied tier.\n */\nexport function entityRoom(service: string, id: string, level: AccessLevel): string {\n return `qd:e:${service}:${id}@${level}`;\n}\n\n/** The room for one scope of a collection: `qd:c:{service}:{collection}:{scope}`. */\nexport function collectionRoom(service: string, collection: string, scope: string): string {\n return `qd:c:${service}:${collection}:${scope}`;\n}\n\n/** The room for a watched topic of a service: `qd:t:{service}:{topic}`. */\nexport function topicRoom(service: string, topic: string): string {\n return `qd:t:${service}:${topic}`;\n}\n\n/**\n * The change topic of a whole service (RFC 0003 section 11.3): every flush\n * that changes one of its rows or collection scopes signals it.\n */\nexport const SERVICE_TOPIC = \"service\";\n\n/** The change topic of one scope of a collection: `{collection}:{scope}` (RFC 0003 section 11.3). */\nexport function collectionTopic(collection: string, scope: string): string {\n return `${collection}:${scope}`;\n}\n\n/**\n * The room of a stream's feed (RFC 0003 section 12.5):\n * `qd:s:{service}:{stream}:{scope}` for one scope of a scoped stream, and\n * `qd:s:{service}:{stream}` for a global stream. Stream names hold no colon.\n */\nexport function streamRoom(service: string, stream: string, scope?: string): string {\n return scope === undefined ? `qd:s:${service}:${stream}` : `qd:s:${service}:${stream}:${scope}`;\n}\n\n/** The room every authenticated socket of a user joins: `user:{userId}`, as in 4.1. */\nexport function userRoom(userId: string): string {\n return `user:${userId}`;\n}\n\n/**\n * The prefixes of the rooms the framework manages: entity, collection, topic\n * and stream rooms (`qd:`) and user rooms (`user:`). `ctx.rooms.join` refuses\n * them; a socket enters those rooms only through their authorized paths.\n */\nexport const RESERVED_ROOM_PREFIXES: readonly string[] = Object.freeze([\"qd:\", \"user:\"]);\n\n/**\n * The reserved prefix `room` starts with (`qd:` or `user:`), or `undefined`\n * when an app room may have that name.\n */\nexport function reservedRoomPrefix(room: string): string | undefined {\n return RESERVED_ROOM_PREFIXES.find((prefix) => room.startsWith(prefix));\n}\n\n/** Events a client sends to the server (RFC 0003 section 8.2). */\nexport const CLIENT_EVENTS = Object.freeze({\n /** A method call: `{ id, s, m, i, v? }`, answered by ack. */\n call: \"qd:call\",\n /** Cancels an in-flight call: `{ id }`. */\n cancel: \"qd:cancel\",\n /** Subscribes to entities by id, with the revisions held. */\n sub: \"qd:sub\",\n /** Unsubscribes from entities. */\n unsub: \"qd:unsub\",\n /** Subscribes to a collection scope, or resumes it from a revision. */\n collectionSub: \"qd:col:sub\",\n /** Unsubscribes from a collection scope. */\n collectionUnsub: \"qd:col:unsub\",\n /** Loads collection items by id. */\n collectionItems: \"qd:col:items\",\n /** Joins a watched topic: `{ s, topic }`. */\n watch: \"qd:watch\",\n /** Leaves a watched topic. */\n unwatch: \"qd:unwatch\",\n /** Subscribes to a stream and receives its seed. */\n streamSub: \"qd:stream:sub\",\n /** Unsubscribes from a stream. */\n streamUnsub: \"qd:stream:unsub\",\n /** A fire-and-forget channel message, sent volatile: `[s, channel, payload]`. */\n channel: \"qd:ch\",\n} as const);\n\n/** Events the server sends to a client (RFC 0003 section 8.3). */\nexport const SERVER_EVENTS = Object.freeze({\n /** The handshake reply: `{ protocol, server, limits, features, userId, serviceAccess }`. */\n hello: \"qd:hello\",\n /** Entity frames: full update, patch or removal. */\n entity: \"qd:e\",\n /** Collection deltas, batched per flush. */\n collection: \"qd:c\",\n /** A watched topic changed; the client invalidates its queries. */\n changed: \"qd:changed\",\n /** Access to a subscription was revoked. */\n revoked: \"qd:revoked\",\n /** Stream items. */\n stream: \"qd:stream\",\n /** Typed custom room events declared in a contract's `events`. */\n event: \"qd:event\",\n /** Who is in an app room the socket is in: the list on joining, then each user who joins or leaves. */\n presence: \"qd:presence\",\n /** Reconnect within a jitter window. */\n rotate: \"qd:rotate\",\n /** The user's service grants changed. */\n access: \"qd:access\",\n} as const);\n\n/** The name of an event a client sends. */\nexport type ClientEventName = (typeof CLIENT_EVENTS)[keyof typeof CLIENT_EVENTS];\n\n/** The name of an event the server sends. */\nexport type ServerEventName = (typeof SERVER_EVENTS)[keyof typeof SERVER_EVENTS];\n","// The protocol version and the connection handshake (RFC 0003 section 8.1).\n// A v5 client connects with `auth: { token, qd: { protocol: 5, client } }`.\n// The server answers with `qd:hello`, or refuses a client that names another\n// protocol with `PROTOCOL_MISMATCH` in the `connect_error` data, and one whose\n// authentication failed with `UNAUTHENTICATED` there. A client that sends no\n// `auth.qd` is a 4.x client, which only the legacy shim serves (section 8.5).\n// There is no capability negotiation: the version decides.\n\nimport type { AccessLevel } from \"../contract/access\";\nimport { isRecord } from \"./guards\";\n\n/** The wire protocol this package speaks. */\nexport const PROTOCOL_VERSION = 5;\n\n/** `auth.qd`: what a v5 client sends when it connects. */\nexport interface QdHandshake {\n /** The protocol the client speaks. The server refuses any but its own. */\n readonly protocol: number;\n /** The client package's version, for logs. */\n readonly client: string;\n}\n\n/** The Socket.IO `auth` a v5 client connects with. */\nexport interface HandshakeAuth {\n /** The session token the server's `authenticate` reads. */\n readonly token?: string;\n /** The protocol the client speaks. A client without it is a 4.x client (RFC 0003 section 8.5). */\n readonly qd: QdHandshake;\n}\n\n/**\n * True when `value` has the shape of `auth.qd`, whatever protocol it names.\n * Compare `protocol` with {@link PROTOCOL_VERSION} to decide whether to serve it.\n */\nexport function isQdHandshake(value: unknown): value is QdHandshake {\n return isRecord(value) && Number.isInteger(value.protocol) && typeof value.client === \"string\";\n}\n\n/** The code in the `connect_error` data of a connection refused for its protocol. */\nexport const PROTOCOL_MISMATCH = \"PROTOCOL_MISMATCH\";\n\n/**\n * The `data` of the error a server refuses a connection with when the client\n * speaks another protocol, or none. It is not an `ErrorCode`: no call failed.\n */\nexport interface ProtocolMismatch {\n readonly code: typeof PROTOCOL_MISMATCH;\n /** The protocol the server speaks. */\n readonly expected: number;\n}\n\n/** True when a `connect_error`'s `data` says the server speaks another protocol. */\nexport function isProtocolMismatch(value: unknown): value is ProtocolMismatch {\n return isRecord(value) && value.code === PROTOCOL_MISMATCH && Number.isInteger(value.expected);\n}\n\n/**\n * The `data` of the error a server refuses a connection with when\n * authenticating it failed: the app's `authenticate` threw, so the\n * credentials are bad or could not be checked. A client tells it apart from\n * {@link ProtocolMismatch} by `code`; reconnecting needs other credentials.\n */\nexport interface AuthenticationRefused {\n readonly code: \"UNAUTHENTICATED\";\n}\n\n/** True when a `connect_error`'s `data` says authenticating the connection failed. */\nexport function isAuthenticationRefused(value: unknown): value is AuthenticationRefused {\n return isRecord(value) && value.code === \"UNAUTHENTICATED\";\n}\n\n/** The most ids one `qd:sub` may name (RFC 0003 section 6), as `qd:hello` announces it. */\nexport const MAX_SUBSCRIBE_IDS = 500;\n\n/** The longest collection scope value or change topic a frame may name, in characters. */\nexport const MAX_SCOPE_LENGTH = 256;\n\n/**\n * Each socket's lane of subscription events (`qd:sub`, `qd:col:sub`,\n * `qd:col:items`, `qd:watch`; RFC 0003 section 8.2), as `qd:hello` announces it.\n */\nexport interface HelloSubscriptionLimits {\n /** Subscription events one socket may have running at once. */\n readonly maxInFlight: number;\n /** Subscription events one socket may have waiting; past that one fails with `RATE_LIMITED`. */\n readonly maxQueued: number;\n}\n\n/** The limits a server announces in `qd:hello`, so a client can stay inside them. */\nexport interface HelloLimits {\n /** Queries one socket may have running at once; more wait in a queue (RFC 0003 section 9). */\n readonly maxInFlightQueries: number;\n /** Queries one socket may have waiting; past that a query fails with `RATE_LIMITED`. */\n readonly maxQueuedQueries: number;\n /** Ids one `qd:sub` may name (RFC 0003 section 6). */\n readonly maxSubscribeIds: number;\n /**\n * A call's default time limit in milliseconds, after which it fails with\n * `TIMEOUT`. A v5 client waits this long plus 2 s for an answer, so a slow\n * call ends with the server's `TIMEOUT` rather than its own.\n */\n readonly callTimeoutMs: number;\n /** The socket's lane of subscription events. */\n readonly subscriptions: HelloSubscriptionLimits;\n}\n\n/**\n * `qd:hello`: the server's answer to a v5 handshake, sent once per\n * connection, after the socket's listeners are in place. It says who the\n * socket acts for, so a client knows its user without another call.\n */\nexport interface HelloFrame {\n /** The protocol the server speaks. */\n readonly protocol: typeof PROTOCOL_VERSION;\n /** The server package's version. */\n readonly server: string;\n /** What a client must stay within. */\n readonly limits: HelloLimits;\n /** Names of optional server features that are on. Informational only. */\n readonly features: readonly string[];\n /** The user the socket acts for, or `null` when it is anonymous. */\n readonly userId: string | null;\n /**\n * The principal's service-wide grants when the socket connected, by\n * service name: `{ taskService: \"Admin\" }`. Empty for an anonymous socket\n * or a principal without grants. Later changes arrive as `qd:access`.\n */\n readonly serviceAccess: Readonly<Record<string, AccessLevel>>;\n}\n"],"mappings":";;;;;AACO,IAAM,oBAAoB;;;ACY1B,SAAS,WAAW,SAAiB,IAAY,OAA4B;AAClF,SAAO,QAAQ,OAAO,IAAI,EAAE,IAAI,KAAK;AACvC;AAGO,SAAS,eAAe,SAAiB,YAAoB,OAAuB;AACzF,SAAO,QAAQ,OAAO,IAAI,UAAU,IAAI,KAAK;AAC/C;AAGO,SAAS,UAAU,SAAiB,OAAuB;AAChE,SAAO,QAAQ,OAAO,IAAI,KAAK;AACjC;AAMO,IAAM,gBAAgB;AAGtB,SAAS,gBAAgB,YAAoB,OAAuB;AACzE,SAAO,GAAG,UAAU,IAAI,KAAK;AAC/B;AAOO,SAAS,WAAW,SAAiB,QAAgB,OAAwB;AAClF,SAAO,UAAU,SAAY,QAAQ,OAAO,IAAI,MAAM,KAAK,QAAQ,OAAO,IAAI,MAAM,IAAI,KAAK;AAC/F;AAGO,SAAS,SAAS,QAAwB;AAC/C,SAAO,QAAQ,MAAM;AACvB;AAOO,IAAM,yBAA4C,OAAO,OAAO,CAAC,OAAO,OAAO,CAAC;AAMhF,SAAS,mBAAmB,MAAkC;AACnE,SAAO,uBAAuB,KAAK,CAAC,WAAW,KAAK,WAAW,MAAM,CAAC;AACxE;AAGO,IAAM,gBAAgB,OAAO,OAAO;AAAA;AAAA,EAEzC,MAAM;AAAA;AAAA,EAEN,QAAQ;AAAA;AAAA,EAER,KAAK;AAAA;AAAA,EAEL,OAAO;AAAA;AAAA,EAEP,eAAe;AAAA;AAAA,EAEf,iBAAiB;AAAA;AAAA,EAEjB,iBAAiB;AAAA;AAAA,EAEjB,OAAO;AAAA;AAAA,EAEP,SAAS;AAAA;AAAA,EAET,WAAW;AAAA;AAAA,EAEX,aAAa;AAAA;AAAA,EAEb,SAAS;AACX,CAAU;AAGH,IAAM,gBAAgB,OAAO,OAAO;AAAA;AAAA,EAEzC,OAAO;AAAA;AAAA,EAEP,QAAQ;AAAA;AAAA,EAER,YAAY;AAAA;AAAA,EAEZ,SAAS;AAAA;AAAA,EAET,SAAS;AAAA;AAAA,EAET,QAAQ;AAAA;AAAA,EAER,OAAO;AAAA;AAAA,EAEP,UAAU;AAAA;AAAA,EAEV,QAAQ;AAAA;AAAA,EAER,QAAQ;AACV,CAAU;;;ACzGH,IAAM,mBAAmB;AAsBzB,SAAS,cAAc,OAAsC;AAClE,SAAO,SAAS,KAAK,KAAK,OAAO,UAAU,MAAM,QAAQ,KAAK,OAAO,MAAM,WAAW;AACxF;AAGO,IAAM,oBAAoB;AAa1B,SAAS,mBAAmB,OAA2C;AAC5E,SAAO,SAAS,KAAK,KAAK,MAAM,SAAS,qBAAqB,OAAO,UAAU,MAAM,QAAQ;AAC/F;AAaO,SAAS,wBAAwB,OAAgD;AACtF,SAAO,SAAS,KAAK,KAAK,MAAM,SAAS;AAC3C;AAGO,IAAM,oBAAoB;AAG1B,IAAM,mBAAmB;","names":[]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ANY_FIELD,
3
3
  modelKey
4
- } from "./chunk-EI57GEMD.js";
4
+ } from "./chunk-H23KEFBA.js";
5
5
  import {
6
6
  PreEncoded,
7
7
  createJsonParser
@@ -475,4 +475,4 @@ export {
475
475
  transportCookieNaming,
476
476
  tokenOf
477
477
  };
478
- //# sourceMappingURL=chunk-5ADGYGC5.js.map
478
+ //# sourceMappingURL=chunk-B4IOFSM6.js.map