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.
- package/README.md +11 -0
- package/cli/build.js +13 -3
- package/compiler/directives.js +356 -0
- package/compiler/lexer.js +18 -3
- package/compiler/parser/core.js +6 -0
- package/compiler/parser/view.js +2 -6
- package/compiler/preprocessor.js +43 -23
- package/compiler/sourcemap.js +3 -1
- package/compiler/transformer/actions.js +329 -0
- package/compiler/transformer/export.js +7 -0
- package/compiler/transformer/expressions.js +85 -33
- package/compiler/transformer/imports.js +3 -0
- package/compiler/transformer/index.js +2 -0
- package/compiler/transformer/store.js +1 -1
- package/compiler/transformer/style.js +45 -16
- package/compiler/transformer/view.js +23 -2
- package/loader/rollup-plugin-server-components.js +391 -0
- package/loader/vite-plugin-server-components.js +420 -0
- package/loader/webpack-loader-server-components.js +356 -0
- package/package.json +124 -82
- package/runtime/async.js +4 -0
- package/runtime/context.js +16 -3
- package/runtime/dom-adapter.js +5 -3
- package/runtime/dom-virtual-list.js +2 -1
- package/runtime/form.js +8 -3
- package/runtime/graphql/cache.js +1 -1
- package/runtime/graphql/client.js +22 -0
- package/runtime/graphql/hooks.js +12 -6
- package/runtime/graphql/subscriptions.js +2 -0
- package/runtime/hmr.js +6 -3
- package/runtime/http.js +1 -0
- package/runtime/i18n.js +2 -0
- package/runtime/lru-cache.js +3 -1
- package/runtime/native.js +46 -20
- package/runtime/pulse.js +3 -0
- package/runtime/router/core.js +5 -1
- package/runtime/router/index.js +17 -1
- package/runtime/router/psc-integration.js +301 -0
- package/runtime/security.js +58 -29
- package/runtime/server-components/actions-server.js +798 -0
- package/runtime/server-components/actions.js +389 -0
- package/runtime/server-components/client.js +447 -0
- package/runtime/server-components/error-sanitizer.js +438 -0
- package/runtime/server-components/index.js +275 -0
- package/runtime/server-components/security-csrf.js +593 -0
- package/runtime/server-components/security-errors.js +227 -0
- package/runtime/server-components/security-ratelimit.js +733 -0
- package/runtime/server-components/security-validation.js +467 -0
- package/runtime/server-components/security.js +598 -0
- package/runtime/server-components/serializer.js +617 -0
- package/runtime/server-components/server.js +382 -0
- package/runtime/server-components/types.js +383 -0
- package/runtime/server-components/utils/mutex.js +60 -0
- package/runtime/server-components/utils/path-sanitizer.js +109 -0
- package/runtime/ssr.js +2 -1
- package/runtime/store.js +19 -10
- package/runtime/utils.js +12 -128
- package/types/animation.d.ts +300 -0
- package/types/i18n.d.ts +283 -0
- package/types/persistence.d.ts +267 -0
- package/types/sse.d.ts +248 -0
- package/types/sw.d.ts +150 -0
- package/runtime/a11y.js.original +0 -1844
- package/runtime/graphql.js.original +0 -1326
- package/runtime/router.js.original +0 -1605
package/runtime/context.js
CHANGED
|
@@ -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
|
-
//
|
|
243
|
-
|
|
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
|
}
|
package/runtime/dom-adapter.js
CHANGED
|
@@ -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
|
|
849
|
-
'
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
1627
|
+
// Object URL may already be revoked; safe to ignore
|
|
1624
1628
|
}
|
|
1625
1629
|
}
|
|
1626
1630
|
const newPreviews = currentPreviews.filter((_, i) => i !== index);
|
|
1627
|
-
|
|
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
|
|
package/runtime/graphql/cache.js
CHANGED
|
@@ -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 =>
|
|
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
|
});
|
package/runtime/graphql/hooks.js
CHANGED
|
@@ -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');
|
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
|
-
|
|
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
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
|
}
|
package/runtime/lru-cache.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
634
|
-
|
|
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
|
-
|
|
648
|
-
|
|
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
|
-
|
|
672
|
-
|
|
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);
|
package/runtime/router/core.js
CHANGED
|
@@ -507,7 +507,11 @@ export function createRouter(options = {}) {
|
|
|
507
507
|
*/
|
|
508
508
|
function outlet(container) {
|
|
509
509
|
if (typeof container === 'string') {
|
|
510
|
-
|
|
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;
|
package/runtime/router/index.js
CHANGED
|
@@ -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
|
};
|