pulse-js-framework 1.10.4 → 1.11.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 (65) hide show
  1. package/README.md +11 -0
  2. package/cli/build.js +13 -3
  3. package/compiler/directives.js +356 -0
  4. package/compiler/lexer.js +18 -3
  5. package/compiler/parser/core.js +6 -0
  6. package/compiler/parser/view.js +2 -6
  7. package/compiler/preprocessor.js +43 -23
  8. package/compiler/sourcemap.js +3 -1
  9. package/compiler/transformer/actions.js +329 -0
  10. package/compiler/transformer/export.js +7 -0
  11. package/compiler/transformer/expressions.js +85 -33
  12. package/compiler/transformer/imports.js +3 -0
  13. package/compiler/transformer/index.js +2 -0
  14. package/compiler/transformer/store.js +1 -1
  15. package/compiler/transformer/style.js +45 -16
  16. package/compiler/transformer/view.js +23 -2
  17. package/loader/rollup-plugin-server-components.js +391 -0
  18. package/loader/vite-plugin-server-components.js +420 -0
  19. package/loader/webpack-loader-server-components.js +356 -0
  20. package/package.json +124 -82
  21. package/runtime/async.js +4 -0
  22. package/runtime/context.js +16 -3
  23. package/runtime/dom-adapter.js +5 -3
  24. package/runtime/dom-virtual-list.js +2 -1
  25. package/runtime/form.js +8 -3
  26. package/runtime/graphql/cache.js +1 -1
  27. package/runtime/graphql/client.js +22 -0
  28. package/runtime/graphql/hooks.js +12 -6
  29. package/runtime/graphql/subscriptions.js +2 -0
  30. package/runtime/hmr.js +6 -3
  31. package/runtime/http.js +1 -0
  32. package/runtime/i18n.js +2 -0
  33. package/runtime/lru-cache.js +3 -1
  34. package/runtime/native.js +46 -20
  35. package/runtime/pulse.js +3 -0
  36. package/runtime/router/core.js +5 -1
  37. package/runtime/router/index.js +17 -1
  38. package/runtime/router/psc-integration.js +301 -0
  39. package/runtime/security.js +58 -29
  40. package/runtime/server-components/actions-server.js +798 -0
  41. package/runtime/server-components/actions.js +389 -0
  42. package/runtime/server-components/client.js +447 -0
  43. package/runtime/server-components/error-sanitizer.js +438 -0
  44. package/runtime/server-components/index.js +275 -0
  45. package/runtime/server-components/security-csrf.js +593 -0
  46. package/runtime/server-components/security-errors.js +227 -0
  47. package/runtime/server-components/security-ratelimit.js +733 -0
  48. package/runtime/server-components/security-validation.js +467 -0
  49. package/runtime/server-components/security.js +598 -0
  50. package/runtime/server-components/serializer.js +617 -0
  51. package/runtime/server-components/server.js +382 -0
  52. package/runtime/server-components/types.js +383 -0
  53. package/runtime/server-components/utils/mutex.js +60 -0
  54. package/runtime/server-components/utils/path-sanitizer.js +109 -0
  55. package/runtime/ssr.js +2 -1
  56. package/runtime/store.js +19 -10
  57. package/runtime/utils.js +12 -128
  58. package/types/animation.d.ts +300 -0
  59. package/types/i18n.d.ts +283 -0
  60. package/types/persistence.d.ts +267 -0
  61. package/types/sse.d.ts +248 -0
  62. package/types/sw.d.ts +150 -0
  63. package/runtime/a11y.js.original +0 -1844
  64. package/runtime/graphql.js.original +0 -1326
  65. package/runtime/router.js.original +0 -1605
@@ -72,6 +72,13 @@ const contextDefaults = new Map();
72
72
  */
73
73
  const contextNames = new Map();
74
74
 
75
+ /**
76
+ * Shared default pulses for contexts without providers
77
+ * Uses a Map keyed by context symbol ID (since context objects are frozen)
78
+ * @type {Map<symbol, Pulse>}
79
+ */
80
+ const contextDefaultPulses = new Map();
81
+
75
82
  // =============================================================================
76
83
  // CONTEXT CREATION
77
84
  // =============================================================================
@@ -235,12 +242,17 @@ export function useContext(context) {
235
242
  return value;
236
243
  }
237
244
 
238
- // No provider found, return default as reactive pulse
245
+ // No provider found, return shared default as reactive pulse
239
246
  log.debug(`useContext: using default for ${context.displayName}`);
240
247
  const defaultVal = contextDefaults.get(context._id);
241
248
 
242
- // Wrap default in pulse for consistent API
243
- return isPulse(defaultVal) ? defaultVal : pulse(defaultVal);
249
+ // Lazily create a shared default pulse so all consumers without a Provider
250
+ // share the same reactive instance (avoids creating a new pulse per call)
251
+ if (isPulse(defaultVal)) return defaultVal;
252
+ if (!contextDefaultPulses.has(context._id)) {
253
+ contextDefaultPulses.set(context._id, pulse(defaultVal));
254
+ }
255
+ return contextDefaultPulses.get(context._id);
244
256
  }
245
257
 
246
258
  /**
@@ -305,6 +317,7 @@ export function disposeContext(context) {
305
317
  contextStacks.delete(context._id);
306
318
  contextDefaults.delete(context._id);
307
319
  contextNames.delete(context._id);
320
+ contextDefaultPulses.delete(context._id);
308
321
 
309
322
  log.debug(`Context disposed: ${context.displayName}`);
310
323
  }
@@ -10,6 +10,8 @@
10
10
  * allowing the same reactive code to run in Node.js, Deno, or custom environments.
11
11
  */
12
12
 
13
+ import { DOMError } from './errors.js';
14
+
13
15
  // ============================================================================
14
16
  // DOM Adapter Interface
15
17
  // ============================================================================
@@ -845,9 +847,9 @@ export function getAdapter() {
845
847
  if (typeof document !== 'undefined') {
846
848
  activeAdapter = new BrowserDOMAdapter();
847
849
  } else {
848
- throw new Error(
849
- '[Pulse] No DOM adapter configured. ' +
850
- 'In non-browser environments, call setAdapter() with a MockDOMAdapter or custom implementation.'
850
+ throw new DOMError(
851
+ 'No DOM adapter configured',
852
+ { code: 'DOM_ADAPTER_MISSING', suggestion: 'In non-browser environments, call setAdapter() with a MockDOMAdapter or custom implementation.' }
851
853
  );
852
854
  }
853
855
  }
@@ -13,6 +13,7 @@ import { pulse, effect, computed, batch } from './pulse.js';
13
13
  import { getAdapter } from './dom-adapter.js';
14
14
  import { list } from './dom-list.js';
15
15
  import { delegate } from './dom-event-delegate.js';
16
+ import { DOMError } from './errors.js';
16
17
 
17
18
  // ============================================================================
18
19
  // Constants
@@ -57,7 +58,7 @@ export function virtualList(getItems, template, keyFn, options = {}) {
57
58
  const { itemHeight, overscan, containerHeight, recycle, on: eventHandlers } = config;
58
59
 
59
60
  if (!itemHeight || itemHeight <= 0) {
60
- throw new Error('[Pulse] virtualList requires a positive itemHeight');
61
+ throw new DOMError('virtualList requires a positive itemHeight', { code: 'INVALID_ITEM_HEIGHT', suggestion: 'Provide options.itemHeight as a positive number' });
61
62
  }
62
63
 
63
64
  const dom = getAdapter();
package/runtime/form.js CHANGED
@@ -40,6 +40,7 @@ function isLocalStorageAvailable() {
40
40
  try {
41
41
  return typeof localStorage !== 'undefined' && localStorage !== null;
42
42
  } catch {
43
+ // localStorage may throw in private browsing or restricted environments
43
44
  return false;
44
45
  }
45
46
  }
@@ -113,6 +114,7 @@ export const validators = {
113
114
  new URL(value);
114
115
  return true;
115
116
  } catch {
117
+ // URL constructor throws on invalid URLs — treat as validation failure
116
118
  return message;
117
119
  }
118
120
  }
@@ -641,6 +643,7 @@ export function useForm(initialValues, validationSchema = {}, options = {}) {
641
643
  try {
642
644
  return localStorage.getItem(persistKey) !== null;
643
645
  } catch {
646
+ // localStorage may be unavailable (private browsing, quota exceeded)
644
647
  return false;
645
648
  }
646
649
  })());
@@ -853,7 +856,7 @@ export function useForm(initialValues, validationSchema = {}, options = {}) {
853
856
  try {
854
857
  localStorage.removeItem(persistKey);
855
858
  } catch {
856
- // Ignore
859
+ // localStorage may throw if storage is restricted; safe to ignore on cleanup
857
860
  }
858
861
  }
859
862
  hasDraft.set(false);
@@ -1616,15 +1619,17 @@ export function useFileField(options = {}) {
1616
1619
  // Revoke removed preview URL
1617
1620
  if (preview && canCreateObjectURL()) {
1618
1621
  const currentPreviews = previews.get();
1622
+ // Revoke the URL at this file index (may be null for non-image files)
1619
1623
  if (currentPreviews[index]) {
1620
1624
  try {
1621
1625
  URL.revokeObjectURL(currentPreviews[index]);
1622
1626
  } catch {
1623
- // Ignore
1627
+ // Object URL may already be revoked; safe to ignore
1624
1628
  }
1625
1629
  }
1626
1630
  const newPreviews = currentPreviews.filter((_, i) => i !== index);
1627
- previewUrls = previewUrls.filter((_, i) => i !== index);
1631
+ // Rebuild previewUrls from the filtered previews to avoid index mismatch
1632
+ previewUrls = newPreviews.filter(Boolean);
1628
1633
  previews.set(newPreviews);
1629
1634
  }
1630
1635
 
@@ -50,7 +50,7 @@ function stableStringify(obj) {
50
50
  return '[' + obj.map(stableStringify).join(',') + ']';
51
51
  }
52
52
  const keys = Object.keys(obj).sort();
53
- return '{' + keys.map(k => `"${k}":${stableStringify(obj[k])}`).join(',') + '}';
53
+ return '{' + keys.map(k => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(',') + '}';
54
54
  }
55
55
 
56
56
  /**
@@ -8,6 +8,8 @@
8
8
 
9
9
  import { pulse, computed, batch } from '../pulse.js';
10
10
  import { createHttp, HttpError } from '../http.js';
11
+ import { createWebSocket } from '../websocket.js';
12
+ import { SubscriptionManager } from './subscriptions.js';
11
13
  import { ClientError } from '../errors.js';
12
14
  import { LRUCache } from '../lru-cache.js';
13
15
  import { InterceptorManager } from '../interceptor-manager.js';
@@ -370,12 +372,32 @@ export class GraphQLClient {
370
372
  async query(query, variables, options = {}) {
371
373
  const cacheKey = options.cacheKey || generateCacheKey(query, variables);
372
374
 
375
+ // Check LRU cache first (if caching is enabled and not explicitly skipped)
376
+ if (this.#options.cache && !options.skipCache) {
377
+ const cached = this.#cache.get(cacheKey);
378
+ if (cached) {
379
+ const age = Date.now() - cached.timestamp;
380
+ if (age < this.#options.cacheTime) {
381
+ return cached.data;
382
+ }
383
+ // Stale entry — remove it
384
+ this.#cache.delete(cacheKey);
385
+ }
386
+ }
387
+
373
388
  // Check for in-flight request (deduplication)
374
389
  if (this.#options.dedupe && this.#inflightQueries.has(cacheKey)) {
375
390
  return this.#inflightQueries.get(cacheKey);
376
391
  }
377
392
 
378
393
  const promise = this.#execute(query, variables, options)
394
+ .then((data) => {
395
+ // Populate cache on successful query
396
+ if (this.#options.cache) {
397
+ this.#cache.set(cacheKey, { data, timestamp: Date.now() });
398
+ }
399
+ return data;
400
+ })
379
401
  .finally(() => {
380
402
  this.#inflightQueries.delete(cacheKey);
381
403
  });
@@ -102,7 +102,7 @@ export function useQuery(query, variables, options = {}) {
102
102
  const versionController = createVersionedAsync();
103
103
 
104
104
  // Execute query
105
- async function executeQuery() {
105
+ async function executeQuery(queryOptions = {}) {
106
106
  if (!isEnabled()) return null;
107
107
 
108
108
  const ctx = versionController.begin();
@@ -117,7 +117,8 @@ export function useQuery(query, variables, options = {}) {
117
117
 
118
118
  try {
119
119
  const result = await client.query(query, resolveVariables(), {
120
- cacheKey: getCacheKey()
120
+ cacheKey: getCacheKey(),
121
+ ...queryOptions
121
122
  });
122
123
 
123
124
  const selectedData = options.select ? options.select(result) : result;
@@ -160,7 +161,7 @@ export function useQuery(query, variables, options = {}) {
160
161
  if (options.refetchInterval && options.refetchInterval > 0) {
161
162
  const intervalId = setInterval(() => {
162
163
  if (!loading.get() && !fetching.get() && isEnabled()) {
163
- executeQuery();
164
+ executeQuery({ skipCache: true });
164
165
  }
165
166
  }, options.refetchInterval);
166
167
 
@@ -169,12 +170,12 @@ export function useQuery(query, variables, options = {}) {
169
170
 
170
171
  // Setup window focus listener
171
172
  if (options.refetchOnFocus) {
172
- onWindowFocus(() => { if (isEnabled()) executeQuery(); }, onCleanup);
173
+ onWindowFocus(() => { if (isEnabled()) executeQuery({ skipCache: true }); }, onCleanup);
173
174
  }
174
175
 
175
176
  // Setup online listener
176
177
  if (options.refetchOnReconnect) {
177
- onWindowOnline(() => { if (isEnabled()) executeQuery(); }, onCleanup);
178
+ onWindowOnline(() => { if (isEnabled()) executeQuery({ skipCache: true }); }, onCleanup);
178
179
  }
179
180
 
180
181
  return {
@@ -189,7 +190,7 @@ export function useQuery(query, variables, options = {}) {
189
190
  return 'idle';
190
191
  }),
191
192
  isStale,
192
- refetch: executeQuery,
193
+ refetch: () => executeQuery({ skipCache: true }),
193
194
  invalidate: () => {
194
195
  isStale.set(true);
195
196
  client.invalidate(getCacheKey());
@@ -411,7 +412,12 @@ export function useSubscription(subscription, variables, options = {}) {
411
412
  if (options.shouldResubscribe !== false) {
412
413
  const currentRetry = retryCount.peek();
413
414
  if (currentRetry < maxRetries) {
415
+ // Send Complete to server before abandoning the old subscription
416
+ const oldUnsubscribe = unsubscribeFn;
414
417
  unsubscribeFn = null;
418
+ if (oldUnsubscribe) {
419
+ try { oldUnsubscribe(); } catch { /* connection may already be closed */ }
420
+ }
415
421
  const delay = calculateBackoffDelay(currentRetry);
416
422
  retryCount.set(currentRetry + 1);
417
423
  status.set('reconnecting');
@@ -98,6 +98,8 @@ export class SubscriptionManager {
98
98
  code: 'SUBSCRIPTION_ERROR'
99
99
  }));
100
100
  }
101
+ // Clear stale entries to prevent accumulation on reconnect
102
+ this.#subscriptions.clear();
101
103
  }
102
104
 
103
105
  /**
package/runtime/hmr.js CHANGED
@@ -56,12 +56,15 @@ import { setCurrentModule, clearCurrentModule, disposeModule } from './pulse.js'
56
56
  */
57
57
  export function createHMRContext(moduleId) {
58
58
  // Check if HMR is available (Vite dev server)
59
- if (typeof import.meta === 'undefined' || !import.meta.hot) {
59
+ // import.meta.hot is injected by Vite at build time
60
+ // Also check globalThis.__PULSE_HMR_HOT__ for testing purposes
61
+ const hot = (typeof import.meta !== 'undefined' && import.meta.hot) ||
62
+ (typeof globalThis !== 'undefined' && (globalThis.__PULSE_HMR_HOT__ || globalThis.import?.meta?.hot));
63
+
64
+ if (!hot) {
60
65
  return createNoopContext();
61
66
  }
62
67
 
63
- const hot = import.meta.hot;
64
-
65
68
  // Initialize data storage if not present
66
69
  if (!hot.data) {
67
70
  hot.data = {};
package/runtime/http.js CHANGED
@@ -246,6 +246,7 @@ class HttpClient {
246
246
  try {
247
247
  return JSON.parse(text);
248
248
  } catch {
249
+ // Response body is not valid JSON — return raw text
249
250
  return text;
250
251
  }
251
252
 
package/runtime/i18n.js CHANGED
@@ -322,6 +322,7 @@ export function createI18n(options = {}) {
322
322
  try {
323
323
  return new Intl.NumberFormat(locale.get(), opts).format(value);
324
324
  } catch {
325
+ // Intl may throw on invalid locale or value — fall back to plain string
325
326
  return String(value);
326
327
  }
327
328
  }
@@ -337,6 +338,7 @@ export function createI18n(options = {}) {
337
338
  try {
338
339
  return new Intl.DateTimeFormat(locale.get(), opts).format(value);
339
340
  } catch {
341
+ // Intl may throw on invalid locale or date — fall back to plain string
340
342
  return String(value);
341
343
  }
342
344
  }
@@ -6,6 +6,8 @@
6
6
  * When capacity is reached, the least recently used item is evicted.
7
7
  */
8
8
 
9
+ import { RuntimeError } from './errors.js';
10
+
9
11
  /**
10
12
  * LRU Cache implementation
11
13
  * Uses Map's insertion order for O(1) operations.
@@ -20,7 +22,7 @@ export class LRUCache {
20
22
  */
21
23
  constructor(capacity, options = {}) {
22
24
  if (capacity <= 0) {
23
- throw new Error('LRU cache capacity must be greater than 0');
25
+ throw new RuntimeError('LRU cache capacity must be greater than 0', { code: 'INVALID_CAPACITY' });
24
26
  }
25
27
  this._capacity = capacity;
26
28
  this._cache = new Map();
package/runtime/native.js CHANGED
@@ -10,6 +10,7 @@
10
10
  import { pulse, effect, batch } from './pulse.js';
11
11
  import { loggers } from './logger.js';
12
12
  import { DANGEROUS_KEYS } from './security.js';
13
+ import { RuntimeError } from './errors.js';
13
14
 
14
15
  const log = loggers.native;
15
16
 
@@ -92,9 +93,10 @@ function _compareSemver(a, b) {
92
93
  * Validate that an object has all required methods/properties
93
94
  * @param {Object} obj - Object to validate
94
95
  * @param {string[]} required - Required property names
96
+ * @param {string} [namespace] - Namespace label for error context
95
97
  * @returns {{valid: boolean, missing: string[]}}
96
98
  */
97
- function _validateApiSurface(obj, required) {
99
+ function _validateApiSurface(obj, required, namespace = '') {
98
100
  if (!obj || typeof obj !== 'object') {
99
101
  return { valid: false, missing: required };
100
102
  }
@@ -281,6 +283,7 @@ function _tryParseJson(value) {
281
283
  try {
282
284
  return JSON.parse(value);
283
285
  } catch {
286
+ // Value is not valid JSON — return the raw string as-is
284
287
  return value;
285
288
  }
286
289
  }
@@ -374,7 +377,7 @@ export function getNative() {
374
377
  'This API only works in a Pulse native mobile app. ' +
375
378
  'For web, use isNativeAvailable() to check before calling native APIs, ' +
376
379
  'or use getPlatform() to detect the current environment.';
377
- throw new Error(error);
380
+ throw new RuntimeError(error, { code: 'NATIVE_UNAVAILABLE', suggestion: 'Use isNativeAvailable() to check before calling native APIs' });
378
381
  }
379
382
  return window.PulseMobile;
380
383
  }
@@ -500,6 +503,7 @@ export function createNativeStorage(prefix = '') {
500
503
  export function createDeviceInfo() {
501
504
  const info = pulse(null);
502
505
  const network = pulse({ connected: true, type: 'unknown' });
506
+ const cleanups = [];
503
507
 
504
508
  // Load device info
505
509
  if (isNativeAvailable()) {
@@ -529,13 +533,14 @@ export function createDeviceInfo() {
529
533
  type: 'unknown'
530
534
  });
531
535
 
532
- window.addEventListener('online', () => {
533
- network.set({ connected: true, type: 'unknown' });
534
- });
535
-
536
- window.addEventListener('offline', () => {
537
- network.set({ connected: false, type: 'none' });
538
- });
536
+ const onOnline = () => { network.set({ connected: true, type: 'unknown' }); };
537
+ const onOffline = () => { network.set({ connected: false, type: 'none' }); };
538
+ window.addEventListener('online', onOnline);
539
+ window.addEventListener('offline', onOffline);
540
+ cleanups.push(
541
+ () => window.removeEventListener('online', onOnline),
542
+ () => window.removeEventListener('offline', onOffline)
543
+ );
539
544
  }
540
545
  }
541
546
 
@@ -559,6 +564,12 @@ export function createDeviceInfo() {
559
564
  /** Is currently online */
560
565
  get isOnline() {
561
566
  return network.get().connected;
567
+ },
568
+
569
+ /** Dispose event listeners to prevent memory leaks */
570
+ dispose() {
571
+ cleanups.forEach(fn => fn());
572
+ cleanups.length = 0;
562
573
  }
563
574
  };
564
575
  }
@@ -627,30 +638,38 @@ export const NativeClipboard = {
627
638
 
628
639
  /**
629
640
  * App lifecycle - pause handler
641
+ * @param {Function} callback - Called when app is paused/hidden
642
+ * @returns {Function} Cleanup function to remove the listener
630
643
  */
631
644
  export function onAppPause(callback) {
645
+ const cleanups = [];
632
646
  if (typeof document !== 'undefined') {
633
- document.addEventListener('visibilitychange', () => {
634
- if (document.hidden) callback();
635
- });
647
+ const handler = () => { if (document.hidden) callback(); };
648
+ document.addEventListener('visibilitychange', handler);
649
+ cleanups.push(() => document.removeEventListener('visibilitychange', handler));
636
650
  }
637
651
  if (isNativeAvailable()) {
638
652
  getNative().App.onPause(callback);
639
653
  }
654
+ return () => cleanups.forEach(fn => fn());
640
655
  }
641
656
 
642
657
  /**
643
658
  * App lifecycle - resume handler
659
+ * @param {Function} callback - Called when app is resumed/visible
660
+ * @returns {Function} Cleanup function to remove the listener
644
661
  */
645
662
  export function onAppResume(callback) {
663
+ const cleanups = [];
646
664
  if (typeof document !== 'undefined') {
647
- document.addEventListener('visibilitychange', () => {
648
- if (!document.hidden) callback();
649
- });
665
+ const handler = () => { if (!document.hidden) callback(); };
666
+ document.addEventListener('visibilitychange', handler);
667
+ cleanups.push(() => document.removeEventListener('visibilitychange', handler));
650
668
  }
651
669
  if (isNativeAvailable()) {
652
670
  getNative().App.onResume(callback);
653
671
  }
672
+ return () => cleanups.forEach(fn => fn());
654
673
  }
655
674
 
656
675
  /**
@@ -664,19 +683,26 @@ export function onBackButton(callback) {
664
683
 
665
684
  /**
666
685
  * Wait for native bridge to be ready
686
+ * @param {Function} callback - Called with { platform } when bridge is ready
687
+ * @returns {Function} Cleanup function to remove the listener
667
688
  */
668
689
  export function onNativeReady(callback) {
669
- if (typeof window === 'undefined') return;
690
+ if (typeof window === 'undefined') return () => {};
670
691
 
671
- window.addEventListener('pulse:ready', (e) => {
672
- callback(e.detail);
673
- });
692
+ const handler = (e) => { callback(e.detail); };
693
+ window.addEventListener('pulse:ready', handler);
674
694
 
695
+ let timerId;
675
696
  // If already ready (web or native initialized)
676
697
  if (typeof window.PulseMobile !== 'undefined') {
677
698
  const platform = window.PulseMobile.platform;
678
- setTimeout(() => callback({ platform }), 0);
699
+ timerId = setTimeout(() => callback({ platform }), 0);
679
700
  }
701
+
702
+ return () => {
703
+ window.removeEventListener('pulse:ready', handler);
704
+ if (timerId) clearTimeout(timerId);
705
+ };
680
706
  }
681
707
 
682
708
  /**
package/runtime/pulse.js CHANGED
@@ -20,6 +20,7 @@
20
20
 
21
21
  import { loggers } from './logger.js';
22
22
  import { Errors } from './errors.js';
23
+ import { DANGEROUS_KEYS } from './security.js';
23
24
 
24
25
  const log = loggers.pulse;
25
26
 
@@ -1080,6 +1081,8 @@ export function createState(obj) {
1080
1081
  const pulses = {};
1081
1082
 
1082
1083
  for (const [key, value] of Object.entries(obj)) {
1084
+ if (DANGEROUS_KEYS.has(key)) continue;
1085
+
1083
1086
  if (Array.isArray(value)) {
1084
1087
  // Arrays get special handling with reactive methods
1085
1088
  pulses[key] = new Pulse(value);
@@ -507,7 +507,11 @@ export function createRouter(options = {}) {
507
507
  */
508
508
  function outlet(container) {
509
509
  if (typeof container === 'string') {
510
- container = document.querySelector(container);
510
+ const el = document.querySelector(container);
511
+ if (!el) {
512
+ throw new Error(`outlet(): no element found matching selector "${container}"`);
513
+ }
514
+ container = el;
511
515
  }
512
516
 
513
517
  let currentView = null;
@@ -12,6 +12,7 @@ export * from './lazy.js';
12
12
  export * from './guards.js';
13
13
  export * from './history.js';
14
14
  export * from './utils.js';
15
+ export * from './psc-integration.js';
15
16
 
16
17
  // Default export for backward compatibility
17
18
  import {
@@ -22,6 +23,14 @@ import {
22
23
  } from './core.js';
23
24
  import { lazy, preload } from './lazy.js';
24
25
  import { matchRoute, parseQuery, buildQueryString } from './utils.js';
26
+ import {
27
+ fetchPSCPayload,
28
+ navigatePSC,
29
+ prefetchPSC,
30
+ clearPSCCache,
31
+ getPSCCacheStats,
32
+ configurePSCCache
33
+ } from './psc-integration.js';
25
34
 
26
35
  export default {
27
36
  createRouter,
@@ -32,5 +41,12 @@ export default {
32
41
  parseQuery,
33
42
  buildQueryString,
34
43
  onBeforeLeave,
35
- onAfterEnter
44
+ onAfterEnter,
45
+ // PSC Integration
46
+ fetchPSCPayload,
47
+ navigatePSC,
48
+ prefetchPSC,
49
+ clearPSCCache,
50
+ getPSCCacheStats,
51
+ configurePSCCache
36
52
  };