@everystate/core 1.0.5 → 1.0.6

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 CHANGED
@@ -1,4 +1,4 @@
1
- # @everystate/core v1.0.5
1
+ # @everystate/core v1.0.6
2
2
 
3
3
  **EveryState: Observable state management with dot-path addressing**
4
4
 
@@ -35,6 +35,14 @@ store.subscribe('user.*', ({ path, value }) => {
35
35
  console.log(`User field ${path} changed to:`, value);
36
36
  });
37
37
 
38
+ // Check if a path exists (handles intentional undefined)
39
+ store.has('count'); // true
40
+ store.has('nonexistent'); // false
41
+
42
+ // List all leaf paths under a prefix
43
+ store.keys('user'); // ['user.name']
44
+ store.keys(); // ['count', 'user.name']
45
+
38
46
  // Cleanup
39
47
  unsub();
40
48
  ```
@@ -112,6 +120,7 @@ EveryState is a reactive state management library where:
112
120
  - **Path-based subscriptions**: Subscribe to exactly what you need
113
121
  - **Wildcard support**: `user.*` catches all user changes
114
122
  - **Atomic batching**: Multiple writes, single notification per path
123
+ - **Path introspection**: `has()` and `keys()` for runtime path discovery
115
124
  - **Zero dependencies**: ~2KB minified
116
125
  - **Framework-agnostic**: Works with React, Vue, Angular, Svelte, or vanilla JS
117
126
 
package/everyState.js CHANGED
@@ -142,6 +142,57 @@ export function createEveryState(initial = {}) {
142
142
  return cur;
143
143
  },
144
144
 
145
+ /**
146
+ * Check if a path exists in the store.
147
+ * Unlike get(path) !== undefined, this correctly handles
148
+ * paths whose value is intentionally set to undefined.
149
+ * @param {string} path - Dot-separated path
150
+ * @returns {boolean} true if the path exists
151
+ */
152
+ has(path) {
153
+ if (destroyed) throw new Error('Cannot check destroyed store');
154
+ if (!path) return true;
155
+ const parts = path.split('.');
156
+ let cur = state;
157
+ for (const p of parts) {
158
+ if (cur == null || typeof cur !== 'object' || !(p in cur)) return false;
159
+ cur = cur[p];
160
+ }
161
+ return true;
162
+ },
163
+
164
+ /**
165
+ * List all leaf paths under a prefix.
166
+ * @param {string} [prefix] - Dot-separated prefix (e.g., 'css').
167
+ * If omitted, lists all paths in the entire store.
168
+ * @returns {string[]} Array of dot-separated leaf paths
169
+ */
170
+ keys(prefix) {
171
+ if (destroyed) throw new Error('Cannot list keys of destroyed store');
172
+ let root = state;
173
+ let base = '';
174
+ if (prefix) {
175
+ base = prefix;
176
+ const parts = prefix.split('.');
177
+ for (const p of parts) {
178
+ if (root == null || typeof root !== 'object' || !(p in root)) return [];
179
+ root = root[p];
180
+ }
181
+ }
182
+ const result = [];
183
+ function walk(obj, path) {
184
+ if (obj == null || typeof obj !== 'object') {
185
+ result.push(path);
186
+ return;
187
+ }
188
+ for (const k of Object.keys(obj)) {
189
+ walk(obj[k], path ? `${path}.${k}` : k);
190
+ }
191
+ }
192
+ walk(root, base);
193
+ return result;
194
+ },
195
+
145
196
  /**
146
197
  * Set value at path and notify subscribers
147
198
  * @param {string} path - Dot-separated path (e.g., 'user.profile.name')
package/index.d.ts CHANGED
@@ -22,6 +22,18 @@ export interface EveryStateStore {
22
22
  */
23
23
  get(path?: string): any;
24
24
 
25
+ /**
26
+ * Check if a path exists in the store.
27
+ * Unlike `get(path) !== undefined`, correctly handles intentionally-stored undefined.
28
+ */
29
+ has(path: string): boolean;
30
+
31
+ /**
32
+ * List all leaf paths under a prefix.
33
+ * If no prefix is provided, lists all paths in the entire store.
34
+ */
35
+ keys(prefix?: string): string[];
36
+
25
37
  /**
26
38
  * Set value at a dot-separated path and notify subscribers.
27
39
  * @returns The value that was set
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystate/core",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "EveryState: Lightweight event-driven state management with path-based subscriptions, wildcards, and async support",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/self-test.js CHANGED
@@ -284,9 +284,72 @@ assert('wildcard-only: detail.oldValue correct', s12detail?.oldValue === 'Alice'
284
284
 
285
285
  s12.destroy();
286
286
 
287
+ console.log('\n13. has()');
288
+ const s13 = createEveryState({ count: 0, user: { name: 'Alice' }, empty: undefined });
289
+
290
+ assert('has: existing primitive', s13.has('count') === true);
291
+ assert('has: existing nested', s13.has('user.name') === true);
292
+ assert('has: existing parent', s13.has('user') === true);
293
+ assert('has: missing path', s13.has('nonexistent') === false);
294
+ assert('has: deep missing', s13.has('user.email') === false);
295
+ assert('has: deep missing parent', s13.has('a.b.c') === false);
296
+ assert('has: no path returns true', s13.has('') === true);
297
+
298
+ // set undefined intentionally, then check
299
+ s13.set('maybe', undefined);
300
+ assert('has: intentionally undefined', s13.has('maybe') === true);
301
+
302
+ // destroyed store
303
+ s13.destroy();
304
+ let threwHas = false;
305
+ try { s13.has('count'); } catch { threwHas = true; }
306
+ assert('has: destroyed throws', threwHas);
307
+
308
+ console.log('\n14. keys()');
309
+ const s14 = createEveryState({ count: 0, user: { name: 'Alice', age: 30 }, flag: true });
310
+
311
+ // All keys
312
+ const allKeys = s14.keys();
313
+ assert('keys: returns array', Array.isArray(allKeys));
314
+ assert('keys: has count', allKeys.includes('count'));
315
+ assert('keys: has user.name', allKeys.includes('user.name'));
316
+ assert('keys: has user.age', allKeys.includes('user.age'));
317
+ assert('keys: has flag', allKeys.includes('flag'));
318
+ assert('keys: no parent objects', !allKeys.includes('user'));
319
+ assert('keys: correct count', allKeys.length === 4);
320
+
321
+ // Prefix
322
+ const userKeys = s14.keys('user');
323
+ assert('keys(prefix): returns children', userKeys.length === 2);
324
+ assert('keys(prefix): has user.name', userKeys.includes('user.name'));
325
+ assert('keys(prefix): has user.age', userKeys.includes('user.age'));
326
+
327
+ // Missing prefix
328
+ const missing = s14.keys('nonexistent');
329
+ assert('keys(missing): empty array', missing.length === 0);
330
+
331
+ // Deep prefix
332
+ s14.set('a.b.c', 1);
333
+ s14.set('a.b.d', 2);
334
+ s14.set('a.e', 3);
335
+ const aKeys = s14.keys('a');
336
+ assert('keys(deep prefix): a has 3 leaves', aKeys.length === 3);
337
+ assert('keys(deep prefix): a.b.c', aKeys.includes('a.b.c'));
338
+ assert('keys(deep prefix): a.b.d', aKeys.includes('a.b.d'));
339
+ assert('keys(deep prefix): a.e', aKeys.includes('a.e'));
340
+
341
+ const abKeys = s14.keys('a.b');
342
+ assert('keys(deep prefix): a.b has 2 leaves', abKeys.length === 2);
343
+
344
+ // Destroyed store
345
+ s14.destroy();
346
+ let threwKeys = false;
347
+ try { s14.keys(); } catch { threwKeys = true; }
348
+ assert('keys: destroyed throws', threwKeys);
349
+
287
350
  // Results
288
351
 
289
- console.log(`\n@everystate/core v1.0.0 self-test`);
352
+ console.log(`\n@everystate/core v1.0.6 self-test`);
290
353
  console.log(`✓ ${passed} assertions passed${failed ? `, ✗ ${failed} failed` : ''}\n`);
291
354
 
292
355
  if (failed > 0) process.exit(1);