@fitzzero/quickdraw-core 5.1.2 → 5.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +793 -57
- package/dist/{admin-DWWrT1h6.d.ts → admin-CS7JB_UH.d.ts} +2 -2
- package/dist/{adminSchemas-D4bFKsrb.d.ts → adminSchemas-29cGgrg5.d.ts} +1 -1
- package/dist/{auth-DXiSJZaf.d.ts → auth-DtdYjrZ9.d.ts} +11 -7
- package/dist/{caller-BT-Mv4bD.d.ts → caller-BTwGl7vm.d.ts} +183 -15
- package/dist/chunk-2FDSKLBO.js +82 -0
- package/dist/chunk-2FDSKLBO.js.map +1 -0
- package/dist/{chunk-GSIPUNJL.js → chunk-64I6MYBC.js} +139 -792
- package/dist/chunk-64I6MYBC.js.map +1 -0
- package/dist/chunk-7473HK24.js +17 -0
- package/dist/chunk-7473HK24.js.map +1 -0
- package/dist/chunk-7Y5ROPXL.js +33 -0
- package/dist/chunk-7Y5ROPXL.js.map +1 -0
- package/dist/chunk-A2RBQD66.js +7 -0
- package/dist/chunk-A2RBQD66.js.map +1 -0
- package/dist/chunk-ASAMUGHG.js +651 -0
- package/dist/chunk-ASAMUGHG.js.map +1 -0
- package/dist/{chunk-B2P7AZ25.js → chunk-CYE4UBOB.js} +6 -4
- package/dist/chunk-CYE4UBOB.js.map +1 -0
- package/dist/chunk-DNP65TD5.js +832 -0
- package/dist/chunk-DNP65TD5.js.map +1 -0
- package/dist/{chunk-5CGO426C.js → chunk-GYLWEJHW.js} +5 -1
- package/dist/chunk-GYLWEJHW.js.map +1 -0
- package/dist/{chunk-AONAFOYU.js → chunk-I3JCFS7H.js} +25 -13
- package/dist/chunk-I3JCFS7H.js.map +1 -0
- package/dist/{chunk-X5CWPX7H.js → chunk-IIQ3WMJK.js} +13 -2
- package/dist/chunk-IIQ3WMJK.js.map +1 -0
- package/dist/{chunk-6K5BY47X.js → chunk-JVTONQFI.js} +1277 -277
- package/dist/chunk-JVTONQFI.js.map +1 -0
- package/dist/{chunk-RL4VKLBX.js → chunk-KKQANRLG.js} +2 -2
- package/dist/{chunk-RL4VKLBX.js.map → chunk-KKQANRLG.js.map} +1 -1
- package/dist/{chunk-GKH3LJ7N.js → chunk-MPQAT55F.js} +38 -205
- package/dist/chunk-MPQAT55F.js.map +1 -0
- package/dist/chunk-NX2V6SIL.js +202 -0
- package/dist/chunk-NX2V6SIL.js.map +1 -0
- package/dist/{chunk-7QCUX4TK.js → chunk-NZHA4TZD.js} +72 -175
- package/dist/chunk-NZHA4TZD.js.map +1 -0
- package/dist/{chunk-MBH6IDSM.js → chunk-OCZJ4EYV.js} +40 -17
- package/dist/chunk-OCZJ4EYV.js.map +1 -0
- package/dist/{chunk-RJ5I2VO3.js → chunk-PHCURHC7.js} +6 -6
- package/dist/chunk-PHCURHC7.js.map +1 -0
- package/dist/{chunk-4YO5H5ON.js → chunk-Q4OGQM5I.js} +4 -10
- package/dist/chunk-Q4OGQM5I.js.map +1 -0
- package/dist/chunk-QSU47XEX.js +13 -0
- package/dist/chunk-QSU47XEX.js.map +1 -0
- package/dist/{chunk-LCC7HNFP.js → chunk-RZXJTDH5.js} +2 -2
- package/dist/{chunk-Z2LZ7IZ7.js → chunk-SN3OZVEQ.js} +5 -31
- package/dist/chunk-SN3OZVEQ.js.map +1 -0
- package/dist/{chunk-Z5ILULF2.js → chunk-SQ5VUG77.js} +4 -3
- package/dist/{chunk-Z5ILULF2.js.map → chunk-SQ5VUG77.js.map} +1 -1
- package/dist/chunk-STH5DLQP.js +1192 -0
- package/dist/chunk-STH5DLQP.js.map +1 -0
- package/dist/{chunk-NMXYENH4.js → chunk-VZFJTKIW.js} +6 -6
- package/dist/{chunk-753VJIUP.js → chunk-X4ZU4QH5.js} +43 -9
- package/dist/chunk-X4ZU4QH5.js.map +1 -0
- package/dist/chunk-XD7SRDUY.js +188 -0
- package/dist/chunk-XD7SRDUY.js.map +1 -0
- package/dist/chunk-Z7I6SONO.js +825 -0
- package/dist/chunk-Z7I6SONO.js.map +1 -0
- package/dist/cli/quickdraw-docs.js +131 -113
- package/dist/cli/quickdraw-docs.js.map +1 -1
- package/dist/client/core.d.ts +387 -0
- package/dist/client/core.js +109 -0
- package/dist/client/core.js.map +1 -0
- package/dist/client/index.d.ts +30 -193
- package/dist/client/index.js +56 -217
- package/dist/client/index.js.map +1 -1
- package/dist/{clientTypes-hWcPUVS9.d.ts → clientTypes-CUUNJwWs.d.ts} +3 -3
- package/dist/{connection-CgijdGpT.d.ts → connection-2EeQ_Et0.d.ts} +16 -9
- package/dist/{createServer-Cp-Wk500.d.ts → createServer-Bx5HOcYK.d.ts} +37 -9
- package/dist/{createTestApp-BB_PkqQ7.d.ts → createTestApp-DAMANT79.d.ts} +36 -9
- package/dist/{envelope-DzWyIXjj.d.ts → envelope-BMBaxppV.d.ts} +200 -8
- package/dist/{http-DotQ5bj8.d.ts → http-YzLAar3w.d.ts} +15 -50
- package/dist/index.d.ts +7 -6
- package/dist/index.js +61 -28
- package/dist/index.js.map +1 -1
- package/dist/{infer-BmoZsL9a.d.ts → infer-Br62cnyW.d.ts} +39 -1
- package/dist/{metrics-DW80Exve.d.ts → metrics-CTVpw0gu.d.ts} +2 -2
- package/dist/parser.js +2 -1
- package/dist/prisma/index.d.ts +13 -6
- package/dist/prisma/index.js +155 -9
- package/dist/prisma/index.js.map +1 -1
- package/dist/remote-N7OUAGOW.js +15 -0
- package/dist/remote-N7OUAGOW.js.map +1 -0
- package/dist/server/auth/index.d.ts +3 -3
- package/dist/server/auth/index.js +5 -3
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/index.d.ts +140 -36
- package/dist/server/index.js +162 -55
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/index.d.ts +34 -18
- package/dist/server/mcp/index.js +29 -12
- package/dist/server/mcp/index.js.map +1 -1
- package/dist/server/otel.d.ts +3 -3
- package/dist/{sharing-UDLn1049.d.ts → sharing-BrsmlDv_.d.ts} +3 -3
- package/dist/testing/client.d.ts +13 -13
- package/dist/testing/client.js +17 -10
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts +49 -12
- package/dist/testing/index.js +102 -19
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock.d.ts +5 -5
- package/dist/testing/mock.js +15 -9
- package/dist/tracked-BR5b2O-U.d.ts +43 -0
- package/dist/{types-CYliTP65.d.ts → types-CLRCQGnO.d.ts} +4 -3
- package/dist/types-tMagHbjy.d.ts +151 -0
- package/dist/utils/index.d.ts +16 -4
- package/dist/utils/index.js +6 -3
- package/package.json +5 -1
- package/dist/chunk-4YO5H5ON.js.map +0 -1
- package/dist/chunk-5CGO426C.js.map +0 -1
- package/dist/chunk-6K5BY47X.js.map +0 -1
- package/dist/chunk-753VJIUP.js.map +0 -1
- package/dist/chunk-7QCUX4TK.js.map +0 -1
- package/dist/chunk-AONAFOYU.js.map +0 -1
- package/dist/chunk-B2P7AZ25.js.map +0 -1
- package/dist/chunk-GKH3LJ7N.js.map +0 -1
- package/dist/chunk-GSIPUNJL.js.map +0 -1
- package/dist/chunk-MBH6IDSM.js.map +0 -1
- package/dist/chunk-RJ5I2VO3.js.map +0 -1
- package/dist/chunk-X5CWPX7H.js.map +0 -1
- package/dist/chunk-Z2LZ7IZ7.js.map +0 -1
- /package/dist/{chunk-LCC7HNFP.js.map → chunk-RZXJTDH5.js.map} +0 -0
- /package/dist/{chunk-NMXYENH4.js.map → chunk-VZFJTKIW.js.map} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isName,
|
|
3
3
|
isRecord
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-QSU47XEX.js";
|
|
5
5
|
|
|
6
6
|
// src/protocol/envelope.ts
|
|
7
7
|
function isCallId(value) {
|
|
@@ -21,4 +21,4 @@ export {
|
|
|
21
21
|
isCallEnvelope,
|
|
22
22
|
isCancel
|
|
23
23
|
};
|
|
24
|
-
//# sourceMappingURL=chunk-
|
|
24
|
+
//# sourceMappingURL=chunk-KKQANRLG.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/protocol/envelope.ts"],"sourcesContent":["// Protocol v5 frames (RFC 0003 sections 8.2 and 8.3): what each `qd:*` event\n// carries and what its acknowledgement receives. `ClientToServerEvents` and\n// `ServerToClientEvents` key every frame by its event name from\n// `contract/names.ts`, in the event-map form Socket.IO's `Server` and the\n// client's `Socket` take as type parameters.\n//\n// Section 8.2 spells out the `qd:call`, `qd:cancel`, `qd:watch` and `qd:ch`\n// payloads, and sections 6, 7, 11.3 and 12.5 describe the rest. Fields the RFC\n// does not name follow its style: `s` service, `m` method, `c` collection,\n// `i` input, `d` data, `e` error, `v` version, `rev` revision.\n//\n// 4.1 used one event per service method and a `{ success, data }` reply\n// (4.1 `src/shared/types.ts:88-90`). Here every call travels as one\n// `qd:call` envelope and is answered `{ ok: true, d }` or `{ ok: false, e }`.\n//\n// Runtime guards cover the frames the dispatcher reads from an untrusted\n// client (`qd:call`, `qd:cancel`). They are written by hand so the package\n// root stays free of schema libraries.\n//\n// One rule for every frame, both ways: a receiver ignores the object fields\n// it does not know and the array elements after the last one it reads, and\n// a later revision of protocol 5 may only add fields to objects and append\n// elements to arrays (`qd:event`, `qd:stream`, `qd:ch`), never remove,\n// rename, retype or reorder what is here. So a client that reads only what\n// it knows keeps working against a newer server, and a server against a\n// newer client. Anything else needs a new protocol number (`qd:hello`'s\n// `protocol`), which the handshake refuses with `PROTOCOL_MISMATCH`.\n\nimport type { AccessLevel } from \"../contract/access\";\nimport type { CLIENT_EVENTS, SERVER_EVENTS } from \"../contract/names\";\nimport type { WireError } from \"./errors\";\nimport { isName, isRecord } from \"./guards\";\nimport type { HelloFrame } from \"./version\";\n\n/**\n * A revision: a whole number of microseconds since the epoch that every\n * flush raises (RFC 0003 section 5.3), `max(Date.now() * 1000, last + 1)` on\n * one process and the shared counter's next value (Valkey's clock in\n * microseconds) behind a cluster; a safe integer, about 1.8e15. Compare\n * revisions as numbers; do not read them as times.\n */\nexport type Revision = number;\n\n/**\n * The version of a result the caller holds, sent back as a call's `v` so the\n * server can answer \"not modified\" (RFC 0003 section 9, step 5): a row's\n * revision, or what the method's `version(input, ctx)` returns. Opaque to the\n * client.\n */\nexport type Version = number | string;\n\n/** A call's number among its socket's calls in flight: a non-negative safe integer. */\nexport type CallId = number;\n\n/** An acknowledgement that succeeded with nothing to return. */\nexport interface Ok {\n readonly ok: true;\n}\n\n/** An acknowledgement that failed, and why. */\nexport interface Failure {\n readonly ok: false;\n readonly e: WireError;\n}\n\n// ---------------------------------------------------------------------------\n// Calls (section 8.2)\n// ---------------------------------------------------------------------------\n\n/** `qd:call`: one method call, answered through the acknowledgement. */\nexport interface CallEnvelope {\n /** Names the call for `qd:cancel`; unique among the socket's calls in flight. */\n readonly id: CallId;\n /** The service name. */\n readonly s: string;\n /** The method name. */\n readonly m: string;\n /** The input, not yet validated. Absent when the input is `undefined`. */\n readonly i?: unknown;\n /** The version of this call's result the caller already holds. */\n readonly v?: Version;\n}\n\n/** A call that returned data, with the version of that data when the method has one. */\nexport interface CallSuccess<Output = unknown> {\n readonly ok: true;\n readonly d: Output;\n readonly v?: Version;\n readonly nm?: undefined;\n}\n\n/** A call whose result is still the version the caller sent: keep the cached copy. */\nexport interface CallNotModified {\n readonly ok: true;\n readonly nm: true;\n readonly v: Version;\n}\n\n/** The acknowledgement of `qd:call`. */\nexport type CallReply<Output = unknown> = CallSuccess<Output> | CallNotModified | Failure;\n\n/** `qd:cancel`: abort a call in flight. The call's acknowledgement still arrives. */\nexport interface CancelFrame {\n readonly id: CallId;\n}\n\n// ---------------------------------------------------------------------------\n// Entity subscriptions (section 6)\n// ---------------------------------------------------------------------------\n\n/** `qd:sub`: subscribe to rows of one service, sending the revision held for each. */\nexport interface EntitySubscribe {\n readonly s: string;\n readonly ids: readonly string[];\n /** The revision held for each id, by position in `ids`; `null` where none is cached. */\n readonly revs?: readonly (Revision | null)[];\n}\n\n/** One id's row, as `qd:sub` answers it. */\nexport interface EntityRow<Row = unknown> {\n readonly ok: true;\n readonly d: Row;\n readonly rev: Revision;\n readonly nm?: undefined;\n}\n\n/** The revision the client sent for this id is current. */\nexport interface EntityNotModified {\n readonly ok: true;\n readonly nm: true;\n readonly rev: Revision;\n}\n\n/** One id's answer to `qd:sub`: the row, \"not modified\", or why it was refused. */\nexport type EntityResult<Row = unknown> = EntityRow<Row> | EntityNotModified | Failure;\n\n/** The acknowledgement of `qd:sub`: one result per requested id, in request order. */\nexport type EntitySubscribeReply<Row = unknown> =\n | { readonly ok: true; readonly r: readonly EntityResult<Row>[] }\n | Failure;\n\n/** `qd:unsub`: stop receiving frames for these rows. */\nexport interface EntityUnsubscribe {\n readonly s: string;\n readonly ids: readonly string[];\n}\n\n/** `qd:e` `u`: the full row. Replace the cached row when `rev` is newer. */\nexport interface EntityUpdate<Row = unknown> {\n readonly t: \"u\";\n readonly s: string;\n readonly id: string;\n readonly rev: Revision;\n readonly d: Row;\n}\n\n/** `qd:e` `p`: changed fields only. Merge when `rev` is newer; fetch the row when it is not cached. */\nexport interface EntityPatch<Row = unknown> {\n readonly t: \"p\";\n readonly s: string;\n readonly id: string;\n readonly rev: Revision;\n readonly d: Partial<Row>;\n}\n\n/** `qd:e` `r`: the row is gone. Keep a tombstone until a newer `u`. */\nexport interface EntityRemove {\n readonly t: \"r\";\n readonly s: string;\n readonly id: string;\n readonly rev: Revision;\n}\n\n/** `qd:e`: a change to one row, sent to the room of each access tier that has subscribers. */\nexport type EntityFrame<Row = unknown> = EntityUpdate<Row> | EntityPatch<Row> | EntityRemove;\n\n// ---------------------------------------------------------------------------\n// Collections (section 7)\n// ---------------------------------------------------------------------------\n\n/** One scope of one collection of a service. */\nexport interface CollectionScopeRef {\n readonly s: string;\n readonly c: string;\n readonly scope: string;\n}\n\n/** `qd:col:sub`: the first page of a scope, a later page by `cursor`, or a resume from `since`. */\nexport interface CollectionSubscribe extends CollectionScopeRef {\n /** The revision the client holds the scope at; the server resumes from it when it can. */\n readonly since?: Revision;\n readonly limit?: number;\n readonly cursor?: string;\n}\n\n/**\n * A scope member in the collection's index (RFC 0003 section 7.4): its id,\n * its revision, then its index field values in the order `index` declares.\n * The revision is the time in the service's `versionColumn` when it declares\n * one (in microseconds, as revisions are), else the revision the snapshot\n * (or, in an `added` delta, the flush) was read at.\n */\nexport type WireIndexRow = readonly [id: string, rev: Revision, ...fields: unknown[]];\n\n/** A page of a scope: the answer to `qd:col:sub` without `since`, or when `since` is too old. */\nexport interface CollectionSnapshot<Item = unknown> {\n readonly ok: true;\n readonly resumed?: undefined;\n /** The revision the page was read at; resume from it with `since`. */\n readonly rev: Revision;\n readonly items: readonly Item[];\n /** How many members the scope has. */\n readonly total: number;\n /** The next page's cursor; `null` on the last page. */\n readonly cursor: string | null;\n /** The page size used: the request's, or `maxLimit` when the request asked for more. */\n readonly limit: number;\n /** Present when the requested limit was above `maxLimit` and was lowered to it. */\n readonly clamped?: true;\n /**\n * One row per member in `order`, on the first page of a collection that\n * declares `index` (not on a page read with `cursor`). Absent when the\n * scope has more members than the index holds: see `indexTruncated`.\n */\n readonly index?: readonly WireIndexRow[];\n /** Present instead of `index` when the scope has more members than the index holds (50,000). */\n readonly indexTruncated?: true;\n}\n\n/** The answer to `qd:col:sub` when the server still holds every change since `since`. */\nexport interface CollectionResumed<Item = unknown> {\n readonly ok: true;\n readonly resumed: true;\n /** The scope's revision after these deltas. */\n readonly rev: Revision;\n readonly deltas: readonly CollectionDelta<Item>[];\n}\n\n/** The acknowledgement of `qd:col:sub`. */\nexport type CollectionSubscribeReply<Item = unknown> =\n | CollectionSnapshot<Item>\n | CollectionResumed<Item>\n | Failure;\n\n/** `qd:col:items`: load full items of a scope by id. */\nexport interface CollectionItemsRequest extends CollectionScopeRef {\n readonly ids: readonly string[];\n}\n\n/**\n * The acknowledgement of `qd:col:items`: the items found, in request order,\n * and the revision they were read at, taken before the read as a\n * snapshot's is: a client drops an item older than what a delta brought it.\n */\nexport type CollectionItemsReply<Item = unknown> =\n | { readonly ok: true; readonly rev: Revision; readonly items: readonly Item[] }\n | Failure;\n\n/**\n * One change to a scope (RFC 0003 section 7.2). In a collection that declares\n * `index`, `added` carries the member's index row, built from the same row as\n * its item, and `patched` and `updated` carry every changed index field\n * (index fields are item fields): a client keeping the index updates the\n * row's fields from them, and its `rev` to the frame's `rev`.\n */\nexport type CollectionDelta<Item = unknown> =\n | { readonly t: \"added\"; readonly item: Item; readonly index?: WireIndexRow }\n | { readonly t: \"updated\"; readonly item: Item }\n | { readonly t: \"patched\"; readonly id: string; readonly d: Partial<Item> }\n | { readonly t: \"removed\"; readonly id: string }\n | { readonly t: \"reset\" };\n\n/** `qd:c`: one flush's changes to one scope, in order. */\nexport interface CollectionFrame<Item = unknown> extends CollectionScopeRef {\n readonly rev: Revision;\n readonly deltas: readonly CollectionDelta<Item>[];\n}\n\n// ---------------------------------------------------------------------------\n// Topics, streams, channels and events (sections 8.2, 11.3 and 12.5)\n// ---------------------------------------------------------------------------\n\n/**\n * `qd:watch` and `qd:unwatch`: join or leave a change topic of service `s`:\n * `{collection}:{scope}` for one scope of a collection, or `service` for the\n * service-wide topic (RFC 0003 section 11.3).\n */\nexport interface WatchFrame {\n readonly s: string;\n readonly topic: string;\n}\n\n/**\n * `qd:changed`: a watched topic changed in the flush at `rev`; invalidate the\n * queries that watch it. Sent once per flush per topic, and carries no data.\n * On the `service` topic, `models` names the models whose writes changed it\n * (rc.5, additive: the client's model names, first letter lowercased), so a\n * query that watches only some of them (`watch: { service: [models] }`)\n * ignores the rest; a frame without it may have changed any of them.\n */\nexport interface ChangedFrame {\n readonly s: string;\n readonly topic: string;\n readonly rev: Revision;\n readonly models?: readonly string[];\n}\n\n/**\n * `qd:stream:sub` and `qd:stream:unsub`: one stream of a service, and for a\n * scoped stream one scope of it (absent for a global stream).\n */\nexport interface StreamSubscribe {\n readonly s: string;\n readonly stream: string;\n readonly scope?: string;\n}\n\n/**\n * The acknowledgement of `qd:stream:sub`: the stream's seed, oldest first,\n * read as the socket joined the stream's room, so items pushed after it\n * arrive as `qd:stream` frames (possibly before this acknowledgement). The\n * seed is the latest items the server kept, or what the service computed\n * for this subscriber (the current state the items change); a computed one\n * the server had to wait for may also reflect items that arrived as frames\n * meanwhile. A subscribe an unsubscribe overtook answers an empty seed. A\n * client replaces what it held with it.\n */\nexport type StreamSubscribeReply<Item = unknown> =\n | { readonly ok: true; readonly seed: readonly Item[] }\n | Failure;\n\n/**\n * `qd:stream`: an item pushed to a stream, as one array argument\n * `[service, stream, scope, item]` (like `qd:event`), so a fast stream sends\n * no key names: `scope` is `null` for a global stream. The positions are\n * fixed; a later revision may append elements after `item`, never insert\n * them, so a client reads the four it knows and ignores the rest. Before\n * `5.0.0-rc.4` it was the object `{ s, stream, scope?, item }`.\n */\nexport type StreamFrame<Item = unknown> = readonly [\n s: string,\n stream: string,\n scope: string | null,\n item: Item,\n];\n\n/**\n * `qd:ch`, sent volatile and never acknowledged: `[service, channel,\n * payload]`. A later revision may append elements after `payload`; the\n * server reads the three it knows and ignores the rest.\n */\nexport type ChannelFrame<Payload = unknown> = readonly [\n s: string,\n channel: string,\n payload: Payload,\n];\n\n/**\n * `qd:event`: a custom room event declared in a contract's `events`:\n * `[service, event, payload]`. A later revision may append elements after\n * `payload`; a client reads the three it knows and ignores the rest.\n */\nexport type EventFrame<Payload = unknown> = readonly [s: string, event: string, payload: Payload];\n\n/**\n * `qd:presence`: who is in an app room (one a method joined with\n * `ctx.rooms.join`) the socket is in, by user id (RFC 0003 section 12.5).\n * Sent to a socket as it joins with `users`, the whole list (its own user\n * included); to the room's other sockets with `joined` when a user's first\n * socket joins, and with `left` when a user's last socket leaves; and to a\n * socket that leaves with `users: []`, since it no longer sees the room. A\n * socket the server took out of the room (`rooms.leave(room, { userId })`)\n * gets that `users: []` unasked: it is out, and its channels requiring the\n * room are dropped until a joining call lets it back. Exactly one of\n * `users`, `joined` and `left` is present. Anonymous sockets are in no list.\n */\nexport interface PresenceFrame {\n readonly room: string;\n /** Every user in the room: replaces what the client holds for it. */\n readonly users?: readonly string[];\n /** A user who joined the room. */\n readonly joined?: string;\n /** A user who left the room: their last socket in it left or disconnected. */\n readonly left?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Connection-level frames (sections 4.4, 7.2 and 8.3)\n// ---------------------------------------------------------------------------\n\n/** Why the server ended a subscription. */\nexport type RevokeReason = \"access\" | \"anchor-deleted\";\n\n/**\n * `qd:revoked`: the server removed this socket from a subscription's room,\n * because the principal's access was lowered or removed (`access`), or the\n * row a collection scope is anchored on was deleted (`anchor-deleted`). A\n * stream feed's subscription is revoked for `access` only; `scope` is absent\n * for a global stream.\n */\nexport type RevokedFrame =\n | {\n readonly kind: \"entity\";\n readonly reason: RevokeReason;\n readonly s: string;\n readonly id: string;\n }\n | {\n readonly kind: \"collection\";\n readonly reason: RevokeReason;\n readonly s: string;\n readonly c: string;\n readonly scope: string;\n }\n | {\n readonly kind: \"stream\";\n readonly reason: RevokeReason;\n readonly s: string;\n readonly stream: string;\n readonly scope?: string;\n };\n\n/** `qd:rotate`: stay connected until a random moment within `withinMs`, then reconnect. */\nexport interface RotateFrame {\n readonly withinMs: number;\n}\n\n/** `qd:access`: the user's service grants changed. */\nexport interface AccessFrame {\n readonly serviceAccess: Readonly<Record<string, AccessLevel>>;\n}\n\n// ---------------------------------------------------------------------------\n// Event maps\n// ---------------------------------------------------------------------------\n\n/** The function a listener calls to acknowledge an event. */\ntype Ack<Reply> = (reply: Reply) => void;\n\n/** The listener of each client event, by its key in `CLIENT_EVENTS`. */\ninterface ClientListeners {\n call: (envelope: CallEnvelope, ack: Ack<CallReply>) => void;\n cancel: (frame: CancelFrame) => void;\n sub: (frame: EntitySubscribe, ack: Ack<EntitySubscribeReply>) => void;\n unsub: (frame: EntityUnsubscribe, ack?: Ack<Ok | Failure>) => void;\n collectionSub: (frame: CollectionSubscribe, ack: Ack<CollectionSubscribeReply>) => void;\n collectionUnsub: (frame: CollectionScopeRef, ack?: Ack<Ok | Failure>) => void;\n collectionItems: (frame: CollectionItemsRequest, ack: Ack<CollectionItemsReply>) => void;\n watch: (frame: WatchFrame, ack: Ack<Ok | Failure>) => void;\n unwatch: (frame: WatchFrame, ack?: Ack<Ok | Failure>) => void;\n streamSub: (frame: StreamSubscribe, ack: Ack<StreamSubscribeReply>) => void;\n streamUnsub: (frame: StreamSubscribe, ack?: Ack<Ok | Failure>) => void;\n channel: (frame: ChannelFrame) => void;\n}\n\n/** The listener of each server event, by its key in `SERVER_EVENTS`. */\ninterface ServerListeners {\n hello: (frame: HelloFrame) => void;\n entity: (frame: EntityFrame) => void;\n collection: (frame: CollectionFrame) => void;\n changed: (frame: ChangedFrame) => void;\n revoked: (frame: RevokedFrame) => void;\n stream: (frame: StreamFrame) => void;\n event: (frame: EventFrame) => void;\n presence: (frame: PresenceFrame) => void;\n rotate: (frame: RotateFrame) => void;\n access: (frame: AccessFrame) => void;\n}\n\n/**\n * The events a v5 client sends, keyed by name (`\"qd:call\"`, ...). The server\n * types its sockets with it: `new Server<ClientToServerEvents, ServerToClientEvents>()`.\n */\nexport type ClientToServerEvents = {\n [Key in keyof typeof CLIENT_EVENTS as (typeof CLIENT_EVENTS)[Key]]: ClientListeners[Key];\n};\n\n/**\n * The events a v5 server sends, keyed by name (`\"qd:hello\"`, ...). The client\n * types its socket with it: `Socket<ServerToClientEvents, ClientToServerEvents>`.\n */\nexport type ServerToClientEvents = {\n [Key in keyof typeof SERVER_EVENTS as (typeof SERVER_EVENTS)[Key]]: ServerListeners[Key];\n};\n\n// ---------------------------------------------------------------------------\n// Guards\n// ---------------------------------------------------------------------------\n\nfunction isCallId(value: unknown): value is CallId {\n return typeof value === \"number\" && Number.isSafeInteger(value) && value >= 0;\n}\n\nfunction isVersion(value: unknown): value is Version {\n return typeof value === \"string\" || (typeof value === \"number\" && Number.isFinite(value));\n}\n\n/**\n * True when `value` has the shape of a `qd:call` envelope: a call id, service\n * and method names, and a version when one is present. The input is not\n * looked at (the method's schema validates it), and unknown keys are ignored.\n */\nexport function isCallEnvelope(value: unknown): value is CallEnvelope {\n return (\n isRecord(value) &&\n isCallId(value.id) &&\n isName(value.s) &&\n isName(value.m) &&\n (value.v === undefined || isVersion(value.v))\n );\n}\n\n/** True when `value` has the shape of a `qd:cancel` frame. Unknown keys are ignored. */\nexport function isCancel(value: unknown): value is CancelFrame {\n return isRecord(value) && isCallId(value.id);\n}\n"],"mappings":";;;;;;AAyeA,SAAS,SAAS,OAAiC;AACjD,SAAO,OAAO,UAAU,YAAY,OAAO,cAAc,KAAK,KAAK,SAAS;AAC9E;AAEA,SAAS,UAAU,OAAkC;AACnD,SAAO,OAAO,UAAU,YAAa,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK;AACzF;AAOO,SAAS,eAAe,OAAuC;AACpE,SACE,SAAS,KAAK,KACd,SAAS,MAAM,EAAE,KACjB,OAAO,MAAM,CAAC,KACd,OAAO,MAAM,CAAC,MACb,MAAM,MAAM,UAAa,UAAU,MAAM,CAAC;AAE/C;AAGO,SAAS,SAAS,OAAsC;AAC7D,SAAO,SAAS,KAAK,KAAK,SAAS,MAAM,EAAE;AAC7C;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/protocol/envelope.ts"],"sourcesContent":["// Protocol v5 frames (RFC 0003 sections 8.2 and 8.3): what each `qd:*` event\n// carries and what its acknowledgement receives. `ClientToServerEvents` and\n// `ServerToClientEvents` key every frame by its event name from\n// `contract/names.ts`, in the event-map form Socket.IO's `Server` and the\n// client's `Socket` take as type parameters.\n//\n// Section 8.2 spells out the `qd:call`, `qd:cancel`, `qd:watch` and `qd:ch`\n// payloads, and sections 6, 7, 11.3 and 12.5 describe the rest. Fields the RFC\n// does not name follow its style: `s` service, `m` method, `c` collection,\n// `i` input, `d` data, `e` error, `v` version, `rev` revision.\n//\n// 4.1 used one event per service method and a `{ success, data }` reply\n// (4.1 `src/shared/types.ts:88-90`). Here every call travels as one\n// `qd:call` envelope and is answered `{ ok: true, d }` or `{ ok: false, e }`.\n//\n// Runtime guards cover the frames the dispatcher reads from an untrusted\n// client (`qd:call`, `qd:cancel`). They are written by hand so the package\n// root stays free of schema libraries.\n//\n// One rule for every frame, both ways: a receiver ignores the object fields\n// it does not know and the array elements after the last one it reads, and\n// a later revision of protocol 5 may only add fields to objects and append\n// elements to arrays (`qd:event`, `qd:stream`, `qd:ch`), never remove,\n// rename, retype or reorder what is here. So a client that reads only what\n// it knows keeps working against a newer server, and a server against a\n// newer client. Anything else needs a new protocol number (`qd:hello`'s\n// `protocol`), which the handshake refuses with `PROTOCOL_MISMATCH`.\n\nimport type { AccessLevel } from \"../contract/access\";\nimport type { CLIENT_EVENTS, SERVER_EVENTS } from \"../contract/names\";\nimport type { WireError } from \"./errors\";\nimport { isName, isRecord } from \"./guards\";\nimport type { HelloFrame } from \"./version\";\n\n/**\n * A revision: a whole number of microseconds since the epoch that every\n * flush raises (RFC 0003 section 5.3), `max(Date.now() * 1000, last + 1)` on\n * one process and the shared counter's next value (Valkey's clock in\n * microseconds) behind a cluster; a safe integer, about 1.8e15. Compare\n * revisions as numbers; do not read them as times.\n */\nexport type Revision = number;\n\n/**\n * The version of a result the caller holds, sent back as a call's `v` so the\n * server can answer \"not modified\" (RFC 0003 section 9, step 5): a row's\n * revision, or what the method's `version(input, ctx)` returns. Opaque to the\n * client.\n */\nexport type Version = number | string;\n\n/** A call's number among its socket's calls in flight: a non-negative safe integer. */\nexport type CallId = number;\n\n/** An acknowledgement that succeeded with nothing to return. */\nexport interface Ok {\n readonly ok: true;\n}\n\n/** An acknowledgement that failed, and why. */\nexport interface Failure {\n readonly ok: false;\n readonly e: WireError;\n}\n\n// ---------------------------------------------------------------------------\n// Calls (section 8.2)\n// ---------------------------------------------------------------------------\n\n/** `qd:call`: one method call, answered through the acknowledgement. */\nexport interface CallEnvelope {\n /** Names the call for `qd:cancel`; unique among the socket's calls in flight. */\n readonly id: CallId;\n /** The service name. */\n readonly s: string;\n /** The method name. */\n readonly m: string;\n /** The input, not yet validated. Absent when the input is `undefined`. */\n readonly i?: unknown;\n /** The version of this call's result the caller already holds. */\n readonly v?: Version;\n}\n\n/** A call that returned data, with the version of that data when the method has one. */\nexport interface CallSuccess<Output = unknown> {\n readonly ok: true;\n readonly d: Output;\n readonly v?: Version;\n readonly nm?: undefined;\n}\n\n/** A call whose result is still the version the caller sent: keep the cached copy. */\nexport interface CallNotModified {\n readonly ok: true;\n readonly nm: true;\n readonly v: Version;\n}\n\n/** The acknowledgement of `qd:call`. */\nexport type CallReply<Output = unknown> = CallSuccess<Output> | CallNotModified | Failure;\n\n/** `qd:cancel`: abort a call in flight. The call's acknowledgement still arrives. */\nexport interface CancelFrame {\n readonly id: CallId;\n}\n\n// ---------------------------------------------------------------------------\n// Entity subscriptions (section 6)\n// ---------------------------------------------------------------------------\n\n/** `qd:sub`: subscribe to rows of one service, sending the revision held for each. */\nexport interface EntitySubscribe {\n readonly s: string;\n readonly ids: readonly string[];\n /** The revision held for each id, by position in `ids`; `null` where none is cached. */\n readonly revs?: readonly (Revision | null)[];\n}\n\n/** One id's row, as `qd:sub` answers it. */\nexport interface EntityRow<Row = unknown> {\n readonly ok: true;\n readonly d: Row;\n readonly rev: Revision;\n readonly nm?: undefined;\n}\n\n/** The revision the client sent for this id is current. */\nexport interface EntityNotModified {\n readonly ok: true;\n readonly nm: true;\n readonly rev: Revision;\n}\n\n/** One id's answer to `qd:sub`: the row, \"not modified\", or why it was refused. */\nexport type EntityResult<Row = unknown> = EntityRow<Row> | EntityNotModified | Failure;\n\n/** The acknowledgement of `qd:sub`: one result per requested id, in request order. */\nexport type EntitySubscribeReply<Row = unknown> =\n | { readonly ok: true; readonly r: readonly EntityResult<Row>[] }\n | Failure;\n\n/** `qd:unsub`: stop receiving frames for these rows. */\nexport interface EntityUnsubscribe {\n readonly s: string;\n readonly ids: readonly string[];\n}\n\n/** `qd:e` `u`: the full row. Replace the cached row when `rev` is newer. */\nexport interface EntityUpdate<Row = unknown> {\n readonly t: \"u\";\n readonly s: string;\n readonly id: string;\n readonly rev: Revision;\n readonly d: Row;\n}\n\n/** `qd:e` `p`: changed fields only. Merge when `rev` is newer; fetch the row when it is not cached. */\nexport interface EntityPatch<Row = unknown> {\n readonly t: \"p\";\n readonly s: string;\n readonly id: string;\n readonly rev: Revision;\n readonly d: Partial<Row>;\n}\n\n/** `qd:e` `r`: the row is gone. Keep a tombstone until a newer `u`. */\nexport interface EntityRemove {\n readonly t: \"r\";\n readonly s: string;\n readonly id: string;\n readonly rev: Revision;\n}\n\n/** `qd:e`: a change to one row, sent to the room of each access tier that has subscribers. */\nexport type EntityFrame<Row = unknown> = EntityUpdate<Row> | EntityPatch<Row> | EntityRemove;\n\n// ---------------------------------------------------------------------------\n// Collections (section 7)\n// ---------------------------------------------------------------------------\n\n/** One scope of one collection of a service. */\nexport interface CollectionScopeRef {\n readonly s: string;\n readonly c: string;\n readonly scope: string;\n}\n\n/** `qd:col:sub`: the first page of a scope, a later page by `cursor`, or a resume from `since`. */\nexport interface CollectionSubscribe extends CollectionScopeRef {\n /** The revision the client holds the scope at; the server resumes from it when it can. */\n readonly since?: Revision;\n readonly limit?: number;\n readonly cursor?: string;\n}\n\n/**\n * A scope member in the collection's index (RFC 0003 section 7.4): its id,\n * its revision, then its index field values in the order `index` declares.\n * The revision is the time in the service's `versionColumn` when it declares\n * one (in microseconds, as revisions are), else the revision the snapshot\n * (or, in an `added` delta, the flush) was read at.\n */\nexport type WireIndexRow = readonly [id: string, rev: Revision, ...fields: unknown[]];\n\n/** A page of a scope: the answer to `qd:col:sub` without `since`, or when `since` is too old. */\nexport interface CollectionSnapshot<Item = unknown> {\n readonly ok: true;\n readonly resumed?: undefined;\n /** The revision the page was read at; resume from it with `since`. */\n readonly rev: Revision;\n readonly items: readonly Item[];\n /** How many members the scope has. */\n readonly total: number;\n /** The next page's cursor; `null` on the last page. */\n readonly cursor: string | null;\n /** The page size used: the request's, or `maxLimit` when the request asked for more. */\n readonly limit: number;\n /** Present when the requested limit was above `maxLimit` and was lowered to it. */\n readonly clamped?: true;\n /**\n * One row per member in `order`, on the first page of a collection that\n * declares `index` (not on a page read with `cursor`). Absent when the\n * scope has more members than the index holds: see `indexTruncated`.\n */\n readonly index?: readonly WireIndexRow[];\n /** Present instead of `index` when the scope has more members than the index holds (50,000). */\n readonly indexTruncated?: true;\n}\n\n/** The answer to `qd:col:sub` when the server still holds every change since `since`. */\nexport interface CollectionResumed<Item = unknown> {\n readonly ok: true;\n readonly resumed: true;\n /** The scope's revision after these deltas. */\n readonly rev: Revision;\n readonly deltas: readonly CollectionDelta<Item>[];\n}\n\n/** The acknowledgement of `qd:col:sub`. */\nexport type CollectionSubscribeReply<Item = unknown> =\n | CollectionSnapshot<Item>\n | CollectionResumed<Item>\n | Failure;\n\n/** `qd:col:items`: load full items of a scope by id. */\nexport interface CollectionItemsRequest extends CollectionScopeRef {\n readonly ids: readonly string[];\n}\n\n/**\n * The acknowledgement of `qd:col:items`: the items found, in request order,\n * and the revision they were read at, taken before the read as a\n * snapshot's is: a client drops an item older than what a delta brought it.\n */\nexport type CollectionItemsReply<Item = unknown> =\n | { readonly ok: true; readonly rev: Revision; readonly items: readonly Item[] }\n | Failure;\n\n/**\n * One change to a scope (RFC 0003 section 7.2). In a collection that declares\n * `index`, `added` carries the member's index row, built from the same row as\n * its item, and `patched` and `updated` carry every changed index field\n * (index fields are item fields): a client keeping the index updates the\n * row's fields from them, and its `rev` to the frame's `rev`.\n */\nexport type CollectionDelta<Item = unknown> =\n | { readonly t: \"added\"; readonly item: Item; readonly index?: WireIndexRow }\n | { readonly t: \"updated\"; readonly item: Item }\n | { readonly t: \"patched\"; readonly id: string; readonly d: Partial<Item> }\n | { readonly t: \"removed\"; readonly id: string }\n | { readonly t: \"reset\" };\n\n/** `qd:c`: one flush's changes to one scope, in order. */\nexport interface CollectionFrame<Item = unknown> extends CollectionScopeRef {\n readonly rev: Revision;\n readonly deltas: readonly CollectionDelta<Item>[];\n}\n\n// ---------------------------------------------------------------------------\n// Topics, streams, channels and events (sections 8.2, 11.3 and 12.5)\n// ---------------------------------------------------------------------------\n\n/**\n * `qd:watch` and `qd:unwatch`: join or leave a change topic of service `s`:\n * `{collection}:{scope}` for one scope of a collection, or `service` for the\n * service-wide topic (RFC 0003 section 11.3).\n */\nexport interface WatchFrame {\n readonly s: string;\n readonly topic: string;\n}\n\n/**\n * `qd:changed`: a watched topic changed in the flush at `rev`; invalidate the\n * queries that watch it. Sent once per flush per topic, and carries no data.\n * On the `service` topic, `models` names the models whose writes changed it\n * (rc.5, additive: the client's model names, first letter lowercased), so a\n * query that watches only some of them (`watch: { service: [models] }`)\n * ignores the rest; a frame without it may have changed any of them.\n */\nexport interface ChangedFrame {\n readonly s: string;\n readonly topic: string;\n readonly rev: Revision;\n readonly models?: readonly string[];\n}\n\n/**\n * `qd:stream:sub` and `qd:stream:unsub`: one stream of a service, and for a\n * scoped stream one scope of it (absent for a global stream).\n */\nexport interface StreamSubscribe {\n readonly s: string;\n readonly stream: string;\n readonly scope?: string;\n}\n\n/**\n * The acknowledgement of `qd:stream:sub`: the stream's seed, oldest first,\n * read as the socket joined the stream's room, so items pushed after it\n * arrive as `qd:stream` frames (possibly before this acknowledgement). The\n * seed is the latest items the server kept, or what the service computed\n * for this subscriber (the current state the items change); a computed one\n * the server had to wait for may also reflect items that arrived as frames\n * meanwhile. A subscribe an unsubscribe overtook answers an empty seed. A\n * client replaces what it held with it.\n */\nexport type StreamSubscribeReply<Item = unknown> =\n | { readonly ok: true; readonly seed: readonly Item[] }\n | Failure;\n\n/**\n * `qd:stream`: an item pushed to a stream, as one array argument\n * `[service, stream, scope, item]` (like `qd:event`), so a fast stream sends\n * no key names: `scope` is `null` for a global stream. The positions are\n * fixed; a later revision may append elements after `item`, never insert\n * them, so a client reads the four it knows and ignores the rest. Before\n * `5.0.0-rc.4` it was the object `{ s, stream, scope?, item }`.\n */\nexport type StreamFrame<Item = unknown> = readonly [\n s: string,\n stream: string,\n scope: string | null,\n item: Item,\n];\n\n/**\n * `qd:ch`, sent volatile and never acknowledged: `[service, channel,\n * payload]`. A later revision may append elements after `payload`; the\n * server reads the three it knows and ignores the rest.\n */\nexport type ChannelFrame<Payload = unknown> = readonly [\n s: string,\n channel: string,\n payload: Payload,\n];\n\n/**\n * `qd:event`: a custom room event declared in a contract's `events`:\n * `[service, event, payload]`. A later revision may append elements after\n * `payload`; a client reads the three it knows and ignores the rest.\n */\nexport type EventFrame<Payload = unknown> = readonly [s: string, event: string, payload: Payload];\n\n/**\n * `qd:presence`: who is in an app room (one a method joined with\n * `ctx.rooms.join`) the socket is in, by user id (RFC 0003 section 12.5).\n * Sent to a socket as it joins with `users`, the whole list (its own user\n * included); to the room's other sockets with `joined` when a user's first\n * socket joins, and with `left` when a user's last socket leaves; and to a\n * socket that leaves with `users: []`, since it no longer sees the room. A\n * socket the server took out of the room (`rooms.leave(room, { userId })`)\n * gets that `users: []` unasked: it is out, and its channels requiring the\n * room are dropped until a joining call lets it back. Exactly one of\n * `users`, `joined` and `left` is present. Anonymous sockets are in no list.\n */\nexport interface PresenceFrame {\n readonly room: string;\n /** Every user in the room: replaces what the client holds for it. */\n readonly users?: readonly string[];\n /** A user who joined the room. */\n readonly joined?: string;\n /** A user who left the room: their last socket in it left or disconnected. */\n readonly left?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Peer links (section 12.5)\n// ---------------------------------------------------------------------------\n\n/**\n * A peer link's two channels: `rel`, ordered and reliable, and `fast`,\n * unordered and never retransmitted (dropped where a connection is backed up).\n */\nexport type PeerLabel = \"rel\" | \"fast\";\n\n/** A WebRTC session description, as `RTCSessionDescription.toJSON()` writes it. */\nexport interface PeerDescription {\n readonly type: \"offer\" | \"answer\";\n readonly sdp: string;\n}\n\n/** A WebRTC ICE candidate, as `RTCIceCandidate.toJSON()` writes it. */\nexport interface PeerCandidate {\n readonly candidate: string;\n readonly sdpMid?: string | null;\n readonly sdpMLineIndex?: number | null;\n readonly usernameFragment?: string | null;\n}\n\n/**\n * What one `qd:peer` frame says about a link, in exactly one member: the\n * offerer's `offer` or the answerer's `answer` (`sdp`), one ICE candidate\n * (`ice`), that the sender sends the link's messages through the relay\n * from now on (`relay`), or that the link is over (`bye`): the sender\n * closed it or, when the server sends it, left the room or disconnected.\n */\nexport interface PeerSignal {\n readonly sdp?: PeerDescription;\n readonly ice?: PeerCandidate;\n readonly relay?: true;\n readonly bye?: true;\n}\n\n/**\n * `qd:peer`, from a client: a signal for socket `to` (another socket's id)\n * about the link the two have in app room `room`, which both must be in.\n * Acknowledged when asked: `Ok`, or `UNAUTHENTICATED`, `FORBIDDEN` (the\n * sender is not in the room), `NOT_FOUND` (no signed-in socket `to` is in\n * the room; behind a cluster adapter a socket on another node is not\n * checked before the signal goes), `CONFLICT` (the sender already signals\n * `limits.peers.maxLinks` peers), `RATE_LIMITED` or `VALIDATION`.\n */\nexport interface PeerSignalFrame {\n readonly room: string;\n readonly to: string;\n readonly sig: PeerSignal;\n}\n\n/**\n * `qd:peer`, from the server: a signal from socket `from` about the link in\n * app room `room`. `userId` is the sender's user, as the server knows it.\n */\nexport interface PeerForwardFrame {\n readonly room: string;\n readonly from: string;\n readonly userId: string;\n readonly sig: PeerSignal;\n}\n\n/**\n * `qd:relay`, from a client, never acknowledged: a link message for socket\n * `to` in app room `room`. `text` is a `PeerMessage` as JSON, at most\n * `limits.peers.maxMessageBytes`. The two sockets must have signalled each\n * other in that room; a message over the rate or the byte budget of\n * `limits.peers`, or to a socket that left the room, is dropped, and so is\n * one on `fast` while the receiving socket's connection is backed up\n * (`PEER_RELAY_FAST_BACKLOG`). A later revision may append elements after\n * `text`.\n */\nexport type RelayFrame = readonly [room: string, to: string, label: PeerLabel, text: string];\n\n/** `qd:relay`, from the server: a link message from socket `from` in app room `room`. */\nexport type RelayForwardFrame = readonly [\n room: string,\n from: string,\n label: PeerLabel,\n text: string,\n];\n\n/**\n * A peer link message: the JSON text of one data channel message or of a\n * relayed `text`, alike on both paths. A receiver ignores a message whose\n * first element it does not know, and elements after the ones it reads.\n */\nexport type PeerMessage = PeerData | PeerPing | PeerPong | HostMessage;\n\n/** A link message carrying the app's `data`, any JSON value. */\nexport type PeerData = readonly [kind: 0, data: unknown];\n\n/** A clock ping: `t` is the sender's clock in milliseconds. Sent on `fast`. */\nexport type PeerPing = readonly [kind: 1, t: number];\n\n/** The pong of a ping: `t` as the ping carried it, `u` the answerer's clock as it answered. Sent on `fast`. */\nexport type PeerPong = readonly [kind: 2, t: number, u: number];\n\n/**\n * A host mode message (section 12.5): what a peer and its room's host send\n * each other over their link, kept apart from the app's own messages.\n */\nexport type HostMessage =\n | HostChannelMessage\n | HostSubscribe\n | HostUnsubscribe\n | HostSeed\n | HostItem\n | HostRefusal;\n\n/** A message on a host-served channel, from a peer to the host of the channel's room. Sent on `fast`. */\nexport type HostChannelMessage = readonly [\n kind: 3,\n service: string,\n channel: string,\n payload: unknown,\n];\n\n/** Subscribes to a feed of a host-served stream, from a peer to the host of its room. Sent on `rel`. */\nexport type HostSubscribe = readonly [kind: 4, service: string, stream: string, scope: string];\n\n/** Leaves a feed of a host-served stream. Sent on `rel`. */\nexport type HostUnsubscribe = readonly [kind: 5, service: string, stream: string, scope: string];\n\n/**\n * The seed of a feed a peer subscribed to, from the host: the latest items\n * it kept (or computed), oldest first, in chunks that each fit the message\n * limit; `last` is true on the last chunk. Sent on `rel`.\n */\nexport type HostSeed = readonly [\n kind: 6,\n service: string,\n stream: string,\n scope: string,\n items: readonly unknown[],\n last: boolean,\n];\n\n/** An item pushed to a feed, from the host: on `fast` for a volatile stream, else on `rel`. */\nexport type HostItem = readonly [\n kind: 7,\n service: string,\n stream: string,\n scope: string,\n item: unknown,\n];\n\n/** A subscribe the host refused (`NOT_FOUND`, `FORBIDDEN`, ...), from the host. Sent on `rel`. */\nexport type HostRefusal = readonly [\n kind: 8,\n service: string,\n stream: string,\n scope: string,\n error: WireError,\n];\n\n/**\n * `qd:host`, from the server: the host of app room `room` (the socket that\n * serves its host-served channels and streams), or `null` when it has none.\n * Sent to the room's sockets on every change, and to a socket that joins a\n * room with a host.\n */\nexport type HostFrame = readonly [room: string, host: string | null];\n\n// ---------------------------------------------------------------------------\n// Connection-level frames (sections 4.4, 7.2 and 8.3)\n// ---------------------------------------------------------------------------\n\n/** Why the server ended a subscription. */\nexport type RevokeReason = \"access\" | \"anchor-deleted\";\n\n/**\n * `qd:revoked`: the server removed this socket from a subscription's room,\n * because the principal's access was lowered or removed (`access`), or the\n * row a collection scope is anchored on was deleted (`anchor-deleted`). A\n * stream feed's subscription is revoked for `access` only; `scope` is absent\n * for a global stream.\n */\nexport type RevokedFrame =\n | {\n readonly kind: \"entity\";\n readonly reason: RevokeReason;\n readonly s: string;\n readonly id: string;\n }\n | {\n readonly kind: \"collection\";\n readonly reason: RevokeReason;\n readonly s: string;\n readonly c: string;\n readonly scope: string;\n }\n | {\n readonly kind: \"stream\";\n readonly reason: RevokeReason;\n readonly s: string;\n readonly stream: string;\n readonly scope?: string;\n };\n\n/** `qd:rotate`: stay connected until a random moment within `withinMs`, then reconnect. */\nexport interface RotateFrame {\n readonly withinMs: number;\n}\n\n/** `qd:access`: the user's service grants changed. */\nexport interface AccessFrame {\n readonly serviceAccess: Readonly<Record<string, AccessLevel>>;\n}\n\n// ---------------------------------------------------------------------------\n// Event maps\n// ---------------------------------------------------------------------------\n\n/** The function a listener calls to acknowledge an event. */\ntype Ack<Reply> = (reply: Reply) => void;\n\n/** The listener of each client event, by its key in `CLIENT_EVENTS`. */\ninterface ClientListeners {\n call: (envelope: CallEnvelope, ack: Ack<CallReply>) => void;\n cancel: (frame: CancelFrame) => void;\n sub: (frame: EntitySubscribe, ack: Ack<EntitySubscribeReply>) => void;\n unsub: (frame: EntityUnsubscribe, ack?: Ack<Ok | Failure>) => void;\n collectionSub: (frame: CollectionSubscribe, ack: Ack<CollectionSubscribeReply>) => void;\n collectionUnsub: (frame: CollectionScopeRef, ack?: Ack<Ok | Failure>) => void;\n collectionItems: (frame: CollectionItemsRequest, ack: Ack<CollectionItemsReply>) => void;\n watch: (frame: WatchFrame, ack: Ack<Ok | Failure>) => void;\n unwatch: (frame: WatchFrame, ack?: Ack<Ok | Failure>) => void;\n streamSub: (frame: StreamSubscribe, ack: Ack<StreamSubscribeReply>) => void;\n streamUnsub: (frame: StreamSubscribe, ack?: Ack<Ok | Failure>) => void;\n channel: (frame: ChannelFrame) => void;\n peer: (frame: PeerSignalFrame, ack?: Ack<Ok | Failure>) => void;\n relay: (frame: RelayFrame) => void;\n}\n\n/** The listener of each server event, by its key in `SERVER_EVENTS`. */\ninterface ServerListeners {\n hello: (frame: HelloFrame) => void;\n entity: (frame: EntityFrame) => void;\n collection: (frame: CollectionFrame) => void;\n changed: (frame: ChangedFrame) => void;\n revoked: (frame: RevokedFrame) => void;\n stream: (frame: StreamFrame) => void;\n event: (frame: EventFrame) => void;\n presence: (frame: PresenceFrame) => void;\n rotate: (frame: RotateFrame) => void;\n access: (frame: AccessFrame) => void;\n peer: (frame: PeerForwardFrame) => void;\n relay: (frame: RelayForwardFrame) => void;\n host: (frame: HostFrame) => void;\n}\n\n/**\n * The events a v5 client sends, keyed by name (`\"qd:call\"`, ...). The server\n * types its sockets with it: `new Server<ClientToServerEvents, ServerToClientEvents>()`.\n */\nexport type ClientToServerEvents = {\n [Key in keyof typeof CLIENT_EVENTS as (typeof CLIENT_EVENTS)[Key]]: ClientListeners[Key];\n};\n\n/**\n * The events a v5 server sends, keyed by name (`\"qd:hello\"`, ...). The client\n * types its socket with it: `Socket<ServerToClientEvents, ClientToServerEvents>`.\n */\nexport type ServerToClientEvents = {\n [Key in keyof typeof SERVER_EVENTS as (typeof SERVER_EVENTS)[Key]]: ServerListeners[Key];\n};\n\n// ---------------------------------------------------------------------------\n// Guards\n// ---------------------------------------------------------------------------\n\nfunction isCallId(value: unknown): value is CallId {\n return typeof value === \"number\" && Number.isSafeInteger(value) && value >= 0;\n}\n\nfunction isVersion(value: unknown): value is Version {\n return typeof value === \"string\" || (typeof value === \"number\" && Number.isFinite(value));\n}\n\n/**\n * True when `value` has the shape of a `qd:call` envelope: a call id, service\n * and method names, and a version when one is present. The input is not\n * looked at (the method's schema validates it), and unknown keys are ignored.\n */\nexport function isCallEnvelope(value: unknown): value is CallEnvelope {\n return (\n isRecord(value) &&\n isCallId(value.id) &&\n isName(value.s) &&\n isName(value.m) &&\n (value.v === undefined || isVersion(value.v))\n );\n}\n\n/** True when `value` has the shape of a `qd:cancel` frame. Unknown keys are ignored. */\nexport function isCancel(value: unknown): value is CancelFrame {\n return isRecord(value) && isCallId(value.id);\n}\n"],"mappings":";;;;;;AAopBA,SAAS,SAAS,OAAiC;AACjD,SAAO,OAAO,UAAU,YAAY,OAAO,cAAc,KAAK,KAAK,SAAS;AAC9E;AAEA,SAAS,UAAU,OAAkC;AACnD,SAAO,OAAO,UAAU,YAAa,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK;AACzF;AAOO,SAAS,eAAe,OAAuC;AACpE,SACE,SAAS,KAAK,KACd,SAAS,MAAM,EAAE,KACjB,OAAO,MAAM,CAAC,KACd,OAAO,MAAM,CAAC,MACb,MAAM,MAAM,UAAa,UAAU,MAAM,CAAC;AAE/C;AAGO,SAAS,SAAS,OAAsC;AAC7D,SAAO,SAAS,KAAK,KAAK,SAAS,MAAM,EAAE;AAC7C;","names":[]}
|
|
@@ -455,170 +455,6 @@ function countOutput() {
|
|
|
455
455
|
);
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
-
// src/contract/kits/searchSchemas.ts
|
|
459
|
-
var SEARCH_DEFAULT_LIMIT = 20;
|
|
460
|
-
var SEARCH_MAX_LIMIT = 100;
|
|
461
|
-
var SEARCH_DEFAULT_MIN_LENGTH = 2;
|
|
462
|
-
var SEARCH_MAX_QUERY_LENGTH = 256;
|
|
463
|
-
var MAX_SCOPE_LENGTH = 256;
|
|
464
|
-
function queryIssues(q) {
|
|
465
|
-
if (typeof q !== "string") {
|
|
466
|
-
return [{ message: "Expected the text to search for", path: ["q"] }];
|
|
467
|
-
}
|
|
468
|
-
return q.length > SEARCH_MAX_QUERY_LENGTH ? [{ message: `At most ${SEARCH_MAX_QUERY_LENGTH} characters`, path: ["q"] }] : [];
|
|
469
|
-
}
|
|
470
|
-
function scopeIssues(scope) {
|
|
471
|
-
const valid = scope === void 0 || typeof scope === "string" && scope.length > 0 && scope.length <= MAX_SCOPE_LENGTH;
|
|
472
|
-
return valid ? [] : [{ message: "Expected a scope value", path: ["scope"] }];
|
|
473
|
-
}
|
|
474
|
-
function queryOf2(value) {
|
|
475
|
-
const limit = value.limit ?? SEARCH_DEFAULT_LIMIT;
|
|
476
|
-
return {
|
|
477
|
-
q: value.q.trim(),
|
|
478
|
-
scope: value.scope,
|
|
479
|
-
cursor: value.cursor,
|
|
480
|
-
limit: Math.min(limit, SEARCH_MAX_LIMIT)
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
function searchJson(scoped) {
|
|
484
|
-
return objectJson(
|
|
485
|
-
{
|
|
486
|
-
q: { type: "string", maxLength: SEARCH_MAX_QUERY_LENGTH },
|
|
487
|
-
...scoped ? { scope: { type: "string", minLength: 1, maxLength: MAX_SCOPE_LENGTH } } : {},
|
|
488
|
-
cursor: { type: "string", minLength: 1, maxLength: MAX_CURSOR_LENGTH },
|
|
489
|
-
limit: {
|
|
490
|
-
type: "integer",
|
|
491
|
-
minimum: 1,
|
|
492
|
-
maximum: SEARCH_MAX_LIMIT,
|
|
493
|
-
default: SEARCH_DEFAULT_LIMIT
|
|
494
|
-
}
|
|
495
|
-
},
|
|
496
|
-
["q"]
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
function searchInput(scoped) {
|
|
500
|
-
const keys = scoped ? ["q", "scope", "cursor", "limit"] : ["q", "cursor", "limit"];
|
|
501
|
-
return kitSchema(
|
|
502
|
-
(input) => {
|
|
503
|
-
if (!isRecord(input)) {
|
|
504
|
-
return { issues: [{ message: "Expected { q, ... }", path: [] }] };
|
|
505
|
-
}
|
|
506
|
-
const issues = [
|
|
507
|
-
...unknownKeys(input, keys),
|
|
508
|
-
...queryIssues(input.q),
|
|
509
|
-
...scoped ? scopeIssues(input.scope) : [],
|
|
510
|
-
...pagingIssues({ cursor: input.cursor, limit: input.limit })
|
|
511
|
-
];
|
|
512
|
-
return issues.length > 0 ? { issues } : { value: queryOf2(input) };
|
|
513
|
-
},
|
|
514
|
-
{ input: () => searchJson(scoped) }
|
|
515
|
-
);
|
|
516
|
-
}
|
|
517
|
-
function searchPageOutput(item) {
|
|
518
|
-
return kitSchema(
|
|
519
|
-
(value) => {
|
|
520
|
-
if (!isRecord(value)) {
|
|
521
|
-
return { issues: [{ message: "Expected a page", path: [] }] };
|
|
522
|
-
}
|
|
523
|
-
const { nextCursor, rev } = value;
|
|
524
|
-
const issues = [...unknownKeys(value, ["items", "nextCursor", "rev"])];
|
|
525
|
-
issues.push(...itemsIssues(value.items));
|
|
526
|
-
if (nextCursor !== null && typeof nextCursor !== "string") {
|
|
527
|
-
issues.push({ message: "Expected a cursor or null", path: ["nextCursor"] });
|
|
528
|
-
}
|
|
529
|
-
if (rev !== void 0 && !(typeof rev === "number" && Number.isFinite(rev))) {
|
|
530
|
-
issues.push({ message: "Expected a revision", path: ["rev"] });
|
|
531
|
-
}
|
|
532
|
-
return issues.length > 0 ? { issues } : { value };
|
|
533
|
-
},
|
|
534
|
-
{
|
|
535
|
-
input: (target) => objectJson(
|
|
536
|
-
{
|
|
537
|
-
items: itemsJson(item, target),
|
|
538
|
-
nextCursor: { type: ["string", "null"] },
|
|
539
|
-
rev: { type: "number" }
|
|
540
|
-
},
|
|
541
|
-
["items", "nextCursor"]
|
|
542
|
-
)
|
|
543
|
-
}
|
|
544
|
-
);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// src/contract/kits/search.ts
|
|
548
|
-
var SPECS = /* @__PURE__ */ new WeakMap();
|
|
549
|
-
function searchSpecOf(method) {
|
|
550
|
-
return typeof method === "object" && method !== null ? SPECS.get(method) : void 0;
|
|
551
|
-
}
|
|
552
|
-
var OPTIONS = ["entity", "fields", "item", "scope", "minLength", "describe"];
|
|
553
|
-
function isRecord2(value) {
|
|
554
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
555
|
-
}
|
|
556
|
-
function fail(message) {
|
|
557
|
-
throw new TypeError(`search.contract: ${message}`);
|
|
558
|
-
}
|
|
559
|
-
function checkFields(fields) {
|
|
560
|
-
const valid = Array.isArray(fields) && fields.length > 0 && fields.every((field) => typeof field === "string" && field.length > 0) && new Set(fields).size === fields.length;
|
|
561
|
-
if (!valid) {
|
|
562
|
-
fail("fields must be a list of the distinct text fields to search, at least one");
|
|
563
|
-
}
|
|
564
|
-
return Object.freeze([...fields]);
|
|
565
|
-
}
|
|
566
|
-
function checkMinLength(minLength) {
|
|
567
|
-
if (minLength === void 0) {
|
|
568
|
-
return SEARCH_DEFAULT_MIN_LENGTH;
|
|
569
|
-
}
|
|
570
|
-
const valid = typeof minLength === "number" && Number.isInteger(minLength) && minLength >= 1 && minLength <= SEARCH_MAX_QUERY_LENGTH;
|
|
571
|
-
if (!valid) {
|
|
572
|
-
fail(`minLength must be a whole number from 1 to ${SEARCH_MAX_QUERY_LENGTH}`);
|
|
573
|
-
}
|
|
574
|
-
return minLength;
|
|
575
|
-
}
|
|
576
|
-
function specOf(options) {
|
|
577
|
-
if (!isRecord2(options)) {
|
|
578
|
-
fail("options must be { entity, fields, item?, scope?, minLength?, describe? }");
|
|
579
|
-
}
|
|
580
|
-
const unknownKey = Object.keys(options).find((key) => !OPTIONS.includes(key));
|
|
581
|
-
if (unknownKey !== void 0) {
|
|
582
|
-
fail(`unknown option "${unknownKey}"; the options are ${OPTIONS.join(", ")}`);
|
|
583
|
-
}
|
|
584
|
-
if (!isStandardSchema(options.entity)) {
|
|
585
|
-
fail("entity must be the contract's entity schema");
|
|
586
|
-
}
|
|
587
|
-
if (options.item !== void 0 && !isStandardSchema(options.item)) {
|
|
588
|
-
fail("item must be the entity schema or one of the contract's projection schemas");
|
|
589
|
-
}
|
|
590
|
-
const { scope } = options;
|
|
591
|
-
if (scope !== void 0 && (typeof scope !== "string" || scope.length === 0)) {
|
|
592
|
-
fail("scope must name a collection of the contract");
|
|
593
|
-
}
|
|
594
|
-
return {
|
|
595
|
-
fields: checkFields(options.fields),
|
|
596
|
-
item: options.item,
|
|
597
|
-
scope,
|
|
598
|
-
minLength: checkMinLength(options.minLength)
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
function describeOf(options, spec) {
|
|
602
|
-
const own = options.describe;
|
|
603
|
-
if (own !== void 0 && (typeof own !== "string" || own.length === 0)) {
|
|
604
|
-
fail("describe must be a non-empty string");
|
|
605
|
-
}
|
|
606
|
-
const scoped = spec.scope === void 0 ? "" : ` Pass scope (one scope of ${spec.scope}) to search only its members.`;
|
|
607
|
-
return own ?? `Searches the rows the caller can read for the text q (at least ${spec.minLength} characters), a page at a time.${scoped} Pass a page's nextCursor back as cursor for the next page.`;
|
|
608
|
-
}
|
|
609
|
-
function contract(options) {
|
|
610
|
-
const spec = specOf(options);
|
|
611
|
-
const entity = options.entity;
|
|
612
|
-
const def = query({
|
|
613
|
-
input: searchInput(spec.scope !== void 0),
|
|
614
|
-
output: searchPageOutput(spec.item ?? entity),
|
|
615
|
-
describe: describeOf(options, spec)
|
|
616
|
-
});
|
|
617
|
-
SPECS.set(def, Object.freeze(spec));
|
|
618
|
-
return Object.freeze({ search: def });
|
|
619
|
-
}
|
|
620
|
-
var search = Object.freeze({ contract });
|
|
621
|
-
|
|
622
458
|
// src/contract/kits/adminFields.ts
|
|
623
459
|
var ADMIN_FIELD_TYPES = Object.freeze([
|
|
624
460
|
"string",
|
|
@@ -747,9 +583,9 @@ function rootObject(json) {
|
|
|
747
583
|
const root = resolve(json, { definitions: json.definitions, $defs: json.$defs });
|
|
748
584
|
return isRecord(root?.properties) ? root : void 0;
|
|
749
585
|
}
|
|
750
|
-
function entityJson(entity,
|
|
586
|
+
function entityJson(entity, fail2) {
|
|
751
587
|
if (!hasJsonSchema(entity)) {
|
|
752
|
-
|
|
588
|
+
fail2(
|
|
753
589
|
"the entity schema cannot describe itself as JSON Schema, which the admin kit's field metadata needs: use Zod 4.2 or later for that schema"
|
|
754
590
|
);
|
|
755
591
|
}
|
|
@@ -758,20 +594,20 @@ function entityJson(entity, fail3) {
|
|
|
758
594
|
if (isRecord(json)) {
|
|
759
595
|
return json;
|
|
760
596
|
}
|
|
761
|
-
return
|
|
597
|
+
return fail2("the entity schema's JSON Schema is not an object");
|
|
762
598
|
} catch (error) {
|
|
763
599
|
const reason = error instanceof Error ? error.message : String(error);
|
|
764
|
-
return
|
|
600
|
+
return fail2(
|
|
765
601
|
`the entity schema cannot be written as JSON Schema (${reason}), which the admin kit's field metadata needs: change that schema`
|
|
766
602
|
);
|
|
767
603
|
}
|
|
768
604
|
}
|
|
769
|
-
function entityFieldsOf(entity,
|
|
770
|
-
const json = entityJson(entity,
|
|
605
|
+
function entityFieldsOf(entity, fail2) {
|
|
606
|
+
const json = entityJson(entity, fail2);
|
|
771
607
|
const root = rootObject(json);
|
|
772
608
|
const properties = root?.properties;
|
|
773
609
|
if (root === void 0 || !isRecord(properties) || !Object.hasOwn(properties, "id")) {
|
|
774
|
-
|
|
610
|
+
fail2("the entity schema must describe an object with an id");
|
|
775
611
|
}
|
|
776
612
|
const required = new Set(Array.isArray(root.required) ? root.required : []);
|
|
777
613
|
const defs = { definitions: json.definitions, $defs: json.$defs };
|
|
@@ -1124,9 +960,9 @@ var ADMIN_METHODS = Object.freeze([
|
|
|
1124
960
|
"adminSubscribers",
|
|
1125
961
|
"adminReemit"
|
|
1126
962
|
]);
|
|
1127
|
-
var
|
|
963
|
+
var SPECS = /* @__PURE__ */ new WeakMap();
|
|
1128
964
|
function adminSpecOf(method) {
|
|
1129
|
-
return typeof method === "object" && method !== null ?
|
|
965
|
+
return typeof method === "object" && method !== null ? SPECS.get(method) : void 0;
|
|
1130
966
|
}
|
|
1131
967
|
var DESCRIBE = Object.freeze({
|
|
1132
968
|
adminList: "Lists every row for a service administrator, a page at a time (page numbers from 1, at most 100 rows a page), filtered and sorted by the declared fields.",
|
|
@@ -1138,41 +974,41 @@ var DESCRIBE = Object.freeze({
|
|
|
1138
974
|
adminSubscribers: "Counts the sockets subscribed to one row, per access level.",
|
|
1139
975
|
adminReemit: "Sends one row as it is now to every socket subscribed to it."
|
|
1140
976
|
});
|
|
1141
|
-
var
|
|
1142
|
-
function
|
|
977
|
+
var OPTIONS = ["entity", "filter", "sort", "editable", "expose", "describe"];
|
|
978
|
+
function isRecord2(value) {
|
|
1143
979
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1144
980
|
}
|
|
1145
|
-
function
|
|
981
|
+
function fail(message) {
|
|
1146
982
|
throw new TypeError(`admin.contract: ${message}`);
|
|
1147
983
|
}
|
|
1148
984
|
function quoted3(names) {
|
|
1149
985
|
return names.map((name) => `"${name}"`).join(", ");
|
|
1150
986
|
}
|
|
1151
987
|
function checkOptions(options) {
|
|
1152
|
-
if (!
|
|
1153
|
-
|
|
988
|
+
if (!isRecord2(options)) {
|
|
989
|
+
fail("options must be { entity, filter?, sort?, editable?, expose?, describe? }");
|
|
1154
990
|
}
|
|
1155
|
-
const unknownKey = Object.keys(options).find((key) => !
|
|
991
|
+
const unknownKey = Object.keys(options).find((key) => !OPTIONS.includes(key));
|
|
1156
992
|
if (unknownKey !== void 0) {
|
|
1157
|
-
|
|
993
|
+
fail(`unknown option "${unknownKey}"; the options are ${OPTIONS.join(", ")}`);
|
|
1158
994
|
}
|
|
1159
995
|
if (!isStandardSchema(options.entity)) {
|
|
1160
|
-
|
|
996
|
+
fail("entity must be the contract's entity schema");
|
|
1161
997
|
}
|
|
1162
998
|
return options;
|
|
1163
999
|
}
|
|
1164
|
-
function
|
|
1000
|
+
function checkFields(owner, names, fields) {
|
|
1165
1001
|
if (names === void 0) {
|
|
1166
1002
|
return Object.freeze([]);
|
|
1167
1003
|
}
|
|
1168
1004
|
const valid = Array.isArray(names) && names.every((name) => typeof name === "string") && new Set(names).size === names.length;
|
|
1169
1005
|
if (!valid) {
|
|
1170
|
-
|
|
1006
|
+
fail(`${owner} must be a list of distinct field names`);
|
|
1171
1007
|
}
|
|
1172
1008
|
for (const name of names) {
|
|
1173
1009
|
const field = fields.find((candidate) => candidate.name === name);
|
|
1174
1010
|
if (field === void 0 || field.type === "json") {
|
|
1175
|
-
|
|
1011
|
+
fail(`${owner}: "${name}" is not a field of the entity holding strings, numbers or booleans`);
|
|
1176
1012
|
}
|
|
1177
1013
|
}
|
|
1178
1014
|
return Object.freeze([...names]);
|
|
@@ -1183,14 +1019,14 @@ function checkEditable(names, fields) {
|
|
|
1183
1019
|
}
|
|
1184
1020
|
const valid = Array.isArray(names) && names.every((name) => typeof name === "string") && new Set(names).size === names.length;
|
|
1185
1021
|
if (!valid) {
|
|
1186
|
-
|
|
1022
|
+
fail("editable must be a list of distinct field names");
|
|
1187
1023
|
}
|
|
1188
1024
|
for (const name of names) {
|
|
1189
1025
|
if (ADMIN_NEVER_WRITABLE.includes(name)) {
|
|
1190
|
-
|
|
1026
|
+
fail(`editable: "${name}" is never editable; the database sets it`);
|
|
1191
1027
|
}
|
|
1192
1028
|
if (!fields.some((field) => field.name === name)) {
|
|
1193
|
-
|
|
1029
|
+
fail(`editable: "${name}" is not a field of the entity`);
|
|
1194
1030
|
}
|
|
1195
1031
|
}
|
|
1196
1032
|
return Object.freeze([...names]);
|
|
@@ -1201,7 +1037,7 @@ function exposedOf(expose) {
|
|
|
1201
1037
|
}
|
|
1202
1038
|
const valid = Array.isArray(expose) && expose.length > 0 && expose.every((name) => ADMIN_METHODS.some((known) => known === name)) && new Set(expose).size === expose.length;
|
|
1203
1039
|
if (!valid) {
|
|
1204
|
-
|
|
1040
|
+
fail(`expose must name one or more distinct admin methods: ${quoted3(ADMIN_METHODS)}`);
|
|
1205
1041
|
}
|
|
1206
1042
|
return expose;
|
|
1207
1043
|
}
|
|
@@ -1209,15 +1045,15 @@ function describedOf(names, describe) {
|
|
|
1209
1045
|
if (describe === void 0) {
|
|
1210
1046
|
return {};
|
|
1211
1047
|
}
|
|
1212
|
-
if (!
|
|
1213
|
-
|
|
1048
|
+
if (!isRecord2(describe)) {
|
|
1049
|
+
fail("describe must map method names to descriptions");
|
|
1214
1050
|
}
|
|
1215
1051
|
for (const [name, text] of Object.entries(describe)) {
|
|
1216
1052
|
if (!names.some((added) => added === name)) {
|
|
1217
|
-
|
|
1053
|
+
fail(`describe names "${name}", which these options do not add`);
|
|
1218
1054
|
}
|
|
1219
1055
|
if (typeof text !== "string" || text.length === 0) {
|
|
1220
|
-
|
|
1056
|
+
fail(`describe for "${name}" must be a non-empty string`);
|
|
1221
1057
|
}
|
|
1222
1058
|
}
|
|
1223
1059
|
return describe;
|
|
@@ -1255,15 +1091,15 @@ function build(name, base, writable, describe) {
|
|
|
1255
1091
|
return mutation({ input: idInput(), output: adminSubscribersOutput(), describe });
|
|
1256
1092
|
}
|
|
1257
1093
|
}
|
|
1258
|
-
function
|
|
1094
|
+
function contract(options) {
|
|
1259
1095
|
const checked = checkOptions(options);
|
|
1260
1096
|
const { entity } = checked;
|
|
1261
|
-
const fields = entityFieldsOf(entity,
|
|
1097
|
+
const fields = entityFieldsOf(entity, fail);
|
|
1262
1098
|
const editable = checkEditable(checked.editable, fields);
|
|
1263
1099
|
const base = Object.freeze({
|
|
1264
1100
|
entity,
|
|
1265
|
-
filter:
|
|
1266
|
-
sort:
|
|
1101
|
+
filter: checkFields("filter", checked.filter, fields),
|
|
1102
|
+
sort: checkFields("sort", checked.sort, fields),
|
|
1267
1103
|
fields,
|
|
1268
1104
|
editable
|
|
1269
1105
|
});
|
|
@@ -1274,12 +1110,12 @@ function contract2(options) {
|
|
|
1274
1110
|
for (const name of names) {
|
|
1275
1111
|
const own = described[name];
|
|
1276
1112
|
const def = build(name, base, writable, typeof own === "string" ? own : DESCRIBE[name]);
|
|
1277
|
-
|
|
1113
|
+
SPECS.set(def, Object.freeze({ ...base, method: name }));
|
|
1278
1114
|
methods[name] = def;
|
|
1279
1115
|
}
|
|
1280
1116
|
return Object.freeze(methods);
|
|
1281
1117
|
}
|
|
1282
|
-
var admin = Object.freeze({ contract
|
|
1118
|
+
var admin = Object.freeze({ contract });
|
|
1283
1119
|
|
|
1284
1120
|
export {
|
|
1285
1121
|
query,
|
|
@@ -1297,6 +1133,8 @@ export {
|
|
|
1297
1133
|
MAX_CURSOR_LENGTH,
|
|
1298
1134
|
pagingIssues,
|
|
1299
1135
|
listInput,
|
|
1136
|
+
itemsIssues,
|
|
1137
|
+
itemsJson,
|
|
1300
1138
|
pageOutput,
|
|
1301
1139
|
CRUD_MAX_IDS,
|
|
1302
1140
|
idInput,
|
|
@@ -1306,14 +1144,9 @@ export {
|
|
|
1306
1144
|
reorderInput,
|
|
1307
1145
|
nullOutput,
|
|
1308
1146
|
countOutput,
|
|
1309
|
-
SEARCH_DEFAULT_LIMIT,
|
|
1310
|
-
SEARCH_MAX_LIMIT,
|
|
1311
|
-
SEARCH_DEFAULT_MIN_LENGTH,
|
|
1312
|
-
SEARCH_MAX_QUERY_LENGTH,
|
|
1313
|
-
searchSpecOf,
|
|
1314
|
-
search,
|
|
1315
1147
|
ADMIN_FIELD_TYPES,
|
|
1316
1148
|
ADMIN_NEVER_WRITABLE,
|
|
1149
|
+
writtenData,
|
|
1317
1150
|
ADMIN_DEFAULT_PAGE_SIZE,
|
|
1318
1151
|
ADMIN_MAX_PAGE_SIZE,
|
|
1319
1152
|
ADMIN_MAX_PAGE,
|
|
@@ -1321,4 +1154,4 @@ export {
|
|
|
1321
1154
|
adminSpecOf,
|
|
1322
1155
|
admin
|
|
1323
1156
|
};
|
|
1324
|
-
//# sourceMappingURL=chunk-
|
|
1157
|
+
//# sourceMappingURL=chunk-MPQAT55F.js.map
|