pulse-js-framework 1.4.10 → 1.5.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/runtime/store.js CHANGED
@@ -97,7 +97,11 @@ export function createStore(initialState = {}, options = {}) {
97
97
  */
98
98
  function createPulse(key, value) {
99
99
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
100
- // Nested object - create nested store
100
+ // Create a pulse for the nested object itself (for $setState support)
101
+ const objectPulse = pulse(value);
102
+ pulses[key] = objectPulse;
103
+
104
+ // Also create nested pulses for individual properties
101
105
  const nested = {};
102
106
  for (const [k, v] of Object.entries(value)) {
103
107
  nested[k] = createPulse(`${key}.${k}`, v);
@@ -142,16 +146,46 @@ export function createStore(initialState = {}, options = {}) {
142
146
  return snapshot;
143
147
  }
144
148
 
149
+ /**
150
+ * Update nested pulses recursively when a parent object is updated
151
+ * @private
152
+ * @param {string} prefix - The key prefix (e.g., 'user')
153
+ * @param {Object} obj - The new object value
154
+ */
155
+ function updateNestedPulses(prefix, obj) {
156
+ for (const [k, v] of Object.entries(obj)) {
157
+ const fullKey = `${prefix}.${k}`;
158
+ if (pulses[fullKey]) {
159
+ pulses[fullKey].set(v);
160
+ // Recursively update deeper nested objects
161
+ if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
162
+ updateNestedPulses(fullKey, v);
163
+ }
164
+ }
165
+ }
166
+ }
167
+
145
168
  /**
146
169
  * Set multiple values at once (batched)
170
+ * Supports both top-level and nested object updates
147
171
  * @param {Object} updates - Key-value pairs to update
148
172
  * @returns {void}
173
+ * @example
174
+ * // Top-level update
175
+ * store.$setState({ count: 5 });
176
+ *
177
+ * // Nested object update
178
+ * store.$setState({ user: { name: 'Jane', age: 25 } });
149
179
  */
150
180
  function setState(updates) {
151
181
  batch(() => {
152
182
  for (const [key, value] of Object.entries(updates)) {
153
183
  if (pulses[key]) {
154
184
  pulses[key].set(value);
185
+ // If the value is an object, also update nested pulses
186
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
187
+ updateNestedPulses(key, value);
188
+ }
155
189
  }
156
190
  }
157
191
  });
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Pulse Runtime Utilities
3
+ * @module pulse-js-framework/runtime/utils
4
+ *
5
+ * Common utility functions for the Pulse framework runtime.
6
+ */
7
+
8
+ // ============================================================================
9
+ // XSS Prevention
10
+ // ============================================================================
11
+
12
+ /**
13
+ * HTML entity escape map
14
+ * @private
15
+ */
16
+ const HTML_ESCAPES = {
17
+ '&': '&',
18
+ '<': '&lt;',
19
+ '>': '&gt;',
20
+ '"': '&quot;',
21
+ "'": '&#39;'
22
+ };
23
+
24
+ /**
25
+ * Regex for HTML special characters
26
+ * @private
27
+ */
28
+ const HTML_ESCAPE_REGEX = /[&<>"']/g;
29
+
30
+ /**
31
+ * Escape HTML special characters to prevent XSS attacks.
32
+ * Use this when inserting untrusted content into HTML.
33
+ *
34
+ * @param {*} str - Value to escape (will be converted to string)
35
+ * @returns {string} Escaped string safe for HTML insertion
36
+ *
37
+ * @example
38
+ * escapeHtml('<script>alert("xss")</script>')
39
+ * // Returns: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'
40
+ *
41
+ * @example
42
+ * // Safe to insert into HTML
43
+ * element.innerHTML = `<div>${escapeHtml(userInput)}</div>`;
44
+ */
45
+ export function escapeHtml(str) {
46
+ if (str == null) return '';
47
+ return String(str).replace(HTML_ESCAPE_REGEX, char => HTML_ESCAPES[char]);
48
+ }
49
+
50
+ /**
51
+ * Unescape HTML entities back to their original characters.
52
+ *
53
+ * @param {string} str - HTML-escaped string
54
+ * @returns {string} Unescaped string
55
+ *
56
+ * @example
57
+ * unescapeHtml('&lt;div&gt;')
58
+ * // Returns: '<div>'
59
+ */
60
+ export function unescapeHtml(str) {
61
+ if (str == null) return '';
62
+ return String(str)
63
+ .replace(/&amp;/g, '&')
64
+ .replace(/&lt;/g, '<')
65
+ .replace(/&gt;/g, '>')
66
+ .replace(/&quot;/g, '"')
67
+ .replace(/&#39;/g, "'");
68
+ }
69
+
70
+ /**
71
+ * Explicitly set innerHTML with a warning.
72
+ * This function is intentionally named to make it obvious that
73
+ * it's potentially dangerous and bypasses XSS protection.
74
+ *
75
+ * SECURITY WARNING: Only use this with trusted, sanitized HTML.
76
+ * Never use with user-provided content without proper sanitization.
77
+ *
78
+ * @param {HTMLElement} element - Target element
79
+ * @param {string} html - HTML string to insert
80
+ *
81
+ * @example
82
+ * // Only use with trusted HTML!
83
+ * dangerouslySetInnerHTML(container, sanitizedHtml);
84
+ */
85
+ export function dangerouslySetInnerHTML(element, html) {
86
+ element.innerHTML = html;
87
+ }
88
+
89
+ /**
90
+ * Create a text node from a value, safely escaping it.
91
+ * This is the recommended way to insert dynamic text content.
92
+ *
93
+ * Note: DOM textContent is already safe from XSS, but this function
94
+ * provides a consistent API and handles null/undefined values.
95
+ *
96
+ * @param {*} value - Value to convert to text node
97
+ * @returns {Text} Safe text node
98
+ *
99
+ * @example
100
+ * const node = createSafeTextNode(userInput);
101
+ * container.appendChild(node);
102
+ */
103
+ export function createSafeTextNode(value) {
104
+ return document.createTextNode(value == null ? '' : String(value));
105
+ }
106
+
107
+ // ============================================================================
108
+ // Attribute Handling
109
+ // ============================================================================
110
+
111
+ /**
112
+ * Escape a value for use in an HTML attribute.
113
+ * Escapes quotes and special characters.
114
+ *
115
+ * @param {*} value - Value to escape
116
+ * @returns {string} Escaped string safe for attribute values
117
+ *
118
+ * @example
119
+ * const safe = escapeAttribute(userInput);
120
+ * element.setAttribute('data-value', safe);
121
+ */
122
+ export function escapeAttribute(value) {
123
+ if (value == null) return '';
124
+ return String(value)
125
+ .replace(/&/g, '&amp;')
126
+ .replace(/"/g, '&quot;')
127
+ .replace(/'/g, '&#39;')
128
+ .replace(/</g, '&lt;')
129
+ .replace(/>/g, '&gt;');
130
+ }
131
+
132
+ /**
133
+ * Safely set an attribute on an element.
134
+ * Validates attribute names to prevent attribute injection.
135
+ *
136
+ * @param {HTMLElement} element - Target element
137
+ * @param {string} name - Attribute name
138
+ * @param {*} value - Attribute value
139
+ * @returns {boolean} True if attribute was set
140
+ *
141
+ * @example
142
+ * safeSetAttribute(element, 'data-id', userId);
143
+ */
144
+ export function safeSetAttribute(element, name, value) {
145
+ // Validate attribute name (prevent injection attacks)
146
+ if (!/^[a-zA-Z][a-zA-Z0-9\-_:.]*$/.test(name)) {
147
+ console.warn(`Invalid attribute name: ${name}`);
148
+ return false;
149
+ }
150
+
151
+ // Prevent dangerous attributes
152
+ const dangerousAttrs = ['onclick', 'onerror', 'onload', 'onmouseover', 'onfocus', 'onblur'];
153
+ if (dangerousAttrs.includes(name.toLowerCase())) {
154
+ console.warn(`Potentially dangerous attribute blocked: ${name}`);
155
+ return false;
156
+ }
157
+
158
+ element.setAttribute(name, value == null ? '' : String(value));
159
+ return true;
160
+ }
161
+
162
+ // ============================================================================
163
+ // URL Validation
164
+ // ============================================================================
165
+
166
+ /**
167
+ * Validate and sanitize a URL to prevent javascript: and data: XSS.
168
+ *
169
+ * @param {string} url - URL to validate
170
+ * @param {Object} [options] - Validation options
171
+ * @param {boolean} [options.allowData=false] - Allow data: URLs
172
+ * @param {boolean} [options.allowRelative=true] - Allow relative URLs
173
+ * @returns {string|null} Sanitized URL or null if invalid
174
+ *
175
+ * @example
176
+ * const safeUrl = sanitizeUrl(userProvidedUrl);
177
+ * if (safeUrl) {
178
+ * link.href = safeUrl;
179
+ * }
180
+ */
181
+ export function sanitizeUrl(url, options = {}) {
182
+ const { allowData = false, allowRelative = true } = options;
183
+
184
+ if (url == null || url === '') return null;
185
+
186
+ const trimmed = String(url).trim();
187
+
188
+ // Check for javascript: protocol (case insensitive, handles encoding)
189
+ const lowerUrl = trimmed.toLowerCase().replace(/\s/g, '');
190
+ if (lowerUrl.startsWith('javascript:')) {
191
+ return null;
192
+ }
193
+
194
+ // Check for data: protocol
195
+ if (lowerUrl.startsWith('data:')) {
196
+ return allowData ? trimmed : null;
197
+ }
198
+
199
+ // Allow relative URLs
200
+ if (allowRelative && (trimmed.startsWith('/') || trimmed.startsWith('.') || !trimmed.includes(':'))) {
201
+ return trimmed;
202
+ }
203
+
204
+ // Only allow http: and https: protocols
205
+ if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
206
+ return trimmed;
207
+ }
208
+
209
+ return null;
210
+ }
211
+
212
+ // ============================================================================
213
+ // Deep Clone
214
+ // ============================================================================
215
+
216
+ /**
217
+ * Deep clone an object or array.
218
+ * Handles nested objects, arrays, dates, and primitive values.
219
+ *
220
+ * @template T
221
+ * @param {T} obj - Object to clone
222
+ * @returns {T} Deep clone of the object
223
+ *
224
+ * @example
225
+ * const clone = deepClone(originalObject);
226
+ */
227
+ export function deepClone(obj) {
228
+ if (obj === null || typeof obj !== 'object') {
229
+ return obj;
230
+ }
231
+
232
+ if (obj instanceof Date) {
233
+ return new Date(obj.getTime());
234
+ }
235
+
236
+ if (Array.isArray(obj)) {
237
+ return obj.map(item => deepClone(item));
238
+ }
239
+
240
+ const clone = {};
241
+ for (const key in obj) {
242
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
243
+ clone[key] = deepClone(obj[key]);
244
+ }
245
+ }
246
+ return clone;
247
+ }
248
+
249
+ // ============================================================================
250
+ // Debounce / Throttle
251
+ // ============================================================================
252
+
253
+ /**
254
+ * Create a debounced version of a function.
255
+ * The function will only be called after the specified delay
256
+ * has passed without any new calls.
257
+ *
258
+ * @param {Function} fn - Function to debounce
259
+ * @param {number} delay - Delay in milliseconds
260
+ * @returns {Function} Debounced function
261
+ *
262
+ * @example
263
+ * const debouncedSearch = debounce(search, 300);
264
+ * input.addEventListener('input', debouncedSearch);
265
+ */
266
+ export function debounce(fn, delay) {
267
+ let timeoutId = null;
268
+
269
+ const debounced = function(...args) {
270
+ if (timeoutId) {
271
+ clearTimeout(timeoutId);
272
+ }
273
+ timeoutId = setTimeout(() => {
274
+ fn.apply(this, args);
275
+ timeoutId = null;
276
+ }, delay);
277
+ };
278
+
279
+ debounced.cancel = () => {
280
+ if (timeoutId) {
281
+ clearTimeout(timeoutId);
282
+ timeoutId = null;
283
+ }
284
+ };
285
+
286
+ return debounced;
287
+ }
288
+
289
+ /**
290
+ * Create a throttled version of a function.
291
+ * The function will be called at most once per specified interval.
292
+ *
293
+ * @param {Function} fn - Function to throttle
294
+ * @param {number} interval - Minimum interval between calls in milliseconds
295
+ * @returns {Function} Throttled function
296
+ *
297
+ * @example
298
+ * const throttledScroll = throttle(handleScroll, 100);
299
+ * window.addEventListener('scroll', throttledScroll);
300
+ */
301
+ export function throttle(fn, interval) {
302
+ let lastCall = 0;
303
+ let timeoutId = null;
304
+
305
+ const throttled = function(...args) {
306
+ const now = Date.now();
307
+ const remaining = interval - (now - lastCall);
308
+
309
+ if (remaining <= 0) {
310
+ if (timeoutId) {
311
+ clearTimeout(timeoutId);
312
+ timeoutId = null;
313
+ }
314
+ lastCall = now;
315
+ fn.apply(this, args);
316
+ } else if (!timeoutId) {
317
+ timeoutId = setTimeout(() => {
318
+ lastCall = Date.now();
319
+ timeoutId = null;
320
+ fn.apply(this, args);
321
+ }, remaining);
322
+ }
323
+ };
324
+
325
+ throttled.cancel = () => {
326
+ if (timeoutId) {
327
+ clearTimeout(timeoutId);
328
+ timeoutId = null;
329
+ }
330
+ };
331
+
332
+ return throttled;
333
+ }
334
+
335
+ export default {
336
+ // XSS Prevention
337
+ escapeHtml,
338
+ unescapeHtml,
339
+ dangerouslySetInnerHTML,
340
+ createSafeTextNode,
341
+ escapeAttribute,
342
+ safeSetAttribute,
343
+ sanitizeUrl,
344
+ // Utilities
345
+ deepClone,
346
+ debounce,
347
+ throttle
348
+ };
package/types/index.d.ts CHANGED
@@ -161,3 +161,22 @@ export {
161
161
  SourceMapConsumer,
162
162
  encodeVLQ
163
163
  } from './sourcemap';
164
+
165
+ // LRU Cache
166
+ export { LRUCache } from './lru-cache';
167
+
168
+ // Utilities (XSS Prevention, etc.)
169
+ export {
170
+ escapeHtml,
171
+ unescapeHtml,
172
+ dangerouslySetInnerHTML,
173
+ createSafeTextNode,
174
+ escapeAttribute,
175
+ safeSetAttribute,
176
+ SanitizeUrlOptions,
177
+ sanitizeUrl,
178
+ deepClone,
179
+ CancellableFunction,
180
+ debounce,
181
+ throttle
182
+ } from './utils';
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Pulse Framework - LRU Cache Type Definitions
3
+ * @module pulse-js-framework/runtime/lru-cache
4
+ */
5
+
6
+ /**
7
+ * LRU (Least Recently Used) Cache implementation.
8
+ * Uses Map's insertion order for efficient O(1) operations.
9
+ * When capacity is reached, the least recently accessed item is evicted.
10
+ *
11
+ * @template K - Key type
12
+ * @template V - Value type
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const cache = new LRUCache<string, number>(100);
17
+ * cache.set('key1', 42);
18
+ * cache.get('key1'); // 42 - moves 'key1' to most recently used
19
+ * ```
20
+ */
21
+ export declare class LRUCache<K = unknown, V = unknown> {
22
+ /**
23
+ * Create an LRU cache
24
+ * @param capacity - Maximum number of items to store (must be > 0)
25
+ * @throws {Error} If capacity is <= 0
26
+ */
27
+ constructor(capacity: number);
28
+
29
+ /**
30
+ * Get an item from the cache.
31
+ * Accessing an item moves it to the "most recently used" position.
32
+ *
33
+ * @param key - Cache key
34
+ * @returns The cached value, or undefined if not found
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const value = cache.get('myKey');
39
+ * if (value !== undefined) {
40
+ * // Use the cached value
41
+ * }
42
+ * ```
43
+ */
44
+ get(key: K): V | undefined;
45
+
46
+ /**
47
+ * Set an item in the cache.
48
+ * If the cache is at capacity, evicts the least recently used item.
49
+ *
50
+ * @param key - Cache key
51
+ * @param value - Value to store
52
+ * @returns this (for chaining)
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * cache.set('key1', 'value1').set('key2', 'value2');
57
+ * ```
58
+ */
59
+ set(key: K, value: V): this;
60
+
61
+ /**
62
+ * Check if a key exists in the cache.
63
+ * Note: This does NOT update the item's "recently used" position.
64
+ *
65
+ * @param key - Cache key
66
+ * @returns true if key exists
67
+ */
68
+ has(key: K): boolean;
69
+
70
+ /**
71
+ * Delete an item from the cache.
72
+ *
73
+ * @param key - Cache key
74
+ * @returns true if item was deleted, false if key didn't exist
75
+ */
76
+ delete(key: K): boolean;
77
+
78
+ /**
79
+ * Clear all items from the cache.
80
+ */
81
+ clear(): void;
82
+
83
+ /**
84
+ * Get the current number of items in the cache.
85
+ */
86
+ readonly size: number;
87
+
88
+ /**
89
+ * Get the maximum capacity of the cache.
90
+ */
91
+ readonly capacity: number;
92
+
93
+ /**
94
+ * Get all keys in the cache (oldest to newest).
95
+ * @returns Iterator of keys
96
+ */
97
+ keys(): IterableIterator<K>;
98
+
99
+ /**
100
+ * Get all values in the cache (oldest to newest).
101
+ * @returns Iterator of values
102
+ */
103
+ values(): IterableIterator<V>;
104
+
105
+ /**
106
+ * Get all entries in the cache (oldest to newest).
107
+ * @returns Iterator of [key, value] pairs
108
+ */
109
+ entries(): IterableIterator<[K, V]>;
110
+
111
+ /**
112
+ * Iterate over all entries.
113
+ * @param callback - Called for each entry
114
+ */
115
+ forEach(callback: (value: V, key: K, cache: LRUCache<K, V>) => void): void;
116
+ }
117
+
118
+ export default LRUCache;