@eleven-am/golem-client 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.
@@ -0,0 +1,471 @@
1
+ import { Kind, OperationTypeNode, parse, print, visit, } from 'graphql';
2
+ import { GolemDocumentError, GolemMultipleOperationsError, GolemNotLiveError } from './errors.js';
3
+ const VERSION_FIELD = '_version';
4
+ const TYPENAME_FIELD = '__typename';
5
+ export function assertLive(analysis) {
6
+ const first = analysis.notLive[0];
7
+ if (first !== undefined) {
8
+ throw first;
9
+ }
10
+ }
11
+ export function streamRequest(stream, variables = {}) {
12
+ const picked = {};
13
+ for (const name of stream.variableNames) {
14
+ if (name in variables) {
15
+ picked[name] = variables[name];
16
+ }
17
+ }
18
+ return { query: stream.query, variables: picked };
19
+ }
20
+ const nameNode = (value) => ({ kind: Kind.NAME, value });
21
+ const bareField = (name) => ({ kind: Kind.FIELD, name: nameNode(name) });
22
+ const responseKey = (field) => (field.alias ?? field.name).value;
23
+ const childPath = (path, field) => (path === '' ? responseKey(field) : `${path}.${responseKey(field)}`);
24
+ const objectValue = (fields) => ({
25
+ kind: Kind.OBJECT,
26
+ fields: Object.entries(fields).map(([name, value]) => ({ kind: Kind.OBJECT_FIELD, name: nameNode(name), value })),
27
+ });
28
+ function conjunction(left, right) {
29
+ if (left === null) {
30
+ return right;
31
+ }
32
+ if (right === null) {
33
+ return left;
34
+ }
35
+ return objectValue({ AND: { kind: Kind.LIST, values: [left, right] } });
36
+ }
37
+ function argument(field, name) {
38
+ return field.arguments?.find((candidate) => candidate.name.value === name);
39
+ }
40
+ function filterScope(value) {
41
+ if (value === undefined || value.kind === Kind.NULL) {
42
+ return null;
43
+ }
44
+ if (value.kind === Kind.OBJECT) {
45
+ return value.fields.length === 0 ? null : value;
46
+ }
47
+ return value.kind === Kind.VARIABLE ? value : null;
48
+ }
49
+ function uniqueScope(model, value) {
50
+ if (value === undefined || value.kind !== Kind.OBJECT || value.fields.length === 0) {
51
+ return null;
52
+ }
53
+ const first = value.fields[0];
54
+ if (first === undefined || value.fields.length !== 1 || first.name.value !== model.cursorSelector) {
55
+ return null;
56
+ }
57
+ const key = first.value;
58
+ if (model.primaryKey.length === 1) {
59
+ const field = model.primaryKey[0];
60
+ if (field === undefined || key.kind === Kind.OBJECT) {
61
+ return null;
62
+ }
63
+ return objectValue({ [field]: objectValue({ equals: key }) });
64
+ }
65
+ if (key.kind !== Kind.OBJECT) {
66
+ return null;
67
+ }
68
+ const parts = {};
69
+ for (const part of key.fields) {
70
+ parts[part.name.value] = objectValue({ equals: part.value });
71
+ }
72
+ return objectValue(parts);
73
+ }
74
+ function applicableTo(fragment, models, manifest) {
75
+ const condition = fragment.typeCondition?.name.value;
76
+ if (condition === undefined || manifest.models[condition] === undefined) {
77
+ return models;
78
+ }
79
+ return models.includes(condition) ? [condition] : [];
80
+ }
81
+ function record(sink, error, key) {
82
+ if (sink !== null && !sink.notLive.has(key)) {
83
+ sink.notLive.set(key, error);
84
+ }
85
+ }
86
+ function injections(model) {
87
+ const names = [...model.primaryKey];
88
+ if (model.events !== null) {
89
+ names.push(VERSION_FIELD);
90
+ }
91
+ names.push(TYPENAME_FIELD);
92
+ return names;
93
+ }
94
+ function ensure(selections, names) {
95
+ const result = [...selections];
96
+ for (const name of names) {
97
+ const keyed = result.filter((selection) => selection.kind === Kind.FIELD && responseKey(selection) === name);
98
+ const taken = keyed.find((selection) => selection.name.value !== name);
99
+ if (taken !== undefined) {
100
+ throw new GolemDocumentError(`the response key ${name} is taken by ${taken.name.value}; golem needs ${name} unaliased on this selection`);
101
+ }
102
+ if (!keyed.some((selection) => (selection.directives ?? []).length === 0)) {
103
+ result.push(bareField(name));
104
+ }
105
+ }
106
+ return result;
107
+ }
108
+ function visitSet(set, models, scope, path, state, walk) {
109
+ const out = [];
110
+ for (const selection of set.selections) {
111
+ if (selection.kind === Kind.FIELD) {
112
+ out.push(visitField(selection, models, scope, path, state, walk));
113
+ }
114
+ else if (selection.kind === Kind.INLINE_FRAGMENT) {
115
+ const narrowed = applicableTo(selection, models, walk.manifest);
116
+ let inner = visitSet(selection.selectionSet, narrowed, scope, path, state, walk);
117
+ const only = narrowed[0];
118
+ if (models.length > 1 && narrowed.length === 1 && only !== undefined) {
119
+ const model = walk.manifest.models[only];
120
+ if (model !== undefined) {
121
+ inner = ensure(inner, injections(model).filter((name) => name !== TYPENAME_FIELD));
122
+ }
123
+ }
124
+ out.push({ ...selection, selectionSet: { ...selection.selectionSet, selections: inner } });
125
+ }
126
+ else {
127
+ const definition = walk.fragments.get(selection.name.value);
128
+ if (definition === undefined) {
129
+ throw new GolemDocumentError(`fragment ${selection.name.value} is not defined in the document`);
130
+ }
131
+ if (walk.sink !== null) {
132
+ visitSet(definition.selectionSet, applicableTo(definition, models, walk.manifest), scope, path, state, walk);
133
+ }
134
+ out.push(selection);
135
+ }
136
+ }
137
+ return out;
138
+ }
139
+ function visitField(field, models, scope, path, state, walk) {
140
+ const name = field.name.value;
141
+ if (name.startsWith('__')) {
142
+ return field;
143
+ }
144
+ if (models.length === 0) {
145
+ return field;
146
+ }
147
+ const only = models[0];
148
+ if (models.length > 1 || only === undefined) {
149
+ throw new GolemDocumentError(`field ${name} is selected on a result that holds ${models.join(' or ')}; select it under a type condition`);
150
+ }
151
+ const model = walk.manifest.models[only];
152
+ const fields = state.fields.get(only);
153
+ if (model === undefined || fields === undefined) {
154
+ throw new GolemDocumentError(`the manifest has no model ${only}`);
155
+ }
156
+ const relation = model.relations[name];
157
+ if (relation !== undefined) {
158
+ state.nested = true;
159
+ return visitRelation(field, only, relation, scope, path, walk);
160
+ }
161
+ const computed = model.computed[name];
162
+ if (computed !== undefined && computed.length > 0) {
163
+ if (field.selectionSet === undefined) {
164
+ return field;
165
+ }
166
+ state.nested = true;
167
+ return { ...field, selectionSet: rows(field.selectionSet, computed, null, false, childPath(path, field), walk) };
168
+ }
169
+ fields.set(responseKey(field), field);
170
+ return field;
171
+ }
172
+ function visitRelation(field, owner, relation, scope, path, walk) {
173
+ const target = walk.manifest.models[relation.target];
174
+ if (target === undefined) {
175
+ throw new GolemDocumentError(`the manifest has no model ${relation.target}`);
176
+ }
177
+ if (field.selectionSet === undefined) {
178
+ return field;
179
+ }
180
+ const subject = `${owner}.${field.name.value}`;
181
+ if (relation.inverse === null) {
182
+ record(walk.sink, new GolemNotLiveError('hidden-inverse', subject, `${subject} is not live: the inverse relation of ${relation.target} is hidden, so rows of ${relation.target} cannot be tied to ${owner}`), `hidden-inverse|${subject}`);
183
+ }
184
+ const inverse = relation.inverse === null ? undefined : target.relations[relation.inverse];
185
+ let inverseFilter = null;
186
+ if (relation.inverse !== null && inverse !== undefined && scope !== null) {
187
+ inverseFilter = objectValue({ [relation.inverse]: objectValue({ [inverse.list ? 'some' : 'is']: scope }) });
188
+ }
189
+ const childScope = conjunction(filterScope(argument(field, 'where')?.value), inverseFilter);
190
+ return { ...field, selectionSet: rows(field.selectionSet, [relation.target], childScope, false, childPath(path, field), walk) };
191
+ }
192
+ function rows(set, models, scope, plain, path, walk) {
193
+ const order = walk.sink === null ? 0 : walk.sink.sequence++;
194
+ const state = { fields: new Map(models.map((model) => [model, new Map()])), nested: false };
195
+ let selections = visitSet(set, models, scope, path, state, walk);
196
+ const single = models.length === 1 ? walk.manifest.models[models[0] ?? ''] : undefined;
197
+ if (single !== undefined) {
198
+ selections = ensure(selections, injections(single));
199
+ }
200
+ else {
201
+ selections = ensure(selections, [TYPENAME_FIELD]);
202
+ }
203
+ register(models, scope, plain && !state.nested, order, path, state, walk);
204
+ return { ...set, selections };
205
+ }
206
+ function disagrees(draft, fields) {
207
+ for (const [responseName, field] of fields) {
208
+ const known = draft.fields.get(responseName);
209
+ if (known !== undefined && print(known) !== print(field)) {
210
+ return true;
211
+ }
212
+ }
213
+ return false;
214
+ }
215
+ function register(models, scope, patchable, order, path, state, walk) {
216
+ const sink = walk.sink;
217
+ if (sink === null) {
218
+ return;
219
+ }
220
+ for (const name of models) {
221
+ const model = walk.manifest.models[name];
222
+ const fields = state.fields.get(name);
223
+ if (model === undefined || fields === undefined) {
224
+ throw new GolemDocumentError(`the manifest has no model ${name}`);
225
+ }
226
+ if (model.events === null) {
227
+ record(sink, new GolemNotLiveError('model-without-events', name, `${name} is not live: the model has no events; declare Subscriptions on ${name} or use a static query`), `model-without-events|${name}`);
228
+ continue;
229
+ }
230
+ const base = `${model.events}|${scope === null ? '' : print(scope)}`;
231
+ let key = base;
232
+ let draft = sink.streams.get(key);
233
+ for (let copy = 1; draft !== undefined && disagrees(draft, fields); copy++) {
234
+ key = `${base}#${copy}`;
235
+ draft = sink.streams.get(key);
236
+ }
237
+ if (draft === undefined) {
238
+ draft = { key, model: name, eventsRoot: model.events, patchable, scope, order, fields: new Map(), paths: new Set() };
239
+ sink.streams.set(key, draft);
240
+ }
241
+ else {
242
+ draft.patchable &&= patchable;
243
+ }
244
+ draft.paths.add(path);
245
+ for (const [responseName, field] of fields) {
246
+ draft.fields.set(responseName, field);
247
+ }
248
+ }
249
+ }
250
+ function collectVariables(node) {
251
+ const names = new Set();
252
+ visit(node, {
253
+ Variable(variable) {
254
+ names.add(variable.name.value);
255
+ },
256
+ });
257
+ return names;
258
+ }
259
+ function finish(draft, manifest, operation) {
260
+ const model = manifest.models[draft.model];
261
+ if (model === undefined) {
262
+ throw new GolemDocumentError(`the manifest has no model ${draft.model}`);
263
+ }
264
+ const entity = ensure([...draft.fields.values()], injections(model));
265
+ const entitySet = { kind: Kind.SELECTION_SET, selections: entity };
266
+ const identity = model.primaryKey.length > 1
267
+ ? { ...bareField('id'), selectionSet: { kind: Kind.SELECTION_SET, selections: model.primaryKey.map(bareField) } }
268
+ : bareField('id');
269
+ const eventFields = [...['eventID', 'version', 'type'].map(bareField), identity];
270
+ eventFields.push({ ...bareField('entity'), selectionSet: entitySet });
271
+ const root = {
272
+ ...bareField(draft.eventsRoot),
273
+ arguments: draft.scope === null
274
+ ? []
275
+ : [{ kind: Kind.ARGUMENT, name: nameNode('where'), value: draft.scope }],
276
+ selectionSet: { kind: Kind.SELECTION_SET, selections: eventFields },
277
+ };
278
+ const body = { kind: Kind.SELECTION_SET, selections: [root] };
279
+ const used = collectVariables(body);
280
+ const definitions = (operation.variableDefinitions ?? []).filter((definition) => used.has(definition.variable.name.value));
281
+ const document = {
282
+ kind: Kind.DOCUMENT,
283
+ definitions: [
284
+ {
285
+ kind: Kind.OPERATION_DEFINITION,
286
+ operation: 'subscription',
287
+ variableDefinitions: definitions,
288
+ selectionSet: body,
289
+ },
290
+ ],
291
+ };
292
+ return {
293
+ key: draft.key,
294
+ model: draft.model,
295
+ eventsRoot: draft.eventsRoot,
296
+ patchable: draft.patchable,
297
+ scope: draft.scope === null ? 'policy-only' : 'exact',
298
+ paths: [...draft.paths].sort(),
299
+ query: print(document),
300
+ variableNames: [...used].sort(),
301
+ };
302
+ }
303
+ function visitRootSet(set, walk) {
304
+ const out = [];
305
+ for (const selection of set.selections) {
306
+ if (selection.kind === Kind.FIELD) {
307
+ out.push(visitRoot(selection, walk));
308
+ }
309
+ else if (selection.kind === Kind.INLINE_FRAGMENT) {
310
+ out.push({ ...selection, selectionSet: { ...selection.selectionSet, selections: visitRootSet(selection.selectionSet, walk) } });
311
+ }
312
+ else {
313
+ const definition = walk.fragments.get(selection.name.value);
314
+ if (definition === undefined) {
315
+ throw new GolemDocumentError(`fragment ${selection.name.value} is not defined in the document`);
316
+ }
317
+ if (walk.sink !== null) {
318
+ visitRootSet(definition.selectionSet, walk);
319
+ }
320
+ out.push(selection);
321
+ }
322
+ }
323
+ return out;
324
+ }
325
+ function visitRoot(field, walk) {
326
+ const name = field.name.value;
327
+ if (name.startsWith('__')) {
328
+ return field;
329
+ }
330
+ const root = walk.manifest.roots[name];
331
+ if (root === undefined) {
332
+ throw new GolemDocumentError(`${name} is not a root in the manifest`);
333
+ }
334
+ if (root.kind === 'generated') {
335
+ const modelName = root.models[0];
336
+ const model = modelName === undefined ? undefined : walk.manifest.models[modelName];
337
+ if (modelName === undefined || model === undefined) {
338
+ throw new GolemDocumentError(`the manifest root ${name} names no model`);
339
+ }
340
+ if (root.shape !== 'rows' && root.shape !== 'row' && root.shape !== 'deletedRow') {
341
+ record(walk.sink, new GolemNotLiveError('non-row-root', name, `${name} is not live: its result shape is ${root.shape}, not model rows`), `non-row-root|${name}`);
342
+ return field;
343
+ }
344
+ if (field.selectionSet === undefined) {
345
+ return field;
346
+ }
347
+ const where = argument(field, 'where')?.value;
348
+ const scope = root.shape === 'rows' ? filterScope(where) : uniqueScope(model, where);
349
+ markNullable(field, root, walk);
350
+ const plain = (field.arguments ?? []).every((candidate) => candidate.name.value === 'where');
351
+ return { ...field, selectionSet: rows(field.selectionSet, [modelName], scope, plain, responseKey(field), walk) };
352
+ }
353
+ if (root.models.length === 0 || field.selectionSet === undefined) {
354
+ record(walk.sink, new GolemNotLiveError('opaque-root-without-models', name, `${name} is not live: the custom root's result holds no model rows`), `opaque-root-without-models|${name}`);
355
+ return field;
356
+ }
357
+ markNullable(field, root, walk);
358
+ return { ...field, selectionSet: rows(field.selectionSet, root.models, null, false, responseKey(field), walk) };
359
+ }
360
+ function markNullable(field, root, walk) {
361
+ if ((root.shape === 'row' || root.shape === 'deletedRow') && root.nullable && walk.sink !== null) {
362
+ walk.sink.nullable.add(responseKey(field));
363
+ }
364
+ }
365
+ function rewriteFragment(definition, walk) {
366
+ const condition = definition.typeCondition.name.value;
367
+ if (condition === 'Query') {
368
+ return { ...definition, selectionSet: { ...definition.selectionSet, selections: visitRootSet(definition.selectionSet, walk) } };
369
+ }
370
+ if (walk.manifest.models[condition] === undefined) {
371
+ return definition;
372
+ }
373
+ return { ...definition, selectionSet: rows(definition.selectionSet, [condition], null, false, '', walk) };
374
+ }
375
+ export function operationOf(document, kind) {
376
+ const operations = document.definitions.filter((definition) => definition.kind === Kind.OPERATION_DEFINITION);
377
+ if (operations.length > 1) {
378
+ throw new GolemMultipleOperationsError(operations.length);
379
+ }
380
+ const operation = operations[0];
381
+ if (operation === undefined) {
382
+ throw new GolemDocumentError('the document holds no operation');
383
+ }
384
+ if (operation.operation !== kind) {
385
+ throw new GolemDocumentError(`only ${kind} operations are analysed here; got ${operation.operation}`);
386
+ }
387
+ return operation;
388
+ }
389
+ function fragmentsOf(document) {
390
+ const fragments = new Map();
391
+ for (const definition of document.definitions) {
392
+ if (definition.kind === Kind.FRAGMENT_DEFINITION) {
393
+ fragments.set(definition.name.value, definition);
394
+ }
395
+ }
396
+ return fragments;
397
+ }
398
+ function analyse(document, manifest) {
399
+ const operation = operationOf(document, OperationTypeNode.QUERY);
400
+ const fragments = fragmentsOf(document);
401
+ const sink = { streams: new Map(), notLive: new Map(), nullable: new Set(), sequence: 0 };
402
+ const live = { manifest, fragments, sink };
403
+ const silent = { manifest, fragments, sink: null };
404
+ const rewrittenOperation = {
405
+ ...operation,
406
+ selectionSet: { ...operation.selectionSet, selections: visitRootSet(operation.selectionSet, live) },
407
+ };
408
+ const definitions = document.definitions.map((definition) => {
409
+ if (definition === operation) {
410
+ return rewrittenOperation;
411
+ }
412
+ if (definition.kind === Kind.FRAGMENT_DEFINITION) {
413
+ return rewriteFragment(definition, silent);
414
+ }
415
+ return definition;
416
+ });
417
+ const rewritten = { ...document, definitions };
418
+ const notLive = [...sink.notLive.values()];
419
+ return {
420
+ document: rewritten,
421
+ query: print(rewritten),
422
+ nullableRoots: sink.nullable,
423
+ streams: notLive.length > 0 ? [] : [...sink.streams.values()].sort((left, right) => left.order - right.order).map((draft) => finish(draft, manifest, operation)),
424
+ notLive,
425
+ live: notLive.length === 0,
426
+ };
427
+ }
428
+ export class DocumentAnalyzer {
429
+ manifest;
430
+ byText = new Map();
431
+ byNode = new WeakMap();
432
+ constructor(manifest) {
433
+ this.manifest = manifest;
434
+ }
435
+ analyze(document) {
436
+ if (typeof document === 'string') {
437
+ let analysis = this.byText.get(document);
438
+ if (analysis === undefined) {
439
+ analysis = analyse(parse(document), this.manifest);
440
+ this.byText.set(document, analysis);
441
+ }
442
+ return analysis;
443
+ }
444
+ let analysis = this.byNode.get(document);
445
+ if (analysis === undefined) {
446
+ analysis = analyse(document, this.manifest);
447
+ this.byNode.set(document, analysis);
448
+ }
449
+ return analysis;
450
+ }
451
+ }
452
+ export function analyzeMutation(document, manifest) {
453
+ const parsed = typeof document === 'string' ? parse(document) : document;
454
+ const operation = operationOf(parsed, OperationTypeNode.MUTATION);
455
+ const roots = [];
456
+ for (const selection of operation.selectionSet.selections) {
457
+ if (selection.kind !== Kind.FIELD) {
458
+ throw new GolemDocumentError('a mutation document selects its roots directly; fragments at the mutation root are not supported');
459
+ }
460
+ const name = selection.name.value;
461
+ if (name.startsWith('__')) {
462
+ continue;
463
+ }
464
+ const described = manifest.roots[name];
465
+ if (described === undefined) {
466
+ throw new GolemDocumentError(`${name} is not a root in the manifest`);
467
+ }
468
+ roots.push({ name, models: described.models });
469
+ }
470
+ return { query: print(parsed), roots };
471
+ }
@@ -0,0 +1,49 @@
1
+ import type { DocumentNode } from 'graphql';
2
+ import type { QueryClient, QueryFunctionContext } from '@tanstack/query-core';
3
+ import { DocumentAnalyzer } from './analysis.js';
4
+ import { LiveSync } from './live.js';
5
+ import type { ClientManifest } from './manifest.js';
6
+ import { Mutations } from './mutations.js';
7
+ import type { CredentialsGetter, SocketFactory } from './session.js';
8
+ import { Transport } from './transport.js';
9
+ export interface GolemClientOptions {
10
+ readonly queryClient: QueryClient;
11
+ readonly url: string;
12
+ readonly manifest: ClientManifest;
13
+ readonly credentials?: CredentialsGetter;
14
+ readonly createSocket?: SocketFactory;
15
+ readonly fetch?: typeof globalThis.fetch;
16
+ }
17
+ export declare class GolemClient {
18
+ readonly queryClient: QueryClient;
19
+ readonly transport: Transport;
20
+ readonly live: LiveSync;
21
+ readonly mutations: Mutations;
22
+ readonly analyzer: DocumentAnalyzer;
23
+ private holds;
24
+ private startedByHolds;
25
+ constructor(options: GolemClientOptions);
26
+ queryOptions(document: DocumentNode | string, variables?: Readonly<Record<string, unknown>>): {
27
+ queryKey: readonly ["golem", string, Readonly<Record<string, unknown>>];
28
+ queryFn: (context: QueryFunctionContext) => Promise<unknown>;
29
+ meta: {
30
+ golem: import("./live.js").GolemQueryMeta;
31
+ };
32
+ };
33
+ infiniteQueryOptions(document: DocumentNode | string, variables?: Readonly<Record<string, unknown>>): {
34
+ queryKey: readonly ["golem", string, Readonly<Record<string, unknown>>, "infinite"];
35
+ queryFn: (context: QueryFunctionContext<readonly unknown[], import("./pagination.js").Cursor | null>) => Promise<unknown>;
36
+ initialPageParam: Readonly<Record<string, unknown>> | null;
37
+ getNextPageParam: (lastPage: unknown) => Readonly<Record<string, unknown>> | undefined;
38
+ meta: {
39
+ golem: import("./live.js").GolemQueryMeta;
40
+ };
41
+ };
42
+ staticQueryOptions(document: DocumentNode | string, variables?: Readonly<Record<string, unknown>>): {
43
+ queryKey: readonly ["golem", "static", string, Readonly<Record<string, unknown>>];
44
+ queryFn: (context: Pick<QueryFunctionContext, "signal">) => Promise<unknown>;
45
+ };
46
+ mutate<T = unknown>(document: DocumentNode | string, variables?: Readonly<Record<string, unknown>>, signal?: AbortSignal): Promise<T>;
47
+ attach(): () => void;
48
+ dispose(): void;
49
+ }
package/dist/client.js ADDED
@@ -0,0 +1,77 @@
1
+ import { DocumentAnalyzer } from './analysis.js';
2
+ import { GolemConfigurationError } from './errors.js';
3
+ import { LiveSync } from './live.js';
4
+ import { Mutations } from './mutations.js';
5
+ import { Transport } from './transport.js';
6
+ const claimed = new WeakMap();
7
+ export class GolemClient {
8
+ queryClient;
9
+ transport;
10
+ live;
11
+ mutations;
12
+ analyzer;
13
+ holds = 0;
14
+ startedByHolds = false;
15
+ constructor(options) {
16
+ if (options.queryClient === undefined || options.queryClient === null) {
17
+ throw new GolemConfigurationError('GolemClient needs the application QueryClient: pass queryClient');
18
+ }
19
+ if (claimed.has(options.queryClient)) {
20
+ throw new GolemConfigurationError('this QueryClient already belongs to a GolemClient; one QueryClient serves one GolemClient');
21
+ }
22
+ this.queryClient = options.queryClient;
23
+ this.transport = new Transport({
24
+ url: options.url,
25
+ manifest: options.manifest,
26
+ credentials: options.credentials,
27
+ createSocket: options.createSocket,
28
+ fetch: options.fetch,
29
+ });
30
+ this.analyzer = new DocumentAnalyzer(options.manifest);
31
+ this.live = new LiveSync({ queryClient: options.queryClient, transport: this.transport, manifest: options.manifest });
32
+ this.mutations = new Mutations({ queryClient: options.queryClient, transport: this.transport, manifest: options.manifest });
33
+ claimed.set(options.queryClient, this);
34
+ }
35
+ queryOptions(document, variables = {}) {
36
+ return this.live.options(this.analyzer.analyze(document), variables);
37
+ }
38
+ infiniteQueryOptions(document, variables = {}) {
39
+ return this.live.infiniteOptions(this.analyzer.analyze(document), variables);
40
+ }
41
+ staticQueryOptions(document, variables = {}) {
42
+ const analysis = this.analyzer.analyze(document);
43
+ return {
44
+ queryKey: ['golem', 'static', analysis.query, variables],
45
+ queryFn: (context) => this.transport.execute({ query: analysis.query, variables, signal: context.signal }),
46
+ };
47
+ }
48
+ mutate(document, variables, signal) {
49
+ return this.mutations.execute(document, variables, signal);
50
+ }
51
+ attach() {
52
+ this.holds++;
53
+ if (this.holds === 1 && !this.live.started) {
54
+ this.live.start();
55
+ this.startedByHolds = true;
56
+ }
57
+ let released = false;
58
+ return () => {
59
+ if (released) {
60
+ return;
61
+ }
62
+ released = true;
63
+ this.holds--;
64
+ if (this.holds === 0 && this.startedByHolds) {
65
+ this.startedByHolds = false;
66
+ this.live.stop();
67
+ }
68
+ };
69
+ }
70
+ dispose() {
71
+ this.live.stop();
72
+ this.transport.dispose();
73
+ if (claimed.get(this.queryClient) === this) {
74
+ claimed.delete(this.queryClient);
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,14 @@
1
+ export declare const COALESCE_WINDOW_MS = 50;
2
+ export declare const COALESCE_MAX_WAIT_MS = 500;
3
+ export declare class Coalescer {
4
+ private readonly fire;
5
+ private readonly window;
6
+ private readonly maxWait;
7
+ private timer;
8
+ private firstRequestAt;
9
+ constructor(fire: () => void, window?: number, maxWait?: number);
10
+ get pending(): boolean;
11
+ request(): void;
12
+ flush(): void;
13
+ cancel(): void;
14
+ }
@@ -0,0 +1,37 @@
1
+ export const COALESCE_WINDOW_MS = 50;
2
+ export const COALESCE_MAX_WAIT_MS = 500;
3
+ export class Coalescer {
4
+ fire;
5
+ window;
6
+ maxWait;
7
+ timer;
8
+ firstRequestAt;
9
+ constructor(fire, window = COALESCE_WINDOW_MS, maxWait = COALESCE_MAX_WAIT_MS) {
10
+ this.fire = fire;
11
+ this.window = window;
12
+ this.maxWait = maxWait;
13
+ }
14
+ get pending() {
15
+ return this.timer !== undefined;
16
+ }
17
+ request() {
18
+ const now = Date.now();
19
+ this.firstRequestAt ??= now;
20
+ if (this.timer !== undefined) {
21
+ clearTimeout(this.timer);
22
+ }
23
+ const remaining = this.firstRequestAt + this.maxWait - now;
24
+ this.timer = setTimeout(() => this.flush(), Math.max(0, Math.min(this.window, remaining)));
25
+ }
26
+ flush() {
27
+ this.cancel();
28
+ this.fire();
29
+ }
30
+ cancel() {
31
+ if (this.timer !== undefined) {
32
+ clearTimeout(this.timer);
33
+ }
34
+ this.timer = undefined;
35
+ this.firstRequestAt = undefined;
36
+ }
37
+ }