@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.
- package/README.md +136 -15
- package/dist/{admin-Bl71J4IG.d.ts → admin-BiVNyWdI.d.ts} +1 -1
- package/dist/{auth-DyCIKmTQ.d.ts → auth-DlFtykbn.d.ts} +2 -2
- package/dist/{caller-Cx5wuao-.d.ts → caller-D4cgj-Xl.d.ts} +120 -14
- package/dist/{chunk-UJQJM7EG.js → chunk-3ONXZP2X.js} +3 -2
- package/dist/chunk-3ONXZP2X.js.map +1 -0
- package/dist/{chunk-SSDLYYQV.js → chunk-5CXPEEWF.js} +2 -2
- package/dist/{chunk-SSDLYYQV.js.map → chunk-5CXPEEWF.js.map} +1 -1
- package/dist/{chunk-5ADGYGC5.js → chunk-B4IOFSM6.js} +2 -2
- package/dist/{chunk-NRM4XAWR.js → chunk-CNECM3VC.js} +2 -2
- package/dist/{chunk-5EZNDES3.js → chunk-CWPBMY7P.js} +2 -2
- package/dist/{chunk-5EZNDES3.js.map → chunk-CWPBMY7P.js.map} +1 -1
- package/dist/{chunk-GXLJA7AH.js → chunk-DEGCLMTU.js} +6 -5
- package/dist/chunk-DEGCLMTU.js.map +1 -0
- package/dist/{chunk-U32GOJRO.js → chunk-GBM7QP5Z.js} +3 -3
- package/dist/chunk-GBM7QP5Z.js.map +1 -0
- package/dist/{chunk-EI57GEMD.js → chunk-H23KEFBA.js} +1 -1
- package/dist/{chunk-EI57GEMD.js.map → chunk-H23KEFBA.js.map} +1 -1
- package/dist/{chunk-7E7STZEO.js → chunk-HVJ4KIYX.js} +1 -1
- package/dist/{chunk-7E7STZEO.js.map → chunk-HVJ4KIYX.js.map} +1 -1
- package/dist/{chunk-DXTJNJM5.js → chunk-ISNQKSHQ.js} +3 -3
- package/dist/{chunk-I2OMV4AD.js → chunk-TNTZBUQX.js} +224 -60
- package/dist/chunk-TNTZBUQX.js.map +1 -0
- package/dist/cli/quickdraw-docs.js +3 -2
- package/dist/cli/quickdraw-docs.js.map +1 -1
- package/dist/client/index.d.ts +4 -4
- package/dist/client/index.js +3 -3
- package/dist/{clientTypes-B62bWHua.d.ts → clientTypes-P_VIjnyz.d.ts} +2 -2
- package/dist/{createServer-P61KBWro.d.ts → createServer-BwhO8YlA.d.ts} +35 -6
- package/dist/{createTestApp-BBm3TakW.d.ts → createTestApp-CBESgORq.d.ts} +10 -6
- package/dist/{envelope-CoiZY9KU.d.ts → envelope-CKQEC7e-.d.ts} +6 -4
- package/dist/{http-89lENH2D.d.ts → http-B5hNMuVY.d.ts} +17 -10
- package/dist/index.d.ts +5 -5
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/{infer-Cwq49ME5.d.ts → infer-Dlplloxi.d.ts} +32 -3
- package/dist/{metrics-Ny1dFbyY.d.ts → metrics-BBUPqcsT.d.ts} +2 -2
- package/dist/prisma/index.d.ts +4 -4
- package/dist/prisma/index.js +3 -3
- package/dist/server/auth/index.d.ts +98 -12
- package/dist/server/auth/index.js +99 -12
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/index.d.ts +65 -27
- package/dist/server/index.js +90 -21
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/index.d.ts +22 -11
- package/dist/server/mcp/index.js +11 -6
- package/dist/server/mcp/index.js.map +1 -1
- package/dist/server/otel.d.ts +3 -3
- package/dist/{sharing-BHyufgVl.d.ts → sharing-RrW03F9j.d.ts} +2 -2
- package/dist/testing/client.d.ts +11 -11
- package/dist/testing/client.js +3 -3
- package/dist/testing/index.d.ts +29 -15
- package/dist/testing/index.js +28 -10
- package/dist/testing/index.js.map +1 -1
- package/dist/{types-BDmssndG.d.ts → types-B3N6xKkl.d.ts} +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-GXLJA7AH.js.map +0 -1
- package/dist/chunk-I2OMV4AD.js.map +0 -1
- package/dist/chunk-U32GOJRO.js.map +0 -1
- package/dist/chunk-UJQJM7EG.js.map +0 -1
- /package/dist/{chunk-5ADGYGC5.js.map → chunk-B4IOFSM6.js.map} +0 -0
- /package/dist/{chunk-NRM4XAWR.js.map → chunk-CNECM3VC.js.map} +0 -0
- /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(...)
|
|
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,
|
|
834
|
-
map: (row: { id: string; title: string; status: string;
|
|
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.
|
|
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,
|
|
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.
|
|
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: {
|
|
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
|
|
1943
|
-
sockets get `qd:presence` frames: the list on
|
|
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
|
-
|
|
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-
|
|
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-
|
|
4
|
-
import { P as Principal, M as MaybePromise } from './types-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
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
|
-
|
|
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 {
|
|
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-
|
|
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.
|
|
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-
|
|
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-
|
|
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-
|
|
478
|
+
//# sourceMappingURL=chunk-B4IOFSM6.js.map
|