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
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Pulse GraphQL - Query Caching
3
+ *
4
+ * Utilities for creating cache keys and managing query cache
5
+ *
6
+ * @module pulse-js-framework/runtime/graphql/cache
7
+ */
8
+
9
+ import { LRUCache } from '../lru-cache.js';
10
+
11
+ // ============================================================================
12
+ // Cache Key Utilities
13
+ // ============================================================================
14
+
15
+ /**
16
+ * Extract operation name from GraphQL query string
17
+ * @param {string} query - GraphQL query string
18
+ * @returns {string|null} Operation name or null
19
+ */
20
+ export function extractOperationName(query) {
21
+ const match = query.match(/(?:query|mutation|subscription)\s+(\w+)/);
22
+ return match ? match[1] : null;
23
+ }
24
+
25
+ /**
26
+ * Simple hash function for strings
27
+ * @param {string} str - String to hash
28
+ * @returns {string} Hash string
29
+ */
30
+ function hashString(str) {
31
+ if (!str) return '';
32
+ let hash = 0;
33
+ for (let i = 0; i < str.length; i++) {
34
+ const char = str.charCodeAt(i);
35
+ hash = ((hash << 5) - hash) + char;
36
+ hash = hash & hash; // Convert to 32bit integer
37
+ }
38
+ return Math.abs(hash).toString(36);
39
+ }
40
+
41
+ /**
42
+ * Stable JSON stringify with sorted keys
43
+ * @param {*} obj - Object to stringify
44
+ * @returns {string} Stable JSON string
45
+ */
46
+ function stableStringify(obj) {
47
+ if (obj === null || obj === undefined) return '';
48
+ if (typeof obj !== 'object') return JSON.stringify(obj);
49
+ if (Array.isArray(obj)) {
50
+ return '[' + obj.map(stableStringify).join(',') + ']';
51
+ }
52
+ const keys = Object.keys(obj).sort();
53
+ return '{' + keys.map(k => `"${k}":${stableStringify(obj[k])}`).join(',') + '}';
54
+ }
55
+
56
+ /**
57
+ * Generate a deterministic cache key for a GraphQL operation
58
+ * @param {string} query - GraphQL query string
59
+ * @param {Object} [variables] - Query variables
60
+ * @returns {string} Cache key
61
+ */
62
+ export function generateCacheKey(query, variables) {
63
+ const operationName = extractOperationName(query);
64
+ const variablesHash = variables ? hashString(stableStringify(variables)) : '';
65
+ const queryHash = operationName || hashString(query.replace(/\s+/g, ' ').trim());
66
+ return `gql:${queryHash}${variablesHash ? ':' + variablesHash : ''}`;
67
+ }
68
+
69
+
@@ -0,0 +1,563 @@
1
+ /**
2
+ * Pulse GraphQL - Core Client
3
+ *
4
+ * GraphQL client with error handling, HTTP requests, and interceptors
5
+ *
6
+ * @module pulse-js-framework/runtime/graphql/client
7
+ */
8
+
9
+ import { pulse, computed, batch } from '../pulse.js';
10
+ import { createHttp, HttpError } from '../http.js';
11
+ import { ClientError } from '../errors.js';
12
+ import { LRUCache } from '../lru-cache.js';
13
+ import { InterceptorManager } from '../interceptor-manager.js';
14
+ import { onWindowFocus, onWindowOnline } from '../utils.js';
15
+ import { generateCacheKey, extractOperationName } from './cache.js';
16
+
17
+ // ============================================================================
18
+ // GraphQL Error Class
19
+ // ============================================================================
20
+
21
+ /**
22
+ * Error class for GraphQL operations.
23
+ * Extends ClientError for consistent error handling patterns.
24
+ */
25
+ export class GraphQLError extends ClientError {
26
+ static suggestions = {
27
+ GRAPHQL_ERROR: 'Check the GraphQL errors array for specific field errors.',
28
+ NETWORK_ERROR: 'Verify network connectivity and GraphQL endpoint URL.',
29
+ PARSE_ERROR: 'The response was not valid GraphQL JSON. Check server configuration.',
30
+ TIMEOUT: 'Request timed out. Consider increasing timeout or optimizing query.',
31
+ AUTHENTICATION_ERROR: 'Authentication required. Check your credentials or token.',
32
+ AUTHORIZATION_ERROR: 'Insufficient permissions for this operation.',
33
+ VALIDATION_ERROR: 'Invalid input provided. Check variables match schema types.',
34
+ SUBSCRIPTION_ERROR: 'WebSocket subscription failed. Check connection status.'
35
+ };
36
+
37
+ static errorName = 'GraphQLError';
38
+ static defaultCode = 'GRAPHQL_ERROR';
39
+ static markerProperty = 'isGraphQLError';
40
+
41
+ /**
42
+ * @param {string} message - Error message
43
+ * @param {Object} [options={}] - Error options
44
+ * @param {string} [options.code] - Error code
45
+ * @param {Array} [options.errors] - GraphQL errors array from response
46
+ * @param {*} [options.data] - Partial data from response
47
+ * @param {Object} [options.extensions] - GraphQL extensions
48
+ * @param {Object} [options.response] - Full HTTP response
49
+ * @param {Object} [options.request] - Request configuration
50
+ */
51
+ constructor(message, options = {}) {
52
+ super(message, options);
53
+ this.errors = options.errors || [];
54
+ this.data = options.data ?? null;
55
+ this.extensions = options.extensions || {};
56
+ this.response = options.response || null;
57
+ this.request = options.request || null;
58
+ }
59
+
60
+ /**
61
+ * Check if error is a GraphQLError
62
+ * @param {*} error - Error to check
63
+ * @returns {boolean}
64
+ */
65
+ static isGraphQLError(error) {
66
+ return error?.isGraphQLError === true || error instanceof GraphQLError;
67
+ }
68
+
69
+ /**
70
+ * Check if response has partial data along with errors
71
+ * @returns {boolean}
72
+ */
73
+ hasPartialData() {
74
+ return this.data !== null && this.data !== undefined;
75
+ }
76
+
77
+ /**
78
+ * Check if this is an authentication error
79
+ * @returns {boolean}
80
+ */
81
+ isAuthenticationError() {
82
+ return this.code === 'AUTHENTICATION_ERROR' ||
83
+ this.errors.some(e => e.extensions?.code === 'UNAUTHENTICATED');
84
+ }
85
+
86
+ /**
87
+ * Check if this is an authorization error
88
+ * @returns {boolean}
89
+ */
90
+ isAuthorizationError() {
91
+ return this.code === 'AUTHORIZATION_ERROR' ||
92
+ this.errors.some(e => e.extensions?.code === 'FORBIDDEN');
93
+ }
94
+
95
+ /**
96
+ * Check if this is a validation error
97
+ * @returns {boolean}
98
+ */
99
+ isValidationError() {
100
+ return this.code === 'VALIDATION_ERROR' ||
101
+ this.errors.some(e => e.extensions?.code === 'BAD_USER_INPUT');
102
+ }
103
+
104
+ /**
105
+ * Get the first error message from GraphQL errors
106
+ * @returns {string|null}
107
+ */
108
+ getFirstError() {
109
+ return this.errors[0]?.message || null;
110
+ }
111
+
112
+ /**
113
+ * Get all error messages
114
+ * @returns {string[]}
115
+ */
116
+ getAllErrors() {
117
+ return this.errors.map(e => e.message);
118
+ }
119
+
120
+ /**
121
+ * Convert to JSON
122
+ * @returns {Object}
123
+ */
124
+ toJSON() {
125
+ return {
126
+ name: this.name,
127
+ message: this.message,
128
+ code: this.code,
129
+ errors: this.errors,
130
+ data: this.data,
131
+ extensions: this.extensions
132
+ };
133
+ }
134
+ }
135
+
136
+ // ============================================================================
137
+ // GraphQL Client
138
+ // ============================================================================
139
+
140
+ /**
141
+ * GraphQL Client class
142
+ */
143
+ export class GraphQLClient {
144
+ #http;
145
+ #ws = null;
146
+ #subscriptionManager = null;
147
+ #options;
148
+ #inflightQueries = new Map();
149
+ // LRU cache to prevent unbounded memory growth (default 500 entries)
150
+ #cache;
151
+
152
+ /**
153
+ * Request interceptors
154
+ */
155
+ interceptors = {
156
+ request: new InterceptorManager(),
157
+ response: new InterceptorManager()
158
+ };
159
+
160
+ /**
161
+ * @param {Object} options - Client options
162
+ */
163
+ constructor(options = {}) {
164
+ if (!options.url) {
165
+ throw new GraphQLError('GraphQL client requires a url option', {
166
+ code: 'GRAPHQL_ERROR'
167
+ });
168
+ }
169
+
170
+ this.#options = {
171
+ url: options.url,
172
+ wsUrl: options.wsUrl || this.#deriveWsUrl(options.url),
173
+ headers: options.headers || {},
174
+ timeout: options.timeout ?? 30000,
175
+ credentials: options.credentials || 'same-origin',
176
+ retries: options.retries ?? 0,
177
+ retryDelay: options.retryDelay ?? 1000,
178
+ wsConnectionParams: options.wsConnectionParams,
179
+ wsReconnect: options.wsReconnect ?? true,
180
+ wsMaxRetries: options.wsMaxRetries ?? 5,
181
+ cache: options.cache ?? true,
182
+ cacheTime: options.cacheTime ?? 300000,
183
+ cacheMaxSize: options.cacheMaxSize ?? 500,
184
+ staleTime: options.staleTime ?? 0,
185
+ dedupe: options.dedupe ?? true,
186
+ throwOnError: options.throwOnError ?? true,
187
+ onError: options.onError
188
+ };
189
+
190
+ // Initialize LRU cache to prevent unbounded memory growth
191
+ this.#cache = new LRUCache(this.#options.cacheMaxSize);
192
+
193
+ // Create HTTP client for queries/mutations
194
+ this.#http = createHttp({
195
+ baseURL: '',
196
+ timeout: this.#options.timeout,
197
+ headers: {
198
+ 'Content-Type': 'application/json',
199
+ ...this.#options.headers
200
+ },
201
+ withCredentials: this.#options.credentials === 'include',
202
+ retries: this.#options.retries,
203
+ retryDelay: this.#options.retryDelay
204
+ });
205
+ }
206
+
207
+ /**
208
+ * Derive WebSocket URL from HTTP URL
209
+ * @param {string} url - HTTP URL
210
+ * @returns {string} WebSocket URL
211
+ */
212
+ #deriveWsUrl(url) {
213
+ try {
214
+ const parsed = new URL(url, globalThis.location?.origin || 'http://localhost');
215
+ parsed.protocol = parsed.protocol === 'https:' ? 'wss:' : 'ws:';
216
+ return parsed.toString();
217
+ } catch {
218
+ return url.replace(/^http/, 'ws');
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Initialize WebSocket for subscriptions (lazy)
224
+ */
225
+ #initWebSocket() {
226
+ if (this.#ws) return;
227
+
228
+ this.#ws = createWebSocket(this.#options.wsUrl, {
229
+ protocols: ['graphql-transport-ws'],
230
+ reconnect: this.#options.wsReconnect,
231
+ maxRetries: this.#options.wsMaxRetries,
232
+ autoConnect: false,
233
+ autoParseJson: true,
234
+ queueWhileDisconnected: true
235
+ });
236
+
237
+ this.#subscriptionManager = new SubscriptionManager(
238
+ this.#ws,
239
+ this.#options.wsConnectionParams
240
+ );
241
+ }
242
+
243
+ /**
244
+ * Execute a GraphQL operation
245
+ * @param {string} query - GraphQL query/mutation
246
+ * @param {Object} [variables] - Variables
247
+ * @param {Object} [options] - Request options
248
+ * @returns {Promise<Object>} Response data
249
+ */
250
+ async #execute(query, variables, options = {}) {
251
+ const operationName = extractOperationName(query);
252
+
253
+ let config = {
254
+ query,
255
+ variables,
256
+ operationName,
257
+ ...options
258
+ };
259
+
260
+ // Run request interceptors
261
+ for (const interceptor of this.interceptors.request) {
262
+ if (interceptor.fulfilled) {
263
+ try {
264
+ config = await interceptor.fulfilled(config);
265
+ } catch (err) {
266
+ if (interceptor.rejected) {
267
+ config = await interceptor.rejected(err);
268
+ } else {
269
+ throw err;
270
+ }
271
+ }
272
+ }
273
+ }
274
+
275
+ try {
276
+ const response = await this.#http.post(this.#options.url, {
277
+ query: config.query,
278
+ variables: config.variables,
279
+ operationName: config.operationName
280
+ });
281
+
282
+ let result = response.data;
283
+
284
+ // Run response interceptors
285
+ for (const interceptor of this.interceptors.response) {
286
+ if (interceptor.fulfilled) {
287
+ try {
288
+ result = await interceptor.fulfilled(result);
289
+ } catch (err) {
290
+ if (interceptor.rejected) {
291
+ result = await interceptor.rejected(err);
292
+ } else {
293
+ throw err;
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ return this.#processResponse(result, config);
300
+ } catch (error) {
301
+ // Convert HTTP errors to GraphQL errors
302
+ if (HttpError.isHttpError(error)) {
303
+ const graphqlError = new GraphQLError(error.message, {
304
+ code: error.isTimeout() ? 'TIMEOUT' : error.isNetworkError() ? 'NETWORK_ERROR' : 'GRAPHQL_ERROR',
305
+ request: config
306
+ });
307
+ this.#options.onError?.(graphqlError);
308
+ throw graphqlError;
309
+ }
310
+ throw error;
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Process GraphQL response
316
+ * @param {Object} response - GraphQL response
317
+ * @param {Object} config - Request config
318
+ * @returns {*} Response data
319
+ */
320
+ #processResponse(response, config) {
321
+ const { data, errors, extensions } = response;
322
+
323
+ // No errors - return data
324
+ if (!errors || errors.length === 0) {
325
+ return data;
326
+ }
327
+
328
+ // Has errors
329
+ const error = new GraphQLError(errors[0].message, {
330
+ code: this.#mapErrorCode(errors[0]),
331
+ errors,
332
+ data,
333
+ extensions,
334
+ request: config
335
+ });
336
+
337
+ this.#options.onError?.(error);
338
+
339
+ if (this.#options.throwOnError) {
340
+ throw error;
341
+ }
342
+
343
+ // Return data even with errors if throwOnError is false
344
+ return data;
345
+ }
346
+
347
+ /**
348
+ * Map GraphQL error to error code
349
+ * @param {Object} error - GraphQL error
350
+ * @returns {string} Error code
351
+ */
352
+ #mapErrorCode(error) {
353
+ const code = error.extensions?.code;
354
+ switch (code) {
355
+ case 'UNAUTHENTICATED': return 'AUTHENTICATION_ERROR';
356
+ case 'FORBIDDEN': return 'AUTHORIZATION_ERROR';
357
+ case 'BAD_USER_INPUT': return 'VALIDATION_ERROR';
358
+ case 'INTERNAL_SERVER_ERROR': return 'GRAPHQL_ERROR';
359
+ default: return 'GRAPHQL_ERROR';
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Execute a GraphQL query
365
+ * @param {string} query - GraphQL query
366
+ * @param {Object} [variables] - Query variables
367
+ * @param {Object} [options] - Query options
368
+ * @returns {Promise<*>} Query result
369
+ */
370
+ async query(query, variables, options = {}) {
371
+ const cacheKey = options.cacheKey || generateCacheKey(query, variables);
372
+
373
+ // Check for in-flight request (deduplication)
374
+ if (this.#options.dedupe && this.#inflightQueries.has(cacheKey)) {
375
+ return this.#inflightQueries.get(cacheKey);
376
+ }
377
+
378
+ const promise = this.#execute(query, variables, options)
379
+ .finally(() => {
380
+ this.#inflightQueries.delete(cacheKey);
381
+ });
382
+
383
+ if (this.#options.dedupe) {
384
+ this.#inflightQueries.set(cacheKey, promise);
385
+ }
386
+
387
+ return promise;
388
+ }
389
+
390
+ /**
391
+ * Execute a GraphQL mutation
392
+ * @param {string} mutation - GraphQL mutation
393
+ * @param {Object} [variables] - Mutation variables
394
+ * @param {Object} [options] - Mutation options
395
+ * @returns {Promise<*>} Mutation result
396
+ */
397
+ async mutate(mutation, variables, options = {}) {
398
+ return this.#execute(mutation, variables, options);
399
+ }
400
+
401
+ /**
402
+ * Subscribe to a GraphQL subscription
403
+ * @param {string} subscription - GraphQL subscription
404
+ * @param {Object} [variables] - Subscription variables
405
+ * @param {Object} handlers - Event handlers
406
+ * @returns {Function} Unsubscribe function
407
+ */
408
+ subscribe(subscription, variables, handlers) {
409
+ this.#initWebSocket();
410
+ return this.#subscriptionManager.subscribe(subscription, variables, handlers);
411
+ }
412
+
413
+ /**
414
+ * Invalidate a cache entry
415
+ * @param {string} cacheKey - Cache key to invalidate
416
+ */
417
+ invalidate(cacheKey) {
418
+ this.#cache.delete(cacheKey);
419
+ }
420
+
421
+ /**
422
+ * Invalidate all cache entries
423
+ */
424
+ invalidateAll() {
425
+ this.#cache.clear();
426
+ }
427
+
428
+ /**
429
+ * Get cache statistics
430
+ * @returns {Object} Cache stats
431
+ */
432
+ getCacheStats() {
433
+ return {
434
+ size: this.#cache.size,
435
+ keys: Array.from(this.#cache.keys())
436
+ };
437
+ }
438
+
439
+ /**
440
+ * Get active subscriptions count
441
+ * @returns {number}
442
+ */
443
+ getActiveSubscriptions() {
444
+ return this.#subscriptionManager?.activeCount || 0;
445
+ }
446
+
447
+ /**
448
+ * Close all subscriptions
449
+ */
450
+ closeAllSubscriptions() {
451
+ this.#subscriptionManager?.closeAll();
452
+ }
453
+
454
+ /**
455
+ * Get WebSocket connection state
456
+ * @returns {Pulse<string>}
457
+ */
458
+ get wsState() {
459
+ this.#initWebSocket();
460
+ return this.#ws.state;
461
+ }
462
+
463
+ /**
464
+ * Get WebSocket connected state
465
+ * @returns {Pulse<boolean>}
466
+ */
467
+ get wsConnected() {
468
+ this.#initWebSocket();
469
+ return this.#subscriptionManager.connected;
470
+ }
471
+
472
+ /**
473
+ * Create a child client with merged configuration
474
+ * @param {Object} options - Override options
475
+ * @returns {GraphQLClient} New client instance
476
+ */
477
+ create(options = {}) {
478
+ return new GraphQLClient({
479
+ ...this.#options,
480
+ headers: { ...this.#options.headers, ...options.headers },
481
+ ...options
482
+ });
483
+ }
484
+
485
+ /**
486
+ * Dispose the client
487
+ */
488
+ dispose() {
489
+ this.#subscriptionManager?.dispose();
490
+ this.#inflightQueries.clear();
491
+ this.#cache.clear();
492
+ }
493
+ }
494
+
495
+ // ============================================================================
496
+ // Factory Function
497
+ // ============================================================================
498
+
499
+ /**
500
+ * Create a GraphQL client instance
501
+ * @param {Object} options - Client options
502
+ * @param {string} options.url - GraphQL endpoint URL
503
+ * @param {string} [options.wsUrl] - WebSocket URL for subscriptions
504
+ * @param {Object} [options.headers] - Default headers
505
+ * @param {number} [options.timeout=30000] - Request timeout in ms
506
+ * @param {string} [options.credentials='same-origin'] - Fetch credentials
507
+ * @param {number} [options.retries=0] - Retry attempts
508
+ * @param {number} [options.retryDelay=1000] - Delay between retries
509
+ * @param {Object|Function} [options.wsConnectionParams] - WebSocket connection params
510
+ * @param {boolean} [options.wsReconnect=true] - Auto-reconnect WebSocket
511
+ * @param {number} [options.wsMaxRetries=5] - Max WebSocket reconnection attempts
512
+ * @param {boolean} [options.cache=true] - Enable query caching
513
+ * @param {number} [options.cacheTime=300000] - Cache TTL in ms
514
+ * @param {number} [options.cacheMaxSize=500] - Maximum cache entries (LRU eviction)
515
+ * @param {number} [options.staleTime=0] - Stale threshold in ms
516
+ * @param {boolean} [options.dedupe=true] - Deduplicate identical in-flight queries
517
+ * @param {boolean} [options.throwOnError=true] - Throw on GraphQL errors
518
+ * @param {Function} [options.onError] - Global error handler
519
+ * @returns {GraphQLClient} GraphQL client instance
520
+ */
521
+ export function createGraphQLClient(options = {}) {
522
+ return new GraphQLClient(options);
523
+ }
524
+
525
+ // ============================================================================
526
+ // Default Client
527
+ // ============================================================================
528
+
529
+ let defaultClient = null;
530
+
531
+ /**
532
+ * Set the default GraphQL client
533
+ * @param {GraphQLClient} client - Client instance
534
+ */
535
+ export function setDefaultClient(client) {
536
+ defaultClient = client;
537
+ }
538
+
539
+ /**
540
+ * Get the default GraphQL client
541
+ * @returns {GraphQLClient|null}
542
+ */
543
+ export function getDefaultClient() {
544
+ return defaultClient;
545
+ }
546
+
547
+ /**
548
+ * Get client from options or default
549
+ * @param {Object} options - Options with optional client
550
+ * @returns {GraphQLClient} Client instance
551
+ */
552
+ function getClient(options) {
553
+ const client = options.client || defaultClient;
554
+ if (!client) {
555
+ throw new GraphQLError(
556
+ 'No GraphQL client provided. Either pass a client option or set a default client with setDefaultClient().',
557
+ { code: 'GRAPHQL_ERROR' }
558
+ );
559
+ }
560
+ return client;
561
+ }
562
+
563
+