pulse-js-framework 1.10.0 → 1.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/compiler/parser/_extract.js +393 -0
  2. package/compiler/parser/blocks.js +361 -0
  3. package/compiler/parser/core.js +306 -0
  4. package/compiler/parser/expressions.js +386 -0
  5. package/compiler/parser/imports.js +108 -0
  6. package/compiler/parser/index.js +47 -0
  7. package/compiler/parser/state.js +155 -0
  8. package/compiler/parser/style.js +445 -0
  9. package/compiler/parser/view.js +632 -0
  10. package/compiler/parser.js +15 -2372
  11. package/compiler/parser.js.original +2376 -0
  12. package/package.json +2 -1
  13. package/runtime/a11y/announcements.js +213 -0
  14. package/runtime/a11y/contrast.js +125 -0
  15. package/runtime/a11y/focus.js +412 -0
  16. package/runtime/a11y/index.js +35 -0
  17. package/runtime/a11y/preferences.js +121 -0
  18. package/runtime/a11y/utils.js +164 -0
  19. package/runtime/a11y/validation.js +258 -0
  20. package/runtime/a11y/widgets.js +545 -0
  21. package/runtime/a11y.js +15 -1840
  22. package/runtime/a11y.js.original +1844 -0
  23. package/runtime/graphql/cache.js +69 -0
  24. package/runtime/graphql/client.js +563 -0
  25. package/runtime/graphql/hooks.js +492 -0
  26. package/runtime/graphql/index.js +62 -0
  27. package/runtime/graphql/subscriptions.js +241 -0
  28. package/runtime/graphql.js +12 -1322
  29. package/runtime/graphql.js.original +1326 -0
  30. package/runtime/router/core.js +956 -0
  31. package/runtime/router/guards.js +90 -0
  32. package/runtime/router/history.js +204 -0
  33. package/runtime/router/index.js +36 -0
  34. package/runtime/router/lazy.js +180 -0
  35. package/runtime/router/utils.js +226 -0
  36. package/runtime/router.js +12 -1600
  37. package/runtime/router.js.original +1605 -0
@@ -1,1326 +1,16 @@
1
1
  /**
2
- * Pulse GraphQL Client - Zero-dependency GraphQL client for Pulse Framework
2
+ * Pulse GraphQL - Backward Compatibility Export
3
+ *
4
+ * This file maintains backward compatibility by re-exporting from graphql/
5
+ * The actual implementation has been split into focused sub-modules:
6
+ * - graphql/client.js - Core client and error handling
7
+ * - graphql/cache.js - Query caching utilities
8
+ * - graphql/subscriptions.js - WebSocket subscriptions
9
+ * - graphql/hooks.js - useQuery, useMutation, useSubscription
10
+ *
11
+ * @deprecated Import from 'pulse-js-framework/runtime/graphql/index.js' instead
3
12
  * @module pulse-js-framework/runtime/graphql
4
13
  */
5
14
 
6
- import { pulse, computed, batch, effect, onCleanup } from './pulse.js';
7
- import { createHttp, HttpError } from './http.js';
8
- import { createWebSocket, WebSocketError } from './websocket.js';
9
- import { createVersionedAsync } from './async.js';
10
- import { ClientError } from './errors.js';
11
- import { LRUCache } from './lru-cache.js';
12
- import { InterceptorManager } from './interceptor-manager.js';
13
- import { onWindowFocus, onWindowOnline } from './utils.js';
14
-
15
- // ============================================================================
16
- // Constants
17
- // ============================================================================
18
-
19
- /**
20
- * graphql-ws protocol message types
21
- * @see https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md
22
- */
23
- const MessageType = {
24
- // Client -> Server
25
- ConnectionInit: 'connection_init',
26
- Subscribe: 'subscribe',
27
- Complete: 'complete',
28
- Ping: 'ping',
29
- Pong: 'pong',
30
-
31
- // Server -> Client
32
- ConnectionAck: 'connection_ack',
33
- Next: 'next',
34
- Error: 'error'
35
- // Complete is bidirectional
36
- };
37
-
38
- // ============================================================================
39
- // GraphQL Error Class
40
- // ============================================================================
41
-
42
- /**
43
- * Error class for GraphQL operations.
44
- * Extends ClientError for consistent error handling patterns.
45
- */
46
- export class GraphQLError extends ClientError {
47
- static suggestions = {
48
- GRAPHQL_ERROR: 'Check the GraphQL errors array for specific field errors.',
49
- NETWORK_ERROR: 'Verify network connectivity and GraphQL endpoint URL.',
50
- PARSE_ERROR: 'The response was not valid GraphQL JSON. Check server configuration.',
51
- TIMEOUT: 'Request timed out. Consider increasing timeout or optimizing query.',
52
- AUTHENTICATION_ERROR: 'Authentication required. Check your credentials or token.',
53
- AUTHORIZATION_ERROR: 'Insufficient permissions for this operation.',
54
- VALIDATION_ERROR: 'Invalid input provided. Check variables match schema types.',
55
- SUBSCRIPTION_ERROR: 'WebSocket subscription failed. Check connection status.'
56
- };
57
-
58
- static errorName = 'GraphQLError';
59
- static defaultCode = 'GRAPHQL_ERROR';
60
- static markerProperty = 'isGraphQLError';
61
-
62
- /**
63
- * @param {string} message - Error message
64
- * @param {Object} [options={}] - Error options
65
- * @param {string} [options.code] - Error code
66
- * @param {Array} [options.errors] - GraphQL errors array from response
67
- * @param {*} [options.data] - Partial data from response
68
- * @param {Object} [options.extensions] - GraphQL extensions
69
- * @param {Object} [options.response] - Full HTTP response
70
- * @param {Object} [options.request] - Request configuration
71
- */
72
- constructor(message, options = {}) {
73
- super(message, options);
74
- this.errors = options.errors || [];
75
- this.data = options.data ?? null;
76
- this.extensions = options.extensions || {};
77
- this.response = options.response || null;
78
- this.request = options.request || null;
79
- }
80
-
81
- /**
82
- * Check if error is a GraphQLError
83
- * @param {*} error - Error to check
84
- * @returns {boolean}
85
- */
86
- static isGraphQLError(error) {
87
- return error?.isGraphQLError === true || error instanceof GraphQLError;
88
- }
89
-
90
- /**
91
- * Check if response has partial data along with errors
92
- * @returns {boolean}
93
- */
94
- hasPartialData() {
95
- return this.data !== null && this.data !== undefined;
96
- }
97
-
98
- /**
99
- * Check if this is an authentication error
100
- * @returns {boolean}
101
- */
102
- isAuthenticationError() {
103
- return this.code === 'AUTHENTICATION_ERROR' ||
104
- this.errors.some(e => e.extensions?.code === 'UNAUTHENTICATED');
105
- }
106
-
107
- /**
108
- * Check if this is an authorization error
109
- * @returns {boolean}
110
- */
111
- isAuthorizationError() {
112
- return this.code === 'AUTHORIZATION_ERROR' ||
113
- this.errors.some(e => e.extensions?.code === 'FORBIDDEN');
114
- }
115
-
116
- /**
117
- * Check if this is a validation error
118
- * @returns {boolean}
119
- */
120
- isValidationError() {
121
- return this.code === 'VALIDATION_ERROR' ||
122
- this.errors.some(e => e.extensions?.code === 'BAD_USER_INPUT');
123
- }
124
-
125
- /**
126
- * Get the first error message from GraphQL errors
127
- * @returns {string|null}
128
- */
129
- getFirstError() {
130
- return this.errors[0]?.message || null;
131
- }
132
-
133
- /**
134
- * Get all error messages
135
- * @returns {string[]}
136
- */
137
- getAllErrors() {
138
- return this.errors.map(e => e.message);
139
- }
140
-
141
- /**
142
- * Convert to JSON
143
- * @returns {Object}
144
- */
145
- toJSON() {
146
- return {
147
- name: this.name,
148
- message: this.message,
149
- code: this.code,
150
- errors: this.errors,
151
- data: this.data,
152
- extensions: this.extensions
153
- };
154
- }
155
- }
156
-
157
- // ============================================================================
158
- // Cache Key Utilities
159
- // ============================================================================
160
-
161
- /**
162
- * Extract operation name from GraphQL query string
163
- * @param {string} query - GraphQL query string
164
- * @returns {string|null} Operation name or null
165
- */
166
- function extractOperationName(query) {
167
- const match = query.match(/(?:query|mutation|subscription)\s+(\w+)/);
168
- return match ? match[1] : null;
169
- }
170
-
171
- /**
172
- * Simple hash function for strings
173
- * @param {string} str - String to hash
174
- * @returns {string} Hash string
175
- */
176
- function hashString(str) {
177
- if (!str) return '';
178
- let hash = 0;
179
- for (let i = 0; i < str.length; i++) {
180
- const char = str.charCodeAt(i);
181
- hash = ((hash << 5) - hash) + char;
182
- hash = hash & hash; // Convert to 32bit integer
183
- }
184
- return Math.abs(hash).toString(36);
185
- }
186
-
187
- /**
188
- * Stable JSON stringify with sorted keys
189
- * @param {*} obj - Object to stringify
190
- * @returns {string} Stable JSON string
191
- */
192
- function stableStringify(obj) {
193
- if (obj === null || obj === undefined) return '';
194
- if (typeof obj !== 'object') return JSON.stringify(obj);
195
- if (Array.isArray(obj)) {
196
- return '[' + obj.map(stableStringify).join(',') + ']';
197
- }
198
- const keys = Object.keys(obj).sort();
199
- return '{' + keys.map(k => `"${k}":${stableStringify(obj[k])}`).join(',') + '}';
200
- }
201
-
202
- /**
203
- * Generate a deterministic cache key for a GraphQL operation
204
- * @param {string} query - GraphQL query string
205
- * @param {Object} [variables] - Query variables
206
- * @returns {string} Cache key
207
- */
208
- function generateCacheKey(query, variables) {
209
- const operationName = extractOperationName(query);
210
- const variablesHash = variables ? hashString(stableStringify(variables)) : '';
211
- const queryHash = operationName || hashString(query.replace(/\s+/g, ' ').trim());
212
- return `gql:${queryHash}${variablesHash ? ':' + variablesHash : ''}`;
213
- }
214
-
215
- // ============================================================================
216
- // Subscription Manager
217
- // ============================================================================
218
-
219
- /**
220
- * Manages GraphQL subscriptions over WebSocket
221
- */
222
- class SubscriptionManager {
223
- #ws = null;
224
- #subscriptions = new Map();
225
- #nextId = 1;
226
- #connectionParams;
227
- #connected = pulse(false);
228
- #pending = [];
229
-
230
- /**
231
- * @param {Object} ws - WebSocket instance from createWebSocket
232
- * @param {Object|Function} [connectionParams] - Connection parameters
233
- */
234
- constructor(ws, connectionParams) {
235
- this.#ws = ws;
236
- this.#connectionParams = connectionParams;
237
-
238
- // Handle incoming messages
239
- ws.on('message', (msg) => this.#handleMessage(msg));
240
- ws.on('open', () => this.#handleOpen());
241
- ws.on('close', () => this.#handleClose());
242
- ws.on('error', (err) => this.#handleError(err));
243
- }
244
-
245
- /**
246
- * Get connection state
247
- */
248
- get connected() {
249
- return this.#connected;
250
- }
251
-
252
- /**
253
- * Handle WebSocket open
254
- */
255
- async #handleOpen() {
256
- // Send connection_init
257
- const params = typeof this.#connectionParams === 'function'
258
- ? await this.#connectionParams()
259
- : this.#connectionParams;
260
-
261
- this.#ws.send({
262
- type: MessageType.ConnectionInit,
263
- payload: params || {}
264
- });
265
- }
266
-
267
- /**
268
- * Handle WebSocket close
269
- */
270
- #handleClose() {
271
- this.#connected.set(false);
272
- // Notify all active subscriptions
273
- for (const [id, sub] of this.#subscriptions) {
274
- sub.handlers.onError?.(new GraphQLError('Connection closed', {
275
- code: 'SUBSCRIPTION_ERROR'
276
- }));
277
- }
278
- }
279
-
280
- /**
281
- * Handle WebSocket error
282
- */
283
- #handleError(error) {
284
- for (const [id, sub] of this.#subscriptions) {
285
- sub.handlers.onError?.(new GraphQLError(error.message || 'WebSocket error', {
286
- code: 'SUBSCRIPTION_ERROR'
287
- }));
288
- }
289
- }
290
-
291
- /**
292
- * Handle incoming WebSocket message
293
- * @param {Object} message - Parsed message
294
- */
295
- #handleMessage(message) {
296
- const { id, type, payload } = message;
297
-
298
- switch (type) {
299
- case MessageType.ConnectionAck:
300
- this.#connected.set(true);
301
- // Send any pending subscriptions
302
- for (const pending of this.#pending) {
303
- this.#ws.send(pending);
304
- }
305
- this.#pending = [];
306
- break;
307
-
308
- case MessageType.Next: {
309
- const sub = this.#subscriptions.get(id);
310
- if (sub) {
311
- sub.handlers.onData?.(payload.data);
312
- }
313
- break;
314
- }
315
-
316
- case MessageType.Error: {
317
- const sub = this.#subscriptions.get(id);
318
- if (sub) {
319
- sub.handlers.onError?.(new GraphQLError('Subscription error', {
320
- code: 'SUBSCRIPTION_ERROR',
321
- errors: Array.isArray(payload) ? payload : [payload]
322
- }));
323
- }
324
- break;
325
- }
326
-
327
- case MessageType.Complete: {
328
- const sub = this.#subscriptions.get(id);
329
- if (sub) {
330
- sub.handlers.onComplete?.();
331
- this.#subscriptions.delete(id);
332
- }
333
- break;
334
- }
335
-
336
- case MessageType.Ping:
337
- this.#ws.send({ type: MessageType.Pong });
338
- break;
339
- }
340
- }
341
-
342
- /**
343
- * Subscribe to a GraphQL subscription
344
- * @param {string} query - GraphQL subscription query
345
- * @param {Object} [variables] - Subscription variables
346
- * @param {Object} handlers - Event handlers
347
- * @returns {Function} Unsubscribe function
348
- */
349
- subscribe(query, variables, handlers) {
350
- const id = String(this.#nextId++);
351
-
352
- const message = {
353
- id,
354
- type: MessageType.Subscribe,
355
- payload: {
356
- query,
357
- variables,
358
- operationName: extractOperationName(query)
359
- }
360
- };
361
-
362
- // Store subscription
363
- this.#subscriptions.set(id, {
364
- query,
365
- variables,
366
- handlers
367
- });
368
-
369
- // Send or queue the subscription message
370
- if (this.#connected.get()) {
371
- this.#ws.send(message);
372
- } else {
373
- this.#pending.push(message);
374
- // Ensure WebSocket is connecting
375
- if (this.#ws.state.get() === 'closed') {
376
- this.#ws.connect();
377
- }
378
- }
379
-
380
- // Return unsubscribe function
381
- return () => {
382
- if (this.#subscriptions.has(id)) {
383
- this.#subscriptions.delete(id);
384
- if (this.#connected.get()) {
385
- this.#ws.send({ id, type: MessageType.Complete });
386
- }
387
- }
388
- };
389
- }
390
-
391
- /**
392
- * Get active subscription count
393
- * @returns {number}
394
- */
395
- get activeCount() {
396
- return this.#subscriptions.size;
397
- }
398
-
399
- /**
400
- * Close all subscriptions
401
- */
402
- closeAll() {
403
- for (const id of this.#subscriptions.keys()) {
404
- if (this.#connected.get()) {
405
- this.#ws.send({ id, type: MessageType.Complete });
406
- }
407
- }
408
- this.#subscriptions.clear();
409
- }
410
-
411
- /**
412
- * Dispose the subscription manager
413
- */
414
- dispose() {
415
- this.closeAll();
416
- this.#ws.dispose();
417
- }
418
- }
419
-
420
- // ============================================================================
421
- // GraphQL Client
422
- // ============================================================================
423
-
424
- /**
425
- * GraphQL Client class
426
- */
427
- class GraphQLClient {
428
- #http;
429
- #ws = null;
430
- #subscriptionManager = null;
431
- #options;
432
- #inflightQueries = new Map();
433
- // LRU cache to prevent unbounded memory growth (default 500 entries)
434
- #cache;
435
-
436
- /**
437
- * Request interceptors
438
- */
439
- interceptors = {
440
- request: new InterceptorManager(),
441
- response: new InterceptorManager()
442
- };
443
-
444
- /**
445
- * @param {Object} options - Client options
446
- */
447
- constructor(options = {}) {
448
- if (!options.url) {
449
- throw new GraphQLError('GraphQL client requires a url option', {
450
- code: 'GRAPHQL_ERROR'
451
- });
452
- }
453
-
454
- this.#options = {
455
- url: options.url,
456
- wsUrl: options.wsUrl || this.#deriveWsUrl(options.url),
457
- headers: options.headers || {},
458
- timeout: options.timeout ?? 30000,
459
- credentials: options.credentials || 'same-origin',
460
- retries: options.retries ?? 0,
461
- retryDelay: options.retryDelay ?? 1000,
462
- wsConnectionParams: options.wsConnectionParams,
463
- wsReconnect: options.wsReconnect ?? true,
464
- wsMaxRetries: options.wsMaxRetries ?? 5,
465
- cache: options.cache ?? true,
466
- cacheTime: options.cacheTime ?? 300000,
467
- cacheMaxSize: options.cacheMaxSize ?? 500,
468
- staleTime: options.staleTime ?? 0,
469
- dedupe: options.dedupe ?? true,
470
- throwOnError: options.throwOnError ?? true,
471
- onError: options.onError
472
- };
473
-
474
- // Initialize LRU cache to prevent unbounded memory growth
475
- this.#cache = new LRUCache(this.#options.cacheMaxSize);
476
-
477
- // Create HTTP client for queries/mutations
478
- this.#http = createHttp({
479
- baseURL: '',
480
- timeout: this.#options.timeout,
481
- headers: {
482
- 'Content-Type': 'application/json',
483
- ...this.#options.headers
484
- },
485
- withCredentials: this.#options.credentials === 'include',
486
- retries: this.#options.retries,
487
- retryDelay: this.#options.retryDelay
488
- });
489
- }
490
-
491
- /**
492
- * Derive WebSocket URL from HTTP URL
493
- * @param {string} url - HTTP URL
494
- * @returns {string} WebSocket URL
495
- */
496
- #deriveWsUrl(url) {
497
- try {
498
- const parsed = new URL(url, globalThis.location?.origin || 'http://localhost');
499
- parsed.protocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:';
500
- return parsed.toString();
501
- } catch {
502
- return url.replace(/^http/, 'ws');
503
- }
504
- }
505
-
506
- /**
507
- * Initialize WebSocket for subscriptions (lazy)
508
- */
509
- #initWebSocket() {
510
- if (this.#ws) return;
511
-
512
- this.#ws = createWebSocket(this.#options.wsUrl, {
513
- protocols: ['graphql-transport-ws'],
514
- reconnect: this.#options.wsReconnect,
515
- maxRetries: this.#options.wsMaxRetries,
516
- autoConnect: false,
517
- autoParseJson: true,
518
- queueWhileDisconnected: true
519
- });
520
-
521
- this.#subscriptionManager = new SubscriptionManager(
522
- this.#ws,
523
- this.#options.wsConnectionParams
524
- );
525
- }
526
-
527
- /**
528
- * Execute a GraphQL operation
529
- * @param {string} query - GraphQL query/mutation
530
- * @param {Object} [variables] - Variables
531
- * @param {Object} [options] - Request options
532
- * @returns {Promise<Object>} Response data
533
- */
534
- async #execute(query, variables, options = {}) {
535
- const operationName = extractOperationName(query);
536
-
537
- let config = {
538
- query,
539
- variables,
540
- operationName,
541
- ...options
542
- };
543
-
544
- // Run request interceptors
545
- for (const interceptor of this.interceptors.request) {
546
- if (interceptor.fulfilled) {
547
- try {
548
- config = await interceptor.fulfilled(config);
549
- } catch (err) {
550
- if (interceptor.rejected) {
551
- config = await interceptor.rejected(err);
552
- } else {
553
- throw err;
554
- }
555
- }
556
- }
557
- }
558
-
559
- try {
560
- const response = await this.#http.post(this.#options.url, {
561
- query: config.query,
562
- variables: config.variables,
563
- operationName: config.operationName
564
- });
565
-
566
- let result = response.data;
567
-
568
- // Run response interceptors
569
- for (const interceptor of this.interceptors.response) {
570
- if (interceptor.fulfilled) {
571
- try {
572
- result = await interceptor.fulfilled(result);
573
- } catch (err) {
574
- if (interceptor.rejected) {
575
- result = await interceptor.rejected(err);
576
- } else {
577
- throw err;
578
- }
579
- }
580
- }
581
- }
582
-
583
- return this.#processResponse(result, config);
584
- } catch (error) {
585
- // Convert HTTP errors to GraphQL errors
586
- if (HttpError.isHttpError(error)) {
587
- const graphqlError = new GraphQLError(error.message, {
588
- code: error.isTimeout() ? 'TIMEOUT' : error.isNetworkError() ? 'NETWORK_ERROR' : 'GRAPHQL_ERROR',
589
- request: config
590
- });
591
- this.#options.onError?.(graphqlError);
592
- throw graphqlError;
593
- }
594
- throw error;
595
- }
596
- }
597
-
598
- /**
599
- * Process GraphQL response
600
- * @param {Object} response - GraphQL response
601
- * @param {Object} config - Request config
602
- * @returns {*} Response data
603
- */
604
- #processResponse(response, config) {
605
- const { data, errors, extensions } = response;
606
-
607
- // No errors - return data
608
- if (!errors || errors.length === 0) {
609
- return data;
610
- }
611
-
612
- // Has errors
613
- const error = new GraphQLError(errors[0].message, {
614
- code: this.#mapErrorCode(errors[0]),
615
- errors,
616
- data,
617
- extensions,
618
- request: config
619
- });
620
-
621
- this.#options.onError?.(error);
622
-
623
- if (this.#options.throwOnError) {
624
- throw error;
625
- }
626
-
627
- // Return data even with errors if throwOnError is false
628
- return data;
629
- }
630
-
631
- /**
632
- * Map GraphQL error to error code
633
- * @param {Object} error - GraphQL error
634
- * @returns {string} Error code
635
- */
636
- #mapErrorCode(error) {
637
- const code = error.extensions?.code;
638
- switch (code) {
639
- case 'UNAUTHENTICATED': return 'AUTHENTICATION_ERROR';
640
- case 'FORBIDDEN': return 'AUTHORIZATION_ERROR';
641
- case 'BAD_USER_INPUT': return 'VALIDATION_ERROR';
642
- case 'INTERNAL_SERVER_ERROR': return 'GRAPHQL_ERROR';
643
- default: return 'GRAPHQL_ERROR';
644
- }
645
- }
646
-
647
- /**
648
- * Execute a GraphQL query
649
- * @param {string} query - GraphQL query
650
- * @param {Object} [variables] - Query variables
651
- * @param {Object} [options] - Query options
652
- * @returns {Promise<*>} Query result
653
- */
654
- async query(query, variables, options = {}) {
655
- const cacheKey = options.cacheKey || generateCacheKey(query, variables);
656
-
657
- // Check for in-flight request (deduplication)
658
- if (this.#options.dedupe && this.#inflightQueries.has(cacheKey)) {
659
- return this.#inflightQueries.get(cacheKey);
660
- }
661
-
662
- const promise = this.#execute(query, variables, options)
663
- .finally(() => {
664
- this.#inflightQueries.delete(cacheKey);
665
- });
666
-
667
- if (this.#options.dedupe) {
668
- this.#inflightQueries.set(cacheKey, promise);
669
- }
670
-
671
- return promise;
672
- }
673
-
674
- /**
675
- * Execute a GraphQL mutation
676
- * @param {string} mutation - GraphQL mutation
677
- * @param {Object} [variables] - Mutation variables
678
- * @param {Object} [options] - Mutation options
679
- * @returns {Promise<*>} Mutation result
680
- */
681
- async mutate(mutation, variables, options = {}) {
682
- return this.#execute(mutation, variables, options);
683
- }
684
-
685
- /**
686
- * Subscribe to a GraphQL subscription
687
- * @param {string} subscription - GraphQL subscription
688
- * @param {Object} [variables] - Subscription variables
689
- * @param {Object} handlers - Event handlers
690
- * @returns {Function} Unsubscribe function
691
- */
692
- subscribe(subscription, variables, handlers) {
693
- this.#initWebSocket();
694
- return this.#subscriptionManager.subscribe(subscription, variables, handlers);
695
- }
696
-
697
- /**
698
- * Invalidate a cache entry
699
- * @param {string} cacheKey - Cache key to invalidate
700
- */
701
- invalidate(cacheKey) {
702
- this.#cache.delete(cacheKey);
703
- }
704
-
705
- /**
706
- * Invalidate all cache entries
707
- */
708
- invalidateAll() {
709
- this.#cache.clear();
710
- }
711
-
712
- /**
713
- * Get cache statistics
714
- * @returns {Object} Cache stats
715
- */
716
- getCacheStats() {
717
- return {
718
- size: this.#cache.size,
719
- keys: Array.from(this.#cache.keys())
720
- };
721
- }
722
-
723
- /**
724
- * Get active subscriptions count
725
- * @returns {number}
726
- */
727
- getActiveSubscriptions() {
728
- return this.#subscriptionManager?.activeCount || 0;
729
- }
730
-
731
- /**
732
- * Close all subscriptions
733
- */
734
- closeAllSubscriptions() {
735
- this.#subscriptionManager?.closeAll();
736
- }
737
-
738
- /**
739
- * Get WebSocket connection state
740
- * @returns {Pulse<string>}
741
- */
742
- get wsState() {
743
- this.#initWebSocket();
744
- return this.#ws.state;
745
- }
746
-
747
- /**
748
- * Get WebSocket connected state
749
- * @returns {Pulse<boolean>}
750
- */
751
- get wsConnected() {
752
- this.#initWebSocket();
753
- return this.#subscriptionManager.connected;
754
- }
755
-
756
- /**
757
- * Create a child client with merged configuration
758
- * @param {Object} options - Override options
759
- * @returns {GraphQLClient} New client instance
760
- */
761
- create(options = {}) {
762
- return new GraphQLClient({
763
- ...this.#options,
764
- headers: { ...this.#options.headers, ...options.headers },
765
- ...options
766
- });
767
- }
768
-
769
- /**
770
- * Dispose the client
771
- */
772
- dispose() {
773
- this.#subscriptionManager?.dispose();
774
- this.#inflightQueries.clear();
775
- this.#cache.clear();
776
- }
777
- }
778
-
779
- // ============================================================================
780
- // Factory Function
781
- // ============================================================================
782
-
783
- /**
784
- * Create a GraphQL client instance
785
- * @param {Object} options - Client options
786
- * @param {string} options.url - GraphQL endpoint URL
787
- * @param {string} [options.wsUrl] - WebSocket URL for subscriptions
788
- * @param {Object} [options.headers] - Default headers
789
- * @param {number} [options.timeout=30000] - Request timeout in ms
790
- * @param {string} [options.credentials='same-origin'] - Fetch credentials
791
- * @param {number} [options.retries=0] - Retry attempts
792
- * @param {number} [options.retryDelay=1000] - Delay between retries
793
- * @param {Object|Function} [options.wsConnectionParams] - WebSocket connection params
794
- * @param {boolean} [options.wsReconnect=true] - Auto-reconnect WebSocket
795
- * @param {number} [options.wsMaxRetries=5] - Max WebSocket reconnection attempts
796
- * @param {boolean} [options.cache=true] - Enable query caching
797
- * @param {number} [options.cacheTime=300000] - Cache TTL in ms
798
- * @param {number} [options.cacheMaxSize=500] - Maximum cache entries (LRU eviction)
799
- * @param {number} [options.staleTime=0] - Stale threshold in ms
800
- * @param {boolean} [options.dedupe=true] - Deduplicate identical in-flight queries
801
- * @param {boolean} [options.throwOnError=true] - Throw on GraphQL errors
802
- * @param {Function} [options.onError] - Global error handler
803
- * @returns {GraphQLClient} GraphQL client instance
804
- */
805
- export function createGraphQLClient(options = {}) {
806
- return new GraphQLClient(options);
807
- }
808
-
809
- // ============================================================================
810
- // Default Client
811
- // ============================================================================
812
-
813
- let defaultClient = null;
814
-
815
- /**
816
- * Set the default GraphQL client
817
- * @param {GraphQLClient} client - Client instance
818
- */
819
- export function setDefaultClient(client) {
820
- defaultClient = client;
821
- }
822
-
823
- /**
824
- * Get the default GraphQL client
825
- * @returns {GraphQLClient|null}
826
- */
827
- export function getDefaultClient() {
828
- return defaultClient;
829
- }
830
-
831
- /**
832
- * Get client from options or default
833
- * @param {Object} options - Options with optional client
834
- * @returns {GraphQLClient} Client instance
835
- */
836
- function getClient(options) {
837
- const client = options.client || defaultClient;
838
- if (!client) {
839
- throw new GraphQLError(
840
- 'No GraphQL client provided. Either pass a client option or set a default client with setDefaultClient().',
841
- { code: 'GRAPHQL_ERROR' }
842
- );
843
- }
844
- return client;
845
- }
846
-
847
- // ============================================================================
848
- // useQuery Hook
849
- // ============================================================================
850
-
851
- /**
852
- * Execute a GraphQL query with caching and reactivity
853
- * @param {string} query - GraphQL query string
854
- * @param {Object|Function} [variables] - Variables or function returning variables
855
- * @param {Object} [options={}] - Query options
856
- * @param {GraphQLClient} [options.client] - GraphQL client instance
857
- * @param {boolean|Pulse<boolean>} [options.enabled=true] - Enable/disable query
858
- * @param {boolean} [options.immediate=true] - Execute immediately
859
- * @param {string|Function} [options.cacheKey] - Custom cache key
860
- * @param {number} [options.cacheTime] - Cache TTL override
861
- * @param {number} [options.staleTime] - Stale threshold override
862
- * @param {boolean} [options.refetchOnFocus=false] - Refetch when window gains focus
863
- * @param {boolean} [options.refetchOnReconnect=false] - Refetch when network reconnects
864
- * @param {number} [options.refetchInterval] - Polling interval in ms
865
- * @param {number} [options.retry] - Retry attempts
866
- * @param {number} [options.retryDelay] - Delay between retries
867
- * @param {Function} [options.onSuccess] - Success callback
868
- * @param {Function} [options.onError] - Error callback
869
- * @param {Function} [options.select] - Transform/select data
870
- * @param {*} [options.placeholderData] - Placeholder while loading
871
- * @param {boolean} [options.keepPreviousData=false] - Keep previous data during refetch
872
- * @returns {Object} Query result with reactive state
873
- */
874
- export function useQuery(query, variables, options = {}) {
875
- const client = getClient(options);
876
-
877
- // Resolve variables (can be function for reactive variables)
878
- const resolveVariables = () => {
879
- if (typeof variables === 'function') {
880
- return variables();
881
- }
882
- return variables;
883
- };
884
-
885
- // Generate cache key
886
- const getCacheKey = () => {
887
- if (typeof options.cacheKey === 'function') {
888
- return options.cacheKey();
889
- }
890
- if (options.cacheKey) {
891
- return options.cacheKey;
892
- }
893
- return generateCacheKey(query, resolveVariables());
894
- };
895
-
896
- // Check if enabled
897
- const isEnabled = () => {
898
- if (typeof options.enabled === 'object' && options.enabled?.get) {
899
- return options.enabled.get();
900
- }
901
- return options.enabled !== false;
902
- };
903
-
904
- // Check if should execute immediately
905
- const shouldExecuteImmediately = options.immediate !== false && isEnabled();
906
-
907
- // State
908
- const data = pulse(options.placeholderData ?? null);
909
- const error = pulse(null);
910
- const loading = pulse(shouldExecuteImmediately);
911
- const fetching = pulse(false);
912
- const isStale = pulse(false);
913
-
914
- const versionController = createVersionedAsync();
915
-
916
- // Execute query
917
- async function executeQuery() {
918
- if (!isEnabled()) return null;
919
-
920
- const ctx = versionController.begin();
921
-
922
- batch(() => {
923
- fetching.set(true);
924
- if (data.get() === null) {
925
- loading.set(true);
926
- }
927
- error.set(null);
928
- });
929
-
930
- try {
931
- const result = await client.query(query, resolveVariables(), {
932
- cacheKey: getCacheKey()
933
- });
934
-
935
- const selectedData = options.select ? options.select(result) : result;
936
-
937
- ctx.ifCurrent(() => {
938
- batch(() => {
939
- data.set(selectedData);
940
- loading.set(false);
941
- fetching.set(false);
942
- isStale.set(false);
943
- });
944
- options.onSuccess?.(selectedData);
945
- });
946
-
947
- return selectedData;
948
- } catch (err) {
949
- const graphqlError = GraphQLError.isGraphQLError(err) ? err : new GraphQLError(err.message, {
950
- code: 'GRAPHQL_ERROR'
951
- });
952
-
953
- ctx.ifCurrent(() => {
954
- batch(() => {
955
- error.set(graphqlError);
956
- loading.set(false);
957
- fetching.set(false);
958
- });
959
- options.onError?.(graphqlError);
960
- });
961
-
962
- return null;
963
- }
964
- }
965
-
966
- // Initial fetch if immediate
967
- if (shouldExecuteImmediately) {
968
- executeQuery();
969
- }
970
-
971
- // Setup auto-refresh interval
972
- if (options.refetchInterval && options.refetchInterval > 0) {
973
- const intervalId = setInterval(() => {
974
- if (!loading.get() && !fetching.get() && isEnabled()) {
975
- executeQuery();
976
- }
977
- }, options.refetchInterval);
978
-
979
- onCleanup(() => clearInterval(intervalId));
980
- }
981
-
982
- // Setup window focus listener
983
- if (options.refetchOnFocus) {
984
- onWindowFocus(() => { if (isEnabled()) executeQuery(); }, onCleanup);
985
- }
986
-
987
- // Setup online listener
988
- if (options.refetchOnReconnect) {
989
- onWindowOnline(() => { if (isEnabled()) executeQuery(); }, onCleanup);
990
- }
991
-
992
- return {
993
- data,
994
- error,
995
- loading,
996
- fetching,
997
- status: computed(() => {
998
- if (loading.get()) return 'loading';
999
- if (error.get()) return 'error';
1000
- if (data.get() !== null) return 'success';
1001
- return 'idle';
1002
- }),
1003
- isStale,
1004
- refetch: executeQuery,
1005
- invalidate: () => {
1006
- isStale.set(true);
1007
- client.invalidate(getCacheKey());
1008
- },
1009
- reset: () => {
1010
- batch(() => {
1011
- data.set(null);
1012
- error.set(null);
1013
- loading.set(false);
1014
- fetching.set(false);
1015
- isStale.set(false);
1016
- });
1017
- }
1018
- };
1019
- }
1020
-
1021
- // ============================================================================
1022
- // useMutation Hook
1023
- // ============================================================================
1024
-
1025
- /**
1026
- * Execute GraphQL mutations
1027
- * @param {string} mutation - GraphQL mutation string
1028
- * @param {Object} [options={}] - Mutation options
1029
- * @param {GraphQLClient} [options.client] - GraphQL client instance
1030
- * @param {Function} [options.onSuccess] - Success callback
1031
- * @param {Function} [options.onError] - Error callback
1032
- * @param {Function} [options.onSettled] - Called after success or error
1033
- * @param {number} [options.retry] - Retry attempts
1034
- * @param {number} [options.retryDelay] - Delay between retries
1035
- * @param {Function} [options.onMutate] - Called before mutation (for optimistic updates)
1036
- * @param {string[]} [options.invalidateQueries] - Cache keys to invalidate on success
1037
- * @returns {Object} Mutation result with execute function
1038
- */
1039
- export function useMutation(mutation, options = {}) {
1040
- const client = getClient(options);
1041
-
1042
- const data = pulse(null);
1043
- const error = pulse(null);
1044
- const loading = pulse(false);
1045
- const status = pulse('idle');
1046
-
1047
- const versionController = createVersionedAsync();
1048
-
1049
- /**
1050
- * Execute the mutation
1051
- * @param {Object} [variables] - Mutation variables
1052
- * @returns {Promise<*>} Mutation result
1053
- */
1054
- async function mutate(variables) {
1055
- const ctx = versionController.begin();
1056
- let rollbackContext;
1057
-
1058
- batch(() => {
1059
- loading.set(true);
1060
- error.set(null);
1061
- status.set('loading');
1062
- });
1063
-
1064
- try {
1065
- // Call onMutate for optimistic updates
1066
- if (options.onMutate) {
1067
- rollbackContext = await options.onMutate(variables);
1068
- }
1069
-
1070
- const result = await client.mutate(mutation, variables);
1071
-
1072
- ctx.ifCurrent(() => {
1073
- batch(() => {
1074
- data.set(result);
1075
- loading.set(false);
1076
- status.set('success');
1077
- });
1078
-
1079
- options.onSuccess?.(result, variables);
1080
- options.onSettled?.(result, null, variables);
1081
-
1082
- // Invalidate queries
1083
- if (options.invalidateQueries) {
1084
- for (const key of options.invalidateQueries) {
1085
- client.invalidate(key);
1086
- }
1087
- }
1088
- });
1089
-
1090
- return result;
1091
- } catch (err) {
1092
- const graphqlError = GraphQLError.isGraphQLError(err) ? err : new GraphQLError(err.message, {
1093
- code: 'GRAPHQL_ERROR'
1094
- });
1095
-
1096
- ctx.ifCurrent(() => {
1097
- batch(() => {
1098
- error.set(graphqlError);
1099
- loading.set(false);
1100
- status.set('error');
1101
- });
1102
-
1103
- options.onError?.(graphqlError, variables, rollbackContext);
1104
- options.onSettled?.(null, graphqlError, variables);
1105
- });
1106
-
1107
- throw graphqlError;
1108
- }
1109
- }
1110
-
1111
- /**
1112
- * Reset mutation state
1113
- */
1114
- function reset() {
1115
- batch(() => {
1116
- data.set(null);
1117
- error.set(null);
1118
- loading.set(false);
1119
- status.set('idle');
1120
- });
1121
- }
1122
-
1123
- return {
1124
- data,
1125
- error,
1126
- loading,
1127
- status,
1128
- mutate,
1129
- mutateAsync: mutate,
1130
- reset
1131
- };
1132
- }
1133
-
1134
- // ============================================================================
1135
- // useSubscription Hook
1136
- // ============================================================================
1137
-
1138
- /**
1139
- * Subscribe to GraphQL subscriptions over WebSocket
1140
- * @param {string} subscription - GraphQL subscription string
1141
- * @param {Object|Function} [variables] - Variables or function returning variables
1142
- * @param {Object} [options={}] - Subscription options
1143
- * @param {GraphQLClient} [options.client] - GraphQL client instance
1144
- * @param {boolean|Pulse<boolean>} [options.enabled=true] - Enable/disable subscription
1145
- * @param {Function} [options.onData] - Called on each message
1146
- * @param {Function} [options.onError] - Error callback
1147
- * @param {Function} [options.onComplete] - Called when subscription ends
1148
- * @param {boolean} [options.shouldResubscribe=true] - Resubscribe on error
1149
- * @param {number} [options.retryBaseDelay=1000] - Base delay for exponential backoff (ms)
1150
- * @param {number} [options.retryMaxDelay=30000] - Maximum delay between retries (ms)
1151
- * @param {number} [options.maxRetries=Infinity] - Maximum number of retry attempts
1152
- * @returns {Object} Subscription result with reactive state
1153
- */
1154
- export function useSubscription(subscription, variables, options = {}) {
1155
- const client = getClient(options);
1156
-
1157
- const data = pulse(null);
1158
- const error = pulse(null);
1159
- const status = pulse('connecting');
1160
- const retryCount = pulse(0);
1161
-
1162
- let unsubscribeFn = null;
1163
- let retryTimeoutId = null;
1164
-
1165
- // Backoff configuration
1166
- const retryBaseDelay = options.retryBaseDelay ?? 1000;
1167
- const retryMaxDelay = options.retryMaxDelay ?? 30000;
1168
- const maxRetries = options.maxRetries ?? Infinity;
1169
-
1170
- /**
1171
- * Calculate delay with exponential backoff and jitter
1172
- * @param {number} attempt - Current retry attempt (0-indexed)
1173
- * @returns {number} Delay in milliseconds
1174
- */
1175
- function calculateBackoffDelay(attempt) {
1176
- // Exponential backoff: baseDelay * 2^attempt
1177
- const exponentialDelay = retryBaseDelay * Math.pow(2, attempt);
1178
- // Cap at max delay
1179
- const cappedDelay = Math.min(exponentialDelay, retryMaxDelay);
1180
- // Add jitter (±25%) to prevent thundering herd
1181
- const jitter = cappedDelay * 0.25 * (Math.random() * 2 - 1);
1182
- return Math.max(0, cappedDelay + jitter);
1183
- }
1184
-
1185
- // Resolve variables
1186
- const resolveVariables = () => {
1187
- if (typeof variables === 'function') {
1188
- return variables();
1189
- }
1190
- return variables;
1191
- };
1192
-
1193
- // Check if enabled
1194
- const isEnabled = () => {
1195
- if (typeof options.enabled === 'object' && options.enabled?.get) {
1196
- return options.enabled.get();
1197
- }
1198
- return options.enabled !== false;
1199
- };
1200
-
1201
- /**
1202
- * Start subscription
1203
- */
1204
- function subscribe() {
1205
- if (!isEnabled() || unsubscribeFn) return;
1206
-
1207
- status.set('connecting');
1208
-
1209
- unsubscribeFn = client.subscribe(subscription, resolveVariables(), {
1210
- onData: (payload) => {
1211
- status.set('connected');
1212
- data.set(payload);
1213
- // Reset retry count on successful data
1214
- retryCount.set(0);
1215
- options.onData?.(payload);
1216
- },
1217
- onError: (err) => {
1218
- error.set(err);
1219
- status.set('error');
1220
- options.onError?.(err);
1221
-
1222
- // Resubscribe on error if enabled and under max retries
1223
- if (options.shouldResubscribe !== false) {
1224
- const currentRetry = retryCount.peek();
1225
- if (currentRetry < maxRetries) {
1226
- unsubscribeFn = null;
1227
- const delay = calculateBackoffDelay(currentRetry);
1228
- retryCount.set(currentRetry + 1);
1229
- status.set('reconnecting');
1230
- retryTimeoutId = setTimeout(() => {
1231
- retryTimeoutId = null;
1232
- subscribe();
1233
- }, delay);
1234
- } else {
1235
- status.set('failed');
1236
- }
1237
- }
1238
- },
1239
- onComplete: () => {
1240
- status.set('closed');
1241
- options.onComplete?.();
1242
- unsubscribeFn = null;
1243
- }
1244
- });
1245
- }
1246
-
1247
- /**
1248
- * Unsubscribe
1249
- */
1250
- function unsubscribe() {
1251
- // Cancel pending retry
1252
- if (retryTimeoutId) {
1253
- clearTimeout(retryTimeoutId);
1254
- retryTimeoutId = null;
1255
- }
1256
- if (unsubscribeFn) {
1257
- unsubscribeFn();
1258
- unsubscribeFn = null;
1259
- status.set('closed');
1260
- }
1261
- retryCount.set(0);
1262
- }
1263
-
1264
- /**
1265
- * Resubscribe (unsubscribe then subscribe)
1266
- */
1267
- function resubscribe() {
1268
- unsubscribe();
1269
- subscribe();
1270
- }
1271
-
1272
- // Start subscription if enabled
1273
- if (isEnabled()) {
1274
- subscribe();
1275
- }
1276
-
1277
- // Watch enabled state for reactive enabling/disabling
1278
- if (typeof options.enabled === 'object' && options.enabled?.get) {
1279
- effect(() => {
1280
- if (options.enabled.get()) {
1281
- subscribe();
1282
- } else {
1283
- unsubscribe();
1284
- }
1285
- });
1286
- }
1287
-
1288
- // Cleanup on dispose
1289
- onCleanup(() => {
1290
- unsubscribe();
1291
- });
1292
-
1293
- return {
1294
- data,
1295
- error,
1296
- status,
1297
- retryCount,
1298
- unsubscribe,
1299
- resubscribe
1300
- };
1301
- }
1302
-
1303
- // ============================================================================
1304
- // Exports
1305
- // ============================================================================
1306
-
1307
- export {
1308
- GraphQLClient,
1309
- InterceptorManager,
1310
- SubscriptionManager,
1311
- generateCacheKey,
1312
- extractOperationName
1313
- };
1314
-
1315
- export default {
1316
- createGraphQLClient,
1317
- GraphQLClient,
1318
- GraphQLError,
1319
- useQuery,
1320
- useMutation,
1321
- useSubscription,
1322
- setDefaultClient,
1323
- getDefaultClient,
1324
- generateCacheKey,
1325
- extractOperationName
1326
- };
15
+ export * from './graphql/index.js';
16
+ export { default } from './graphql/index.js';