@ultimabase/graphql 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +4 -2
  3. package/dist/data-types.d.ts +15 -0
  4. package/dist/data-types.d.ts.map +1 -0
  5. package/dist/data-types.js +53 -0
  6. package/dist/data.d.ts +34 -0
  7. package/dist/data.d.ts.map +1 -0
  8. package/dist/data.js +207 -0
  9. package/dist/depth.d.ts +6 -0
  10. package/dist/depth.d.ts.map +1 -0
  11. package/dist/depth.js +67 -0
  12. package/dist/errors.d.ts +7 -0
  13. package/dist/errors.d.ts.map +1 -0
  14. package/dist/errors.js +42 -0
  15. package/dist/functions.d.ts +28 -0
  16. package/dist/functions.d.ts.map +1 -0
  17. package/dist/functions.js +195 -0
  18. package/dist/graphiql.d.ts +3 -0
  19. package/dist/graphiql.d.ts.map +1 -0
  20. package/dist/graphiql.js +46 -0
  21. package/dist/index.d.ts +47 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +165 -0
  24. package/dist/inputs.d.ts +20 -0
  25. package/dist/inputs.d.ts.map +1 -0
  26. package/dist/inputs.js +236 -0
  27. package/dist/json-fields.d.ts +12 -0
  28. package/dist/json-fields.d.ts.map +1 -0
  29. package/dist/json-fields.js +66 -0
  30. package/dist/json-schema.d.ts +7 -0
  31. package/dist/json-schema.d.ts.map +1 -0
  32. package/dist/json-schema.js +16 -0
  33. package/dist/live-stream.d.ts +59 -0
  34. package/dist/live-stream.d.ts.map +1 -0
  35. package/dist/live-stream.js +136 -0
  36. package/dist/mutations.d.ts +9 -0
  37. package/dist/mutations.d.ts.map +1 -0
  38. package/dist/mutations.js +228 -0
  39. package/dist/names.d.ts +8 -0
  40. package/dist/names.d.ts.map +1 -0
  41. package/dist/names.js +14 -0
  42. package/dist/outputs.d.ts +16 -0
  43. package/dist/outputs.d.ts.map +1 -0
  44. package/dist/outputs.js +177 -0
  45. package/dist/overlap.d.ts +5 -0
  46. package/dist/overlap.d.ts.map +1 -0
  47. package/dist/overlap.js +82 -0
  48. package/dist/pages.d.ts +11 -0
  49. package/dist/pages.d.ts.map +1 -0
  50. package/dist/pages.js +22 -0
  51. package/dist/registry.d.ts +80 -0
  52. package/dist/registry.d.ts.map +1 -0
  53. package/dist/registry.js +97 -0
  54. package/dist/reread.d.ts +9 -0
  55. package/dist/reread.d.ts.map +1 -0
  56. package/dist/reread.js +116 -0
  57. package/dist/rows.d.ts +23 -0
  58. package/dist/rows.d.ts.map +1 -0
  59. package/dist/rows.js +106 -0
  60. package/dist/scalars.d.ts +13 -0
  61. package/dist/scalars.d.ts.map +1 -0
  62. package/dist/scalars.js +88 -0
  63. package/dist/schema.d.ts +17 -0
  64. package/dist/schema.d.ts.map +1 -0
  65. package/dist/schema.js +130 -0
  66. package/dist/select.d.ts +24 -0
  67. package/dist/select.d.ts.map +1 -0
  68. package/dist/select.js +152 -0
  69. package/dist/sockets.d.ts +25 -0
  70. package/dist/sockets.d.ts.map +1 -0
  71. package/dist/sockets.js +234 -0
  72. package/dist/subscriptions.d.ts +14 -0
  73. package/dist/subscriptions.d.ts.map +1 -0
  74. package/dist/subscriptions.js +242 -0
  75. package/package.json +53 -3
  76. package/src/data-types.ts +100 -0
  77. package/src/data.ts +366 -0
  78. package/src/depth.ts +98 -0
  79. package/src/errors.ts +48 -0
  80. package/src/functions.ts +291 -0
  81. package/src/graphiql.ts +54 -0
  82. package/src/index.ts +231 -0
  83. package/src/inputs.ts +348 -0
  84. package/src/json-fields.ts +95 -0
  85. package/src/json-schema.ts +21 -0
  86. package/src/live-stream.ts +192 -0
  87. package/src/mutations.ts +317 -0
  88. package/src/names.ts +25 -0
  89. package/src/outputs.ts +267 -0
  90. package/src/overlap.ts +97 -0
  91. package/src/pages.ts +39 -0
  92. package/src/registry.ts +189 -0
  93. package/src/reread.ts +159 -0
  94. package/src/rows.ts +151 -0
  95. package/src/scalars.ts +110 -0
  96. package/src/schema.ts +189 -0
  97. package/src/select.ts +230 -0
  98. package/src/sockets.ts +291 -0
  99. package/src/subscriptions.ts +334 -0
package/dist/select.js ADDED
@@ -0,0 +1,152 @@
1
+ // A query's selection as one kernel read (D-107): the fields asked for, and
2
+ // each `many` relation's own page and count, read with its parent's rows.
3
+ import { badInput, readable } from '@ultimabase/http/internal';
4
+ import { getArgumentValues, getDirectiveValues, getNamedType, GraphQLIncludeDirective, GraphQLSkipDirective, isObjectType, Kind, } from 'graphql';
5
+ import { objectType } from './outputs.js';
6
+ import { plain } from './scalars.js';
7
+ import { listRead } from './pages.js';
8
+ function included(info, node) {
9
+ const skip = getDirectiveValues(GraphQLSkipDirective, node, info.variableValues);
10
+ if (skip?.['if'] === true)
11
+ return false;
12
+ const include = getDirectiveValues(GraphQLIncludeDirective, node, info.variableValues);
13
+ return include?.['if'] !== false;
14
+ }
15
+ // A fragment spread again adds nothing the first spread did not, so each is
16
+ // collected once: spreading each fragment twice into the next would otherwise
17
+ // cost 2^n visits.
18
+ function collect(info, set, into, key, spread) {
19
+ for (const selection of set?.selections ?? []) {
20
+ if (!included(info, selection))
21
+ continue;
22
+ if (selection.kind === Kind.FIELD) {
23
+ const found = into.get(key(selection));
24
+ if (found === undefined)
25
+ into.set(key(selection), [selection]);
26
+ else
27
+ found.push(selection);
28
+ }
29
+ else if (selection.kind === Kind.INLINE_FRAGMENT) {
30
+ collect(info, selection.selectionSet, into, key, spread);
31
+ }
32
+ else if (!spread.has(selection.name.value)) {
33
+ spread.add(selection.name.value);
34
+ const fragment = info.fragments[selection.name.value];
35
+ collect(info, fragment?.selectionSet, into, key, spread);
36
+ }
37
+ }
38
+ }
39
+ const byName = (node) => node.name.value;
40
+ /** The fields under some nodes, by name, through fragments and directives. */
41
+ export function grouped(info, nodes, key = byName) {
42
+ const into = new Map();
43
+ const spread = new Set();
44
+ for (const node of nodes)
45
+ collect(info, node.selectionSet, into, key, spread);
46
+ return into;
47
+ }
48
+ /** One relation's arguments, the same under every alias. */
49
+ function argsOf(type, name, nodes, info) {
50
+ const field = type.getFields()[name];
51
+ const [first, ...others] = nodes.map((node) => field === undefined
52
+ ? {}
53
+ : getArgumentValues(field, node, info.variableValues));
54
+ const key = JSON.stringify(first);
55
+ if (others.some((args) => JSON.stringify(args) !== key)) {
56
+ throw badInput(`${name} is selected twice with different arguments; read it once.`);
57
+ }
58
+ return first ?? {};
59
+ }
60
+ /** Under a connection, the nodes its edges select. */
61
+ export function nodesOf(info, nodes) {
62
+ const edges = grouped(info, nodes).get('edges') ?? [];
63
+ return [...(grouped(info, edges).get('node') ?? [])];
64
+ }
65
+ /** What a root list selects of its rows: under `edges { node }`. */
66
+ export function rowNodes(registry, info) {
67
+ return registry.settings.pagination === 'list'
68
+ ? info.fieldNodes
69
+ : nodesOf(info, info.fieldNodes);
70
+ }
71
+ const system = new Set(['id', 'createdAt', 'updatedAt']);
72
+ /** A virtual's read: `true`, its arguments, and a model's `select` (D-151). */
73
+ function virtualRead(registry, info, type, virtual, nodes, master) {
74
+ const { output } = virtual;
75
+ const args = plain(argsOf(type, virtual.name, nodes, info));
76
+ const input = typeof args === 'object' && args !== null ? args : {};
77
+ if (output.kind !== 'model')
78
+ return virtual.input === undefined || input;
79
+ const target = objectType(registry, output.name);
80
+ return { ...input, select: selectOf(registry, info, target, nodes, master) };
81
+ }
82
+ /** The kernel's `select` for what a query asks of a model's rows. */
83
+ export function selectOf(registry, info, type, nodes, master) {
84
+ const entry = registry.entry(type.name);
85
+ const select = { id: true };
86
+ // The connections' `totalCount`, each the count of its `where` (D-134).
87
+ const counts = {};
88
+ for (const [name, fieldNodes] of grouped(info, nodes)) {
89
+ if (name === '__typename')
90
+ continue;
91
+ if (system.has(name)) {
92
+ select[name] = true;
93
+ continue;
94
+ }
95
+ const virtual = entry.exposed.virtuals.find((v) => v.name === name);
96
+ if (virtual !== undefined) {
97
+ select[name] = virtualRead(registry, info, type, virtual, fieldNodes, master);
98
+ continue;
99
+ }
100
+ const field = entry.model.fields.find((f) => f.name === name);
101
+ if (field === undefined) {
102
+ // A field `extend` added: its resolver gets the row's scalars.
103
+ for (const scalar of readable(entry)) {
104
+ if (scalar.kind === 'scalar')
105
+ select[scalar.name] = true;
106
+ }
107
+ for (const key of system)
108
+ select[key] = true;
109
+ continue;
110
+ }
111
+ if (field.kind === 'scalar') {
112
+ select[name] = true;
113
+ continue;
114
+ }
115
+ const target = registry.entry(field.target);
116
+ const targetObject = objectType(registry, field.target);
117
+ if (field.kind === 'one') {
118
+ select[name] = {
119
+ select: selectOf(registry, info, targetObject, fieldNodes, master),
120
+ };
121
+ continue;
122
+ }
123
+ const args = argsOf(type, name, fieldNodes, info);
124
+ const { read } = listRead(registry, target, args, master);
125
+ const list = registry.settings.pagination === 'list';
126
+ const below = list ? fieldNodes : nodesOf(info, fieldNodes);
127
+ select[name] = {
128
+ ...read,
129
+ select: selectOf(registry, info, targetObject, below, master),
130
+ };
131
+ if (!list && grouped(info, fieldNodes).has('totalCount')) {
132
+ counts[name] =
133
+ read['where'] === undefined ? true : { where: read['where'] };
134
+ }
135
+ }
136
+ if (Object.keys(counts).length > 0)
137
+ select['_count'] = { select: counts };
138
+ return select;
139
+ }
140
+ /** `context.select(info)`, for a field that returns a model or a list of one. */
141
+ export function selectFor(registry, master) {
142
+ return (info) => {
143
+ const type = getNamedType(info.returnType);
144
+ if (!isObjectType(type) || !registry.entries.has(type.name)) {
145
+ throw new Error(`context.select(info) reads a model's rows; ${info.parentType.name}.${info.fieldName} returns ${type.name}.`);
146
+ }
147
+ return {
148
+ ...selectOf(registry, info, type, info.fieldNodes, master),
149
+ id: true,
150
+ };
151
+ };
152
+ }
@@ -0,0 +1,25 @@
1
+ import type { HttpApi } from '@ultimabase/http';
2
+ import type { RealtimeApi } from '@ultimabase/realtime';
3
+ import type { DocumentNode, ExecutionArgs, ExecutionResult, GraphQLSchema, Source, ValidationRule } from 'graphql';
4
+ import { GraphQLError } from 'graphql';
5
+ import type { Caller, Client, Logger } from 'ultimabase';
6
+ import type { formatter } from './errors.ts';
7
+ import type { Context } from './registry.ts';
8
+ /** How the GraphQL plugin runs a document, over HTTP as over WebSocket. */
9
+ export type Operations = {
10
+ readonly schema: GraphQLSchema;
11
+ readonly parse: (source: string | Source) => DocumentNode;
12
+ readonly rules: (master: boolean) => readonly ValidationRule[];
13
+ readonly validate: (document: DocumentNode, rules?: readonly ValidationRule[]) => readonly GraphQLError[];
14
+ readonly execute: (args: ExecutionArgs) => Promise<ExecutionResult>;
15
+ readonly context: (caller: Caller, client: Client) => Context;
16
+ readonly hideSuggestions: boolean;
17
+ readonly format: ReturnType<typeof formatter>;
18
+ readonly logger: Logger;
19
+ };
20
+ /**
21
+ * Serves `path`'s WebSocket upgrades with `operations`, each connection read
22
+ * by `realtime`; returns what closes them.
23
+ */
24
+ export declare function serveSockets(http: HttpApi, path: string, realtime: RealtimeApi, operations: Operations): () => Promise<void>;
25
+ //# sourceMappingURL=sockets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sockets.d.ts","sourceRoot":"","sources":["../src/sockets.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAW,MAAM,kBAAkB,CAAA;AACxD,OAAO,KAAK,EAAE,WAAW,EAAU,MAAM,sBAAsB,CAAA;AAC/D,OAAO,KAAK,EACX,YAAY,EACZ,aAAa,EACb,eAAe,EACf,aAAa,EACb,MAAM,EACN,cAAc,EACd,MAAM,SAAS,CAAA;AAChB,OAAO,EAGN,YAAY,EAGZ,MAAM,SAAS,CAAA;AAIhB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAExD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAG5C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAE5C,2EAA2E;AAC3E,MAAM,MAAM,UAAU,GAAG;IACxB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;IAC9B,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,KAAK,YAAY,CAAA;IACzD,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,SAAS,cAAc,EAAE,CAAA;IAC9D,QAAQ,CAAC,QAAQ,EAAE,CAClB,QAAQ,EAAE,YAAY,EACtB,KAAK,CAAC,EAAE,SAAS,cAAc,EAAE,KAC7B,SAAS,YAAY,EAAE,CAAA;IAC5B,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,OAAO,CAAC,eAAe,CAAC,CAAA;IACnE,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAA;IAC7D,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAA;IACjC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,CAAA;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACvB,CAAA;AAqFD;;;GAGG;AACH,wBAAgB,YAAY,CAC3B,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,WAAW,EACrB,UAAU,EAAE,UAAU,GACpB,MAAM,OAAO,CAAC,IAAI,CAAC,CAwJrB"}
@@ -0,0 +1,234 @@
1
+ import { createSourceEventStream, executeSubscriptionEvent, GraphQLError, mapSourceToResponseEvent, validateSubscriptionArgs, } from 'graphql';
2
+ import { CloseCode, handleProtocols, makeServer } from 'graphql-ws';
3
+ import { WebSocketServer } from 'ws';
4
+ import { PublicError } from 'ultimabase';
5
+ import { LiveEvent, lives } from './live-stream.js';
6
+ /** A client's buffer past which its changes are dropped, and the most it sends us. */
7
+ const maxBuffer = 1024 * 1024;
8
+ /** Between pings; a socket that has not answered the last one is cut. */
9
+ const keepAlive = 12_000;
10
+ const unset = () => {
11
+ throw new Error('A cookie is set over HTTP: a WebSocket cannot set one.');
12
+ };
13
+ /** The upgrade request's cookies, which a WebSocket cannot set. */
14
+ function cookiesOf(request) {
15
+ const cookies = new Map();
16
+ for (const part of (request.headers.get('cookie') ?? '').split(';')) {
17
+ const at = part.indexOf('=');
18
+ if (at <= 0)
19
+ continue;
20
+ const name = part.slice(0, at).trim();
21
+ if (cookies.has(name))
22
+ continue;
23
+ try {
24
+ cookies.set(name, decodeURIComponent(part.slice(at + 1).trim()));
25
+ }
26
+ catch {
27
+ // A value that is no URI component is no cookie this API wrote.
28
+ }
29
+ }
30
+ return { get: (name) => cookies.get(name), set: unset, clear: unset };
31
+ }
32
+ // How each socket sends, counted by its channel once it has one.
33
+ const sends = new WeakMap();
34
+ /** The socket's channel: what it holds unsent, and when that is written. */
35
+ function channelOf(viewer, socket) {
36
+ let pending = 0;
37
+ const waiters = [];
38
+ const written = () => {
39
+ pending -= 1;
40
+ if (pending > 0)
41
+ return;
42
+ for (const then of waiters.splice(0))
43
+ then();
44
+ };
45
+ sends.set(socket, (data) => {
46
+ pending += 1;
47
+ socket.send(data, written);
48
+ });
49
+ return {
50
+ viewer,
51
+ full: () => socket.bufferedAmount > maxBuffer,
52
+ drained: (then) => {
53
+ if (pending === 0)
54
+ then();
55
+ else
56
+ waiters.push(then);
57
+ },
58
+ forbid: () => socket.close(CloseCode.Forbidden, 'Forbidden'),
59
+ kept: new Map(),
60
+ };
61
+ }
62
+ const text = (data) => Buffer.concat([data].flat().map((part) => new Uint8Array(part))).toString();
63
+ const isLive = (value) => typeof value === 'object' && value !== null;
64
+ /** The authorization `connection_init` gives, over the upgrade's own. */
65
+ function requestOf(upgrade, params) {
66
+ const headers = new Headers(upgrade.request.headers);
67
+ const authorization = typeof params === 'object' && params !== null
68
+ ? Reflect.get(params, 'authorization')
69
+ : undefined;
70
+ if (typeof authorization === 'string') {
71
+ headers.set('authorization', authorization);
72
+ }
73
+ return new Request(upgrade.request.url, { headers });
74
+ }
75
+ /**
76
+ * Serves `path`'s WebSocket upgrades with `operations`, each connection read
77
+ * by `realtime`; returns what closes them.
78
+ */
79
+ export function serveSockets(http, path, realtime, operations) {
80
+ const { schema, logger, format, hideSuggestions } = operations;
81
+ const formatted = (errors) => errors.map((error) => {
82
+ const shown = format(error);
83
+ return shown instanceof GraphQLError
84
+ ? shown.toJSON()
85
+ : { message: shown.message };
86
+ });
87
+ const server = makeServer({
88
+ schema,
89
+ onConnect: async (ctx) => {
90
+ const { upgrade, socket } = ctx.extra;
91
+ try {
92
+ const request = requestOf(upgrade, ctx.connectionParams);
93
+ const viewer = await realtime.connect(request);
94
+ if (socket.readyState !== socket.OPEN) {
95
+ viewer.close();
96
+ return false;
97
+ }
98
+ ctx.extra.viewer = viewer;
99
+ ctx.extra.channel = channelOf(viewer, socket);
100
+ return true;
101
+ }
102
+ catch (error) {
103
+ if (!(error instanceof PublicError)) {
104
+ logger.error({ err: error }, 'A WebSocket caller was not read.');
105
+ }
106
+ return false;
107
+ }
108
+ },
109
+ onSubscribe: (ctx, _, payload) => {
110
+ const { viewer, channel, upgrade, socket } = ctx.extra;
111
+ if (viewer === undefined || channel === undefined || viewer.closed) {
112
+ socket.close(CloseCode.Forbidden, 'Forbidden');
113
+ return [new GraphQLError('Forbidden')];
114
+ }
115
+ let document;
116
+ try {
117
+ document = operations.parse(payload.query);
118
+ }
119
+ catch (error) {
120
+ if (error instanceof GraphQLError)
121
+ return [error];
122
+ throw error;
123
+ }
124
+ const { caller } = viewer;
125
+ const master = caller.kind === 'master';
126
+ const refused = operations.validate(document, operations.rules(master));
127
+ if (refused.length > 0)
128
+ return refused;
129
+ const client = {
130
+ address: upgrade.address,
131
+ cookies: cookiesOf(upgrade.request),
132
+ };
133
+ const contextValue = operations.context(caller, client);
134
+ const live = payload.extensions?.['live'];
135
+ const page = isLive(live) && live.mode === 'page';
136
+ const resume = isLive(live) ? live.resume : undefined;
137
+ lives.set(contextValue, {
138
+ channel,
139
+ page,
140
+ ...(typeof resume === 'string' && { resume }),
141
+ });
142
+ return {
143
+ schema,
144
+ document,
145
+ operationName: payload.operationName,
146
+ variableValues: payload.variables,
147
+ contextValue,
148
+ };
149
+ },
150
+ execute: async (args) => operations.execute(args),
151
+ subscribe: async (args) => {
152
+ const validated = validateSubscriptionArgs({ ...args, hideSuggestions });
153
+ if (!('schema' in validated))
154
+ return { errors: validated };
155
+ const source = await createSourceEventStream(validated);
156
+ if (!(Symbol.asyncIterator in source))
157
+ return source;
158
+ return mapSourceToResponseEvent(validated, source, async (event) => {
159
+ const result = await executeSubscriptionEvent(event);
160
+ const { rootValue } = event;
161
+ return rootValue instanceof LiveEvent
162
+ ? rootValue.finish(result)
163
+ : result;
164
+ });
165
+ },
166
+ onNext: (_ctx, _id, _payload, _args, { data, errors, extensions }) => ({
167
+ ...(data === undefined ? {} : { data }),
168
+ ...(errors === undefined ? {} : { errors: formatted(errors) }),
169
+ ...(extensions === undefined ? {} : { extensions }),
170
+ }),
171
+ onError: (_ctx, _id, _payload, errors) => formatted(errors),
172
+ });
173
+ const sockets = new WebSocketServer({
174
+ noServer: true,
175
+ maxPayload: maxBuffer,
176
+ perMessageDeflate: { threshold: 1024 },
177
+ handleProtocols,
178
+ });
179
+ const beats = new WeakMap();
180
+ const opened = (socket, upgrade) => {
181
+ const extra = { upgrade, socket };
182
+ let alive = true;
183
+ socket.on('pong', () => {
184
+ alive = true;
185
+ });
186
+ beats.set(socket, () => {
187
+ if (!alive)
188
+ return socket.terminate();
189
+ alive = false;
190
+ socket.ping();
191
+ });
192
+ const closed = server.opened({
193
+ protocol: socket.protocol,
194
+ send: async (data) => {
195
+ if (socket.readyState !== socket.OPEN)
196
+ return;
197
+ const send = sends.get(socket);
198
+ if (send === undefined)
199
+ socket.send(data);
200
+ else
201
+ send(data);
202
+ },
203
+ close: (code, reason) => socket.close(code, reason),
204
+ onMessage: (received) => {
205
+ socket.on('message', (data) => {
206
+ received(text(data)).catch((error) => {
207
+ logger.error({ err: error }, 'A WebSocket message failed.');
208
+ socket.close(CloseCode.InternalServerError, 'Internal error');
209
+ });
210
+ });
211
+ },
212
+ }, extra);
213
+ socket.once('close', (code, reason) => {
214
+ void closed(code, String(reason));
215
+ extra.viewer?.close();
216
+ });
217
+ };
218
+ const beating = setInterval(() => {
219
+ for (const socket of sockets.clients)
220
+ beats.get(socket)?.();
221
+ }, keepAlive);
222
+ beating.unref();
223
+ http.upgrade(path, (upgrade) => {
224
+ sockets.handleUpgrade(upgrade.incoming, upgrade.socket, upgrade.head, (socket) => opened(socket, upgrade));
225
+ });
226
+ return async () => {
227
+ clearInterval(beating);
228
+ for (const socket of sockets.clients)
229
+ socket.close(1001, 'Going away');
230
+ await new Promise((done) => {
231
+ sockets.close(() => done());
232
+ });
233
+ };
234
+ }
@@ -0,0 +1,14 @@
1
+ import type { GraphQLFieldConfig, GraphQLResolveInfo } from 'graphql';
2
+ import type { Args, Context, Registry } from './registry.ts';
3
+ type Fields = {
4
+ [field: string]: GraphQLFieldConfig<unknown, Context, Args>;
5
+ };
6
+ /**
7
+ * A live function's subscription (D-154), as its query field: `value` runs
8
+ * it as the subscriber each time it may have changed.
9
+ */
10
+ export declare function followed(owner: string, field: Omit<GraphQLFieldConfig<unknown, Context, Args>, 'resolve'>, value: (args: Args, context: Context, info: GraphQLResolveInfo) => Promise<unknown>): GraphQLFieldConfig<unknown, Context, Args>;
11
+ /** The live models' subscriptions, by root name. */
12
+ export declare function subscriptionFields(registry: Registry, live: (model: string) => boolean): Fields;
13
+ export {};
14
+ //# sourceMappingURL=subscriptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscriptions.d.ts","sourceRoot":"","sources":["../src/subscriptions.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAEX,kBAAkB,EAClB,kBAAkB,EAClB,MAAM,SAAS,CAAA;AAQhB,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAS,QAAQ,EAAE,MAAM,eAAe,CAAA;AAInE,KAAK,MAAM,GAAG;IAAE,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;CAAE,CAAA;AAmQ7E;;;GAGG;AACH,wBAAgB,QAAQ,CACvB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,IAAI,CAAC,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,CAAC,EAClE,KAAK,EAAE,CACN,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,kBAAkB,KACpB,OAAO,CAAC,OAAO,CAAC,GACnB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,CAkB5C;AAED,oDAAoD;AACpD,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,GAC9B,MAAM,CAQR"}
@@ -0,0 +1,242 @@
1
+ // The subscriptions (D-154): each live model's row and list, with its query's
2
+ // name, arguments and type, followed by the realtime engine. The first result
3
+ // is `{ data }`; a list then sends only what changed, under `extensions.live`,
4
+ // or its whole window in page mode. A list's first result carries the token
5
+ // that resumes it once completed, grown.
6
+ import { randomUUID } from 'node:crypto';
7
+ import { applied, badInput, connection, pageOf, run, } from '@ultimabase/http/internal';
8
+ import { GraphQLError, GraphQLList, GraphQLNonNull } from 'graphql';
9
+ import { objectOf, uniqueInput, uniqueWhere } from './inputs.js';
10
+ import { LiveEvent, liveStream, lives, resumed } from './live-stream.js';
11
+ import { connectionType, isMaster, listArgs, objectType } from './outputs.js';
12
+ import { whereOf } from './pages.js';
13
+ import { grouped, rowNodes, selectOf } from './select.js';
14
+ const responseKey = (node) => node.alias?.value ?? node.name.value;
15
+ /** The response key of the first field named `name` the info's field selects. */
16
+ function keyOf(info, name) {
17
+ for (const [key, [first]] of grouped(info, info.fieldNodes, responseKey)) {
18
+ if (first.name.value === name)
19
+ return key;
20
+ }
21
+ return undefined;
22
+ }
23
+ const valueAt = (value, key) => key === undefined || typeof value !== 'object' || value === null
24
+ ? undefined
25
+ : Reflect.get(value, key);
26
+ const fromTop = { size: undefined, backward: false };
27
+ /** A window from the top of its order, as a connection. */
28
+ function connectionOf(rows, hasMore, totalCount) {
29
+ const { edges, pageInfo } = connection(rows, fromTop, (row) => row);
30
+ return { edges, pageInfo: { ...pageInfo, hasNextPage: hasMore }, totalCount };
31
+ }
32
+ const revised = (result, revision, resume) => ({
33
+ ...result,
34
+ extensions: {
35
+ ...result.extensions,
36
+ live: { revision, ...(resume !== undefined && { resume }) },
37
+ },
38
+ });
39
+ /** Where a list's root, its edges, page info and count are in the result. */
40
+ const keysOf = (info, connected) => [
41
+ String(info.path.key),
42
+ ...(connected
43
+ ? [
44
+ keyOf(info, 'edges'),
45
+ keyOf(info, 'pageInfo'),
46
+ keyOf(info, 'totalCount'),
47
+ ]
48
+ : [undefined, undefined, undefined]),
49
+ ];
50
+ /**
51
+ * What a live read's messages run, with what it sent so far; a list's
52
+ * first result carries `resume`.
53
+ */
54
+ function follower(info, connected, page, resume) {
55
+ const [key, edgesKey, pageInfoKey, totalCountKey] = keysOf(info, connected);
56
+ let token = resume;
57
+ let window = [];
58
+ let hasMore = false;
59
+ let count;
60
+ let pageInfo = '';
61
+ const rootOf = (rows) => connected ? connectionOf(rows, hasMore, count) : rows;
62
+ const sent = (result) => {
63
+ const at = valueAt(result.data, key);
64
+ pageInfo = JSON.stringify(valueAt(at, pageInfoKey)) ?? '';
65
+ return result;
66
+ };
67
+ return (message) => {
68
+ if (message.kind === 'row' || message.kind === 'value') {
69
+ const { revision } = message;
70
+ const root = message.kind === 'row' ? message.row : message.value;
71
+ return new LiveEvent(root, (result) => revised(result, revision));
72
+ }
73
+ const { revision } = message;
74
+ hasMore = message.hasMore ?? hasMore;
75
+ count = message.totalCount ?? count;
76
+ if (message.kind === 'reset' || page) {
77
+ window =
78
+ message.kind === 'reset'
79
+ ? message.rows
80
+ : applied(window, message.changes, (row) => row);
81
+ const shown = token;
82
+ token = undefined;
83
+ return new LiveEvent(rootOf(window), (result) => revised(sent(result), revision, shown));
84
+ }
85
+ const { changes } = message;
86
+ window = applied(window, changes, ({ id }) => ({ id }));
87
+ const upserts = changes.flatMap((change) => 'upsert' in change ? [change.upsert] : []);
88
+ const root = connected
89
+ ? {
90
+ ...connectionOf(window, hasMore, count),
91
+ edges: connectionOf(upserts, false, undefined).edges,
92
+ }
93
+ : upserts;
94
+ return new LiveEvent(root, (result) => {
95
+ const at = valueAt(result.data, key);
96
+ const upserted = connected ? valueAt(at, edgesKey) : at;
97
+ const shown = Array.isArray(upserted) ? upserted : [];
98
+ let next = 0;
99
+ const live = { revision };
100
+ if (!connected || edgesKey !== undefined) {
101
+ live['changes'] = changes.map((change) => {
102
+ if ('remove' in change)
103
+ return change;
104
+ next += 1;
105
+ return { upsert: shown[next - 1], after: change.after };
106
+ });
107
+ }
108
+ if (message.totalCount !== undefined && totalCountKey !== undefined) {
109
+ live['totalCount'] = valueAt(at, totalCountKey);
110
+ }
111
+ const before = pageInfo;
112
+ sent(result);
113
+ if (pageInfoKey !== undefined && pageInfo !== before) {
114
+ live['pageInfo'] = valueAt(at, pageInfoKey);
115
+ }
116
+ return {
117
+ ...(result.errors && { errors: result.errors }),
118
+ extensions: { ...result.extensions, live },
119
+ };
120
+ });
121
+ };
122
+ }
123
+ /**
124
+ * The last result of a live read the engine closed, or of a live function
125
+ * that failed with `error`, which the error formatter shows or masks.
126
+ */
127
+ const closedEvent = (error) => new LiveEvent(null, () => ({
128
+ errors: [
129
+ error === undefined
130
+ ? new GraphQLError('The live read closed.', {
131
+ extensions: { code: 'INTERNAL' },
132
+ })
133
+ : new GraphQLError('A live function failed.', {
134
+ originalError: error instanceof Error
135
+ ? error
136
+ : new Error('A live function threw.', { cause: error }),
137
+ }),
138
+ ],
139
+ }));
140
+ /** What a subscription's event holds; its stream only yields `LiveEvent`s. */
141
+ const rootOf = (event) => Reflect.get(Object(event), 'root');
142
+ /** The WebSocket operation `context` runs in. */
143
+ function liveOf(context) {
144
+ const live = lives.get(context);
145
+ if (live === undefined)
146
+ throw badInput('A subscription runs over WebSocket.');
147
+ return live;
148
+ }
149
+ /** `post(where)` and `posts(where, orderBy, first)` of a live model. */
150
+ function liveFields(registry, entry) {
151
+ const name = entry.model.name;
152
+ const type = objectType(registry, name);
153
+ const { pagination } = registry.settings;
154
+ const connected = pagination === 'connection';
155
+ const list = connected
156
+ ? new GraphQLNonNull(connectionType(registry, `${name}Connection`, name))
157
+ : new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(type)));
158
+ // A window starts at the top of its order (D-154).
159
+ const windowArgs = Object.fromEntries(Object.entries(listArgs(registry, name)).filter(([arg]) => !['after', 'last', 'before', 'skip'].includes(arg)));
160
+ return {
161
+ [entry.single]: {
162
+ type,
163
+ args: {
164
+ where: { type: new GraphQLNonNull(uniqueInput(registry, name)) },
165
+ },
166
+ subscribe: async (_, args, context, info) => {
167
+ const { channel, page } = liveOf(context);
168
+ const where = uniqueWhere(registry, name, objectOf(args['where']));
169
+ const master = isMaster(context);
170
+ const select = selectOf(registry, info, type, info.fieldNodes, master);
171
+ return run(async () => liveStream(channel, async (send) => channel.viewer.row(name, { where, select }, send), follower(info, false, page), closedEvent));
172
+ },
173
+ resolve: rootOf,
174
+ },
175
+ [entry.list]: {
176
+ type: list,
177
+ args: windowArgs,
178
+ subscribe: async (_, args, context, info) => {
179
+ const { channel, page } = liveOf(context);
180
+ const master = isMaster(context);
181
+ const { read: paged, relay } = pageOf(pagination, entry, args, master);
182
+ const asked = relay === undefined ? paged['take'] : relay.size;
183
+ const rows = typeof asked === 'number' ? asked : entry.take;
184
+ const arg = connected ? 'first' : 'take';
185
+ if (page && rows > entry.maxTake) {
186
+ throw badInput(`${arg} is at most ${entry.maxTake} on ${name} in page mode.`);
187
+ }
188
+ const nodes = rowNodes(registry, info);
189
+ const select = selectOf(registry, info, type, nodes, master);
190
+ const { orderBy: order } = args;
191
+ const read = {
192
+ ...whereOf(registry, name, args),
193
+ ...(order === undefined || order === null ? {} : { orderBy: order }),
194
+ select,
195
+ totalCount: connected && grouped(info, info.fieldNodes).has('totalCount'),
196
+ };
197
+ const signature = JSON.stringify([
198
+ name,
199
+ page,
200
+ keysOf(info, connected),
201
+ read,
202
+ ]);
203
+ const { resume } = liveOf(context);
204
+ return run(async () => {
205
+ const taken = resume === undefined
206
+ ? undefined
207
+ : await resumed(channel, resume, signature, rows);
208
+ if (taken !== undefined)
209
+ return taken;
210
+ const keep = { token: randomUUID(), signature, first: rows };
211
+ return liveStream(channel, async (send) => channel.viewer.list(name, { ...read, first: rows }, send), follower(info, connected, page, keep.token), closedEvent, keep);
212
+ });
213
+ },
214
+ resolve: rootOf,
215
+ },
216
+ };
217
+ }
218
+ /**
219
+ * A live function's subscription (D-154), as its query field: `value` runs
220
+ * it as the subscriber each time it may have changed.
221
+ */
222
+ export function followed(owner, field, value) {
223
+ return {
224
+ ...field,
225
+ subscribe: async (_, args, context, info) => {
226
+ const { channel } = liveOf(context);
227
+ const compute = async (caller) => value(args, { ...context, caller }, info);
228
+ return run(async () => liveStream(channel, async (send) => channel.viewer.value(owner, compute, send), follower(info, false, false), closedEvent));
229
+ },
230
+ resolve: rootOf,
231
+ };
232
+ }
233
+ /** The live models' subscriptions, by root name. */
234
+ export function subscriptionFields(registry, live) {
235
+ const fields = {};
236
+ for (const entry of registry.entries.values()) {
237
+ if (entry.visible && live(entry.model.name)) {
238
+ Object.assign(fields, liveFields(registry, entry));
239
+ }
240
+ }
241
+ return fields;
242
+ }