@everystate/core 1.0.0 → 1.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Imsirovic Ajdin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -13,9 +13,9 @@ npm install @everystate/core
13
13
  ## Quick Start
14
14
 
15
15
  ```js
16
- import { createEventState } from '@everystate/core';
16
+ import { createEveryState } from '@everystate/core';
17
17
 
18
- const store = createEventState({ count: 0, user: { name: 'Alice' } });
18
+ const store = createEveryState({ count: 0, user: { name: 'Alice' } });
19
19
 
20
20
  // Subscribe to specific path
21
21
  const unsub = store.subscribe('count', (value) => {
@@ -65,13 +65,26 @@ EveryState makes state **addressable, observable, and testable** without special
65
65
  ## Ecosystem
66
66
 
67
67
  - `@everystate/core`: Core state engine (you are here)
68
- - `@everystate/view`: DOM-as-state with surgical updates
69
- - `@everystate/perf`: Performance monitoring overlay
70
68
  - `@everystate/css`: Reactive styling and design tokens
71
- - `@everystate/router`: SPA routing as state
69
+ - `@everystate/perf`: Performance monitoring overlay
72
70
  - `@everystate/react`: React hooks adapter
73
- - `@everystate/renderer`: Direct-binding reactive renderer
74
- - `@everystate/event-test`: Zero-dependency testing
71
+ - `@everystate/router`: SPA routing as state
72
+ - `@everystate/test`: Zero-dependency testing
73
+ - `@everystate/view`: DOM-as-state with surgical updates
74
+
75
+
76
+ | Package | Description | License |
77
+ |---|---|---|
78
+ | [@everystate/aliases](https://www.npmjs.com/package/@everystate/aliases) | Ergonomic single-character and short-name DOM aliases for vanilla JS | MIT |
79
+ | [@everystate/core](https://www.npmjs.com/package/@everystate/core) | Path-based state management with wildcard subscriptions and async support. Core state engine (you are here). | MIT |
80
+ | [@everystate/css](https://www.npmjs.com/package/@everystate/css) | Reactive CSSOM engine: design tokens, typed validation, WCAG enforcement, all via path-based state | MIT |
81
+ | [@everystate/examples](https://www.npmjs.com/package/@everystate/examples) | Example applications and patterns | MIT |
82
+ | [@everystate/perf](https://www.npmjs.com/package/@everystate/perf) | Performance monitoring overlay | MIT |
83
+ | [@everystate/react](https://www.npmjs.com/package/@everystate/react) | React hooks adapter: `usePath`, `useIntent`, `useAsync` hooks and `EventStateProvider` | MIT |
84
+ | [@everystate/renderer](https://www.npmjs.com/package/@everystate/renderer) | Direct-binding reactive renderer: `bind-*`, `set`, `each` attributes. Zero build step | Proprietary |
85
+ | [@everystate/router](https://www.npmjs.com/package/@everystate/router) | SPA routing as state | MIT |
86
+ | [@everystate/test](https://www.npmjs.com/package/@everystate/test) | Event-sequence testing for UIstate stores. Zero dependency. | Proprietary |
87
+ | [@everystate/view](https://www.npmjs.com/package/@everystate/view) | State-driven view: DOMless resolve + surgical DOM projector. View tree as first-class state | MIT |
75
88
 
76
89
  ## Documentation
77
90
 
package/everyState.js ADDED
@@ -0,0 +1,304 @@
1
+ /**
2
+ * EveryState v1.0.0 - Optimized Path-Based State Management
3
+ *
4
+ * A lightweight, performant state management library using path-based subscriptions.
5
+ * Optimized for selective notifications and granular updates.
6
+ *
7
+ * Features:
8
+ * - Path-based get/set operations (e.g., 'user.profile.name')
9
+ * - Selective subscriptions (only relevant subscribers fire)
10
+ * - Wildcard subscriptions (e.g., 'user.*' catches all user changes)
11
+ * - Global subscriptions (e.g., '*' catches all changes)
12
+ * - Atomic batching (batch/setMany — subscribers fire after all writes)
13
+ * - Zero dependencies
14
+ * - ~2KB minified
15
+ *
16
+ * Performance characteristics:
17
+ * - 2-9x faster than Zustand for selective subscriptions
18
+ * - Competitive overall performance
19
+ * - Minimal rendering overhead (1.27x faster paint times)
20
+ *
21
+ * @example
22
+ * const store = createEveryState({ count: 0, user: { name: 'Alice' } });
23
+ *
24
+ * // Subscribe to specific path
25
+ * const unsub = store.subscribe('count', (value) => {
26
+ * console.log('Count changed:', value);
27
+ * });
28
+ *
29
+ * // Update state
30
+ * store.set('count', 1);
31
+ *
32
+ * // Get state
33
+ * const count = store.get('count');
34
+ *
35
+ * // Wildcard subscription
36
+ * store.subscribe('user.*', ({ path, value }) => {
37
+ * console.log(`User field ${path} changed to:`, value);
38
+ * });
39
+ *
40
+ * // Global subscription
41
+ * store.subscribe('*', ({ path, value }) => {
42
+ * console.log(`State changed at ${path}:`, value);
43
+ * });
44
+ *
45
+ * // Batch multiple writes (subscribers fire once per path, after batch)
46
+ * store.batch(() => {
47
+ * store.set('user.name', 'Charlie');
48
+ * store.set('user.email', 'charlie@example.com');
49
+ * });
50
+ *
51
+ * // Or use setMany for the same effect
52
+ * store.setMany({ 'user.name': 'Charlie', 'user.email': 'charlie@example.com' });
53
+ *
54
+ * // Cleanup
55
+ * unsub();
56
+ * store.destroy();
57
+ */
58
+
59
+ export function createEveryState(initial = {}) {
60
+ const state = JSON.parse(JSON.stringify(initial));
61
+ const listeners = new Map();
62
+ const asyncOps = new Map();
63
+ let destroyed = false;
64
+
65
+ // Batching: buffer writes and flush once at the end
66
+ let batching = false;
67
+ const batchBuffer = new Map();
68
+
69
+ function writeAndNotify(path, value) {
70
+ const parts = path.split(".");
71
+ const key = parts.pop();
72
+ let cur = state;
73
+
74
+ for (const p of parts) {
75
+ if (!cur[p]) cur[p] = {};
76
+ cur = cur[p];
77
+ }
78
+
79
+ const oldValue = cur[key];
80
+ cur[key] = value;
81
+
82
+ // Fast-path: skip all listener dispatch when nobody is subscribed.
83
+ // This preserves observer-before-observable: the moment any subscriber
84
+ // registers, listeners.size > 0 and the full dispatch runs.
85
+ if (destroyed || listeners.size === 0) return value;
86
+
87
+ // Lazy detail allocation: only build the object when a listener is
88
+ // actually present. No closure is created — the inline `||` re-uses
89
+ // the same object across exact / wildcard / global dispatch.
90
+ let detail = null;
91
+
92
+ const exactListeners = listeners.get(path);
93
+ if (exactListeners && exactListeners.size > 0) {
94
+ detail = { path, value, oldValue };
95
+ exactListeners.forEach(cb => cb(value, detail));
96
+ }
97
+
98
+ if (parts.length) {
99
+ let parent = "";
100
+ for (const p of parts) {
101
+ parent = parent ? `${parent}.${p}` : p;
102
+ const wildcardListeners = listeners.get(`${parent}.*`);
103
+ if (wildcardListeners && wildcardListeners.size > 0) {
104
+ detail = detail || { path, value, oldValue };
105
+ wildcardListeners.forEach(cb => cb(detail));
106
+ }
107
+ }
108
+ }
109
+
110
+ const globalListeners = listeners.get('*');
111
+ if (globalListeners && globalListeners.size > 0) {
112
+ detail = detail || { path, value, oldValue };
113
+ globalListeners.forEach(cb => cb(detail));
114
+ }
115
+
116
+ return value;
117
+ }
118
+
119
+ function flushBatch() {
120
+ const entries = Array.from(batchBuffer.entries());
121
+ batchBuffer.clear();
122
+ for (const [p, v] of entries) {
123
+ writeAndNotify(p, v);
124
+ }
125
+ }
126
+
127
+ return {
128
+ /**
129
+ * Get value at path
130
+ * @param {string} path - Dot-separated path (e.g., 'user.profile.name')
131
+ * @returns {*} Value at path, or entire state if no path provided
132
+ */
133
+ get(path) {
134
+ if (destroyed) throw new Error('Cannot get from destroyed store');
135
+ if (!path) return state;
136
+ const parts = path.split('.');
137
+ let cur = state;
138
+ for (const p of parts) {
139
+ if (cur == null) return undefined;
140
+ cur = cur[p];
141
+ }
142
+ return cur;
143
+ },
144
+
145
+ /**
146
+ * Set value at path and notify subscribers
147
+ * @param {string} path - Dot-separated path (e.g., 'user.profile.name')
148
+ * @param {*} value - New value
149
+ * @returns {*} The value that was set
150
+ */
151
+ set(path, value) {
152
+ if (destroyed) throw new Error('Cannot set on destroyed store');
153
+ if (!path) return value;
154
+
155
+ if (batching) {
156
+ batchBuffer.set(path, value);
157
+ return value;
158
+ }
159
+
160
+ return writeAndNotify(path, value);
161
+ },
162
+
163
+ async setAsync(path, fetcher) {
164
+ if (destroyed) throw new Error('Cannot setAsync on destroyed store');
165
+ if (!path) throw new TypeError('setAsync requires a path');
166
+ if (typeof fetcher !== 'function') {
167
+ throw new TypeError('setAsync(path, fetcher) requires a function fetcher');
168
+ }
169
+
170
+ if (asyncOps.has(path)) {
171
+ asyncOps.get(path).controller.abort();
172
+ }
173
+
174
+ const controller = new AbortController();
175
+ asyncOps.set(path, { controller });
176
+
177
+ try {
178
+ this.batch(() => {
179
+ this.set(`${path}.status`, 'loading');
180
+ this.set(`${path}.error`, null);
181
+ });
182
+
183
+ const data = await fetcher(controller.signal);
184
+
185
+ if (destroyed) throw new Error('Cannot setAsync on destroyed store');
186
+
187
+ this.batch(() => {
188
+ this.set(`${path}.data`, data);
189
+ this.set(`${path}.status`, 'success');
190
+ });
191
+ return data;
192
+ } catch (err) {
193
+ if (err?.name === 'AbortError') {
194
+ this.set(`${path}.status`, 'cancelled');
195
+ const cancelErr = new Error('Request cancelled');
196
+ cancelErr.name = 'AbortError';
197
+ throw cancelErr;
198
+ }
199
+
200
+ this.batch(() => {
201
+ this.set(`${path}.status`, 'error');
202
+ this.set(`${path}.error`, err?.message ?? String(err));
203
+ });
204
+ throw err;
205
+ } finally {
206
+ const op = asyncOps.get(path);
207
+ if (op?.controller === controller) {
208
+ asyncOps.delete(path);
209
+ }
210
+ }
211
+ },
212
+
213
+ cancel(path) {
214
+ if (destroyed) throw new Error('Cannot cancel on destroyed store');
215
+ if (!path) throw new TypeError('cancel requires a path');
216
+
217
+ if (asyncOps.has(path)) {
218
+ asyncOps.get(path).controller.abort();
219
+ asyncOps.delete(path);
220
+ this.set(`${path}.status`, 'cancelled');
221
+ }
222
+ },
223
+
224
+ /**
225
+ * Batch multiple set() calls. Subscribers fire once per unique path
226
+ * after the batch completes, not during. Supports nesting.
227
+ * @param {Function} fn - Function containing set() calls to batch
228
+ */
229
+ batch(fn) {
230
+ if (destroyed) throw new Error('Cannot batch on destroyed store');
231
+ if (typeof fn !== 'function') throw new TypeError('batch requires a function');
232
+ const wasBatching = batching;
233
+ batching = true;
234
+ try {
235
+ fn();
236
+ } finally {
237
+ batching = wasBatching;
238
+ if (!batching) flushBatch();
239
+ }
240
+ },
241
+
242
+ /**
243
+ * Set multiple paths atomically. Equivalent to batch(() => { set(a); set(b); ... }).
244
+ * Accepts a plain object, an array of [path, value] pairs, or a Map.
245
+ * @param {Object|Array|Map} entries - Paths and values to set
246
+ */
247
+ setMany(entries) {
248
+ if (destroyed) throw new Error('Cannot setMany on destroyed store');
249
+ if (!entries) return;
250
+ this.batch(() => {
251
+ if (Array.isArray(entries)) {
252
+ for (const [p, v] of entries) this.set(p, v);
253
+ } else if (entries instanceof Map) {
254
+ for (const [p, v] of entries.entries()) this.set(p, v);
255
+ } else if (typeof entries === 'object') {
256
+ for (const p of Object.keys(entries)) this.set(p, entries[p]);
257
+ }
258
+ });
259
+ },
260
+
261
+ /**
262
+ * Subscribe to changes at path
263
+ * @param {string} path - Path to subscribe to (supports wildcards: 'user.*', '*')
264
+ * @param {Function} handler - Callback function.
265
+ * - Exact path subscriptions: (value, meta) => void
266
+ * - Wildcard/global subscriptions: (meta) => void
267
+ * @returns {Function} Unsubscribe function
268
+ */
269
+ subscribe(path, handler) {
270
+ if (destroyed) throw new Error('Cannot subscribe to destroyed store');
271
+ if (!path || typeof handler !== 'function') {
272
+ throw new TypeError('subscribe requires path and handler');
273
+ }
274
+
275
+ if (!listeners.has(path)) {
276
+ listeners.set(path, new Set());
277
+ }
278
+ listeners.get(path).add(handler);
279
+
280
+ return () => {
281
+ const set = listeners.get(path);
282
+ if (set) {
283
+ set.delete(handler);
284
+ if (set.size === 0) listeners.delete(path);
285
+ }
286
+ };
287
+ },
288
+
289
+ /**
290
+ * Destroy store and clear all subscriptions
291
+ */
292
+ destroy() {
293
+ if (!destroyed) {
294
+ destroyed = true;
295
+ batchBuffer.clear();
296
+ asyncOps.forEach(({ controller }) => controller.abort());
297
+ asyncOps.clear();
298
+ listeners.clear();
299
+ }
300
+ }
301
+ };
302
+ }
303
+
304
+ export default createEveryState;
package/index.js CHANGED
@@ -1,9 +1,6 @@
1
1
  /**
2
2
  * @everystate/core
3
- *
4
- * EveryState wrapper for @uistate/core
5
- * Re-exports all functionality from the underlying @uistate/core package
6
3
  */
7
4
 
8
- export { createEventState } from '@uistate/core';
9
- export * from '@uistate/core';
5
+ export { createEveryState } from './everyState.js';
6
+ export { createQueryClient } from './queryClient.js';
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@everystate/core",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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",
7
+ "types": "index.d.ts",
8
+ "scripts": {
9
+ "test": "node self-test.js"
10
+ },
7
11
  "keywords": [
8
12
  "everystate",
9
13
  "state-management",
@@ -30,9 +34,8 @@
30
34
  "homepage": "https://github.com/ImsirovicAjdin/everystate-core#readme",
31
35
  "files": [
32
36
  "index.js",
37
+ "everyState.js",
38
+ "queryClient.js",
33
39
  "README.md"
34
- ],
35
- "dependencies": {
36
- "@uistate/core": "^5.6.0"
37
- }
40
+ ]
38
41
  }
package/queryClient.js ADDED
@@ -0,0 +1,54 @@
1
+ // Mini QueryClient wrapper for EveryState
2
+ // Thin async layer with standard query patterns
3
+
4
+ export const createQueryClient = (store) => {
5
+ return {
6
+ // Run a query with async state handling
7
+ async query(key, fetcher) {
8
+ return await store.setAsync(`query.${key}`, fetcher);
9
+ },
10
+
11
+ // Subscribe to query data
12
+ subscribe(key, cb) {
13
+ return store.subscribe(`query.${key}.data`, cb);
14
+ },
15
+
16
+ // Subscribe to query status
17
+ subscribeToStatus(key, cb) {
18
+ return store.subscribe(`query.${key}.status`, cb);
19
+ },
20
+
21
+ // Subscribe to query errors
22
+ subscribeToError(key, cb) {
23
+ return store.subscribe(`query.${key}.error`, cb);
24
+ },
25
+
26
+ // Read current query data
27
+ getData(key) {
28
+ return store.get(`query.${key}.data`);
29
+ },
30
+
31
+ // Read current query status
32
+ getStatus(key) {
33
+ return store.get(`query.${key}.status`);
34
+ },
35
+
36
+ // Read current query error
37
+ getError(key) {
38
+ return store.get(`query.${key}.error`);
39
+ },
40
+
41
+ // Cancel active query
42
+ cancel(key) {
43
+ store.cancel(`query.${key}`);
44
+ },
45
+
46
+ // Reset query to idle
47
+ invalidate(key) {
48
+ const p = `query.${key}`;
49
+ store.set(`${p}.data`, null);
50
+ store.set(`${p}.status`, "idle");
51
+ store.set(`${p}.error`, null);
52
+ },
53
+ };
54
+ };