@everystate/core 1.0.4 → 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 +28 -10
- package/everyState.js +51 -0
- package/index.d.ts +12 -0
- package/package.json +5 -2
- package/self-test.js +64 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @everystate/core
|
|
1
|
+
# @everystate/core v1.0.6
|
|
2
2
|
|
|
3
3
|
**EveryState: Observable state management with dot-path addressing**
|
|
4
4
|
|
|
@@ -10,6 +10,8 @@ Every piece of state has a name. Every name is subscribable. Every operation is
|
|
|
10
10
|
npm install @everystate/core
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
> **Zero external dependencies** - Pure state management with no third-party packages required.
|
|
14
|
+
|
|
13
15
|
## Quick Start
|
|
14
16
|
|
|
15
17
|
```js
|
|
@@ -33,6 +35,14 @@ store.subscribe('user.*', ({ path, value }) => {
|
|
|
33
35
|
console.log(`User field ${path} changed to:`, value);
|
|
34
36
|
});
|
|
35
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
|
+
|
|
36
46
|
// Cleanup
|
|
37
47
|
unsub();
|
|
38
48
|
```
|
|
@@ -62,20 +72,17 @@ npm --prefix node_modules/@everystate/core run self-test
|
|
|
62
72
|
### Integration tests (@everystate/test)
|
|
63
73
|
|
|
64
74
|
The `tests/` folder contains a separate integration suite that uses
|
|
65
|
-
`@everystate/test` (
|
|
66
|
-
the **self-test** stays lightweight, while integration tests
|
|
67
|
-
for deeper validation.
|
|
75
|
+
`@everystate/test` (declared as `devDependency`). This is an intentional
|
|
76
|
+
tradeoff: the **self-test** stays lightweight, while integration tests
|
|
77
|
+
remain available for deeper validation.
|
|
68
78
|
|
|
69
|
-
|
|
79
|
+
**For end users** (after installing the package):
|
|
70
80
|
|
|
71
81
|
```bash
|
|
82
|
+
# Install test dependency
|
|
72
83
|
npm install @everystate/test
|
|
73
|
-
node node_modules/@everystate/core/tests/core.test.js
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Short form (from the package folder):
|
|
77
84
|
|
|
78
|
-
|
|
85
|
+
# Run from package folder
|
|
79
86
|
cd node_modules/@everystate/core
|
|
80
87
|
npm run test:integration
|
|
81
88
|
# or short alias
|
|
@@ -90,6 +97,16 @@ npm --prefix node_modules/@everystate/core run test:integration
|
|
|
90
97
|
npm --prefix node_modules/@everystate/core run test:i
|
|
91
98
|
```
|
|
92
99
|
|
|
100
|
+
**For package developers** (working in the source repo):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Install dev dependencies first
|
|
104
|
+
npm install
|
|
105
|
+
|
|
106
|
+
# Run integration tests
|
|
107
|
+
npm run test:integration
|
|
108
|
+
```
|
|
109
|
+
|
|
93
110
|
## What is EveryState?
|
|
94
111
|
|
|
95
112
|
EveryState is a reactive state management library where:
|
|
@@ -103,6 +120,7 @@ EveryState is a reactive state management library where:
|
|
|
103
120
|
- **Path-based subscriptions**: Subscribe to exactly what you need
|
|
104
121
|
- **Wildcard support**: `user.*` catches all user changes
|
|
105
122
|
- **Atomic batching**: Multiple writes, single notification per path
|
|
123
|
+
- **Path introspection**: `has()` and `keys()` for runtime path discovery
|
|
106
124
|
- **Zero dependencies**: ~2KB minified
|
|
107
125
|
- **Framework-agnostic**: Works with React, Vue, Angular, Svelte, or vanilla JS
|
|
108
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.
|
|
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",
|
|
@@ -45,5 +45,8 @@
|
|
|
45
45
|
"*.html",
|
|
46
46
|
"cli.js",
|
|
47
47
|
"tests/"
|
|
48
|
-
]
|
|
48
|
+
],
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@everystate/test": "^1.0.0"
|
|
51
|
+
}
|
|
49
52
|
}
|
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.
|
|
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);
|