@uistate/core 5.0.3 → 5.1.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/eventStateNew.js +212 -0
- package/eventTest.js +1 -1
- package/examples/028-counter-improved-eventTest/app/store.js +1 -1
- package/examples/028-counter-improved-eventTest/runtime/core/eventStateNew.js +149 -0
- package/examples/028-counter-improved-eventTest/tests/eventTest.js +1 -1
- package/examples/030-todo-app-with-eventTest/app/store.js +1 -1
- package/examples/030-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
- package/examples/030-todo-app-with-eventTest/tests/eventTest.js +1 -1
- package/examples/031-todo-app-with-eventTest/app/store.js +1 -1
- package/examples/031-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
- package/examples/031-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +1 -1
- package/examples/031-todo-app-with-eventTest/tests/eventTest.js +1 -1
- package/examples/032-todo-app-with-eventTest/app/store.js +1 -1
- package/examples/032-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
- package/examples/032-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +1 -1
- package/examples/032-todo-app-with-eventTest/tests/eventTest.js +1 -1
- package/index.js +2 -2
- package/package.json +3 -1
package/eventStateNew.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventState v2 - 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
|
+
* - Zero dependencies
|
|
13
|
+
* - ~2KB minified
|
|
14
|
+
*
|
|
15
|
+
* Performance characteristics:
|
|
16
|
+
* - 2-9x faster than Zustand for selective subscriptions
|
|
17
|
+
* - Competitive overall performance
|
|
18
|
+
* - Minimal rendering overhead (1.27x faster paint times)
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const store = createEventState({ count: 0, user: { name: 'Alice' } });
|
|
22
|
+
*
|
|
23
|
+
* // Subscribe to specific path
|
|
24
|
+
* const unsub = store.subscribe('count', (value) => {
|
|
25
|
+
* console.log('Count changed:', value);
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // Update state
|
|
29
|
+
* store.set('count', 1);
|
|
30
|
+
*
|
|
31
|
+
* // Get state
|
|
32
|
+
* const count = store.get('count');
|
|
33
|
+
*
|
|
34
|
+
* // Wildcard subscription
|
|
35
|
+
* store.subscribe('user.*', ({ path, value }) => {
|
|
36
|
+
* console.log(`User field ${path} changed to:`, value);
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* // Global subscription
|
|
40
|
+
* store.subscribe('*', ({ path, value }) => {
|
|
41
|
+
* console.log(`State changed at ${path}:`, value);
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* // Cleanup
|
|
45
|
+
* unsub();
|
|
46
|
+
* store.destroy();
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
export function createEventState(initial = {}) {
|
|
50
|
+
const state = JSON.parse(JSON.stringify(initial));
|
|
51
|
+
const listeners = new Map();
|
|
52
|
+
const asyncOps = new Map();
|
|
53
|
+
let destroyed = false;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
/**
|
|
57
|
+
* Get value at path
|
|
58
|
+
* @param {string} path - Dot-separated path (e.g., 'user.profile.name')
|
|
59
|
+
* @returns {*} Value at path, or entire state if no path provided
|
|
60
|
+
*/
|
|
61
|
+
get(path) {
|
|
62
|
+
if (destroyed) throw new Error('Cannot get from destroyed store');
|
|
63
|
+
if (!path) return state;
|
|
64
|
+
return path.split(".").reduce((obj, key) => obj?.[key], state);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Set value at path and notify subscribers
|
|
69
|
+
* @param {string} path - Dot-separated path (e.g., 'user.profile.name')
|
|
70
|
+
* @param {*} value - New value
|
|
71
|
+
* @returns {*} The value that was set
|
|
72
|
+
*/
|
|
73
|
+
set(path, value) {
|
|
74
|
+
if (destroyed) throw new Error('Cannot set on destroyed store');
|
|
75
|
+
if (!path) return value;
|
|
76
|
+
|
|
77
|
+
const parts = path.split(".");
|
|
78
|
+
const key = parts.pop();
|
|
79
|
+
let cur = state;
|
|
80
|
+
|
|
81
|
+
// Navigate to parent object, creating nested objects as needed
|
|
82
|
+
for (const p of parts) {
|
|
83
|
+
if (!cur[p]) cur[p] = {};
|
|
84
|
+
cur = cur[p];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const oldValue = cur[key];
|
|
88
|
+
cur[key] = value;
|
|
89
|
+
|
|
90
|
+
if (!destroyed) {
|
|
91
|
+
const detail = { path, value, oldValue };
|
|
92
|
+
|
|
93
|
+
// Notify exact path subscribers
|
|
94
|
+
const exactListeners = listeners.get(path);
|
|
95
|
+
if (exactListeners) {
|
|
96
|
+
exactListeners.forEach(cb => cb(value, detail));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Notify wildcard subscribers for parent paths
|
|
100
|
+
if (parts.length) {
|
|
101
|
+
let parent = "";
|
|
102
|
+
for (const p of parts) {
|
|
103
|
+
parent = parent ? `${parent}.${p}` : p;
|
|
104
|
+
const wildcardListeners = listeners.get(`${parent}.*`);
|
|
105
|
+
if (wildcardListeners) {
|
|
106
|
+
wildcardListeners.forEach(cb => cb(detail));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Notify global subscribers
|
|
112
|
+
const globalListeners = listeners.get('*');
|
|
113
|
+
if (globalListeners) {
|
|
114
|
+
globalListeners.forEach(cb => cb(detail));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return value;
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
async setAsync(path, fetcher) {
|
|
122
|
+
if (destroyed) throw new Error('Cannot setAsync on destroyed store');
|
|
123
|
+
if (!path) throw new TypeError('setAsync requires a path');
|
|
124
|
+
if (typeof fetcher !== 'function') {
|
|
125
|
+
throw new TypeError('setAsync(path, fetcher) requires a function fetcher');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (asyncOps.has(path)) {
|
|
129
|
+
asyncOps.get(path).controller.abort();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const controller = new AbortController();
|
|
133
|
+
asyncOps.set(path, { controller });
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
this.set(`${path}.status`, 'loading');
|
|
137
|
+
this.set(`${path}.error`, null);
|
|
138
|
+
|
|
139
|
+
const data = await fetcher(controller.signal);
|
|
140
|
+
|
|
141
|
+
if (destroyed) throw new Error('Cannot setAsync on destroyed store');
|
|
142
|
+
|
|
143
|
+
this.set(path, data);
|
|
144
|
+
this.set(`${path}.status`, 'success');
|
|
145
|
+
return data;
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (err?.name === 'AbortError') {
|
|
148
|
+
this.set(`${path}.status`, 'cancelled');
|
|
149
|
+
const cancelErr = new Error('Request cancelled');
|
|
150
|
+
cancelErr.name = 'AbortError';
|
|
151
|
+
throw cancelErr;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.set(`${path}.status`, 'error');
|
|
155
|
+
this.set(`${path}.error`, err?.message ?? String(err));
|
|
156
|
+
throw err;
|
|
157
|
+
} finally {
|
|
158
|
+
const op = asyncOps.get(path);
|
|
159
|
+
if (op?.controller === controller) {
|
|
160
|
+
asyncOps.delete(path);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
cancel(path) {
|
|
166
|
+
if (destroyed) throw new Error('Cannot cancel on destroyed store');
|
|
167
|
+
if (!path) throw new TypeError('cancel requires a path');
|
|
168
|
+
|
|
169
|
+
if (asyncOps.has(path)) {
|
|
170
|
+
asyncOps.get(path).controller.abort();
|
|
171
|
+
asyncOps.delete(path);
|
|
172
|
+
this.set(`${path}.status`, 'cancelled');
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Subscribe to changes at path
|
|
178
|
+
* @param {string} path - Path to subscribe to (supports wildcards: 'user.*', '*')
|
|
179
|
+
* @param {Function} handler - Callback function.
|
|
180
|
+
* - Exact path subscriptions: (value, meta) => void
|
|
181
|
+
* - Wildcard/global subscriptions: (meta) => void
|
|
182
|
+
* @returns {Function} Unsubscribe function
|
|
183
|
+
*/
|
|
184
|
+
subscribe(path, handler) {
|
|
185
|
+
if (destroyed) throw new Error('Cannot subscribe to destroyed store');
|
|
186
|
+
if (!path || typeof handler !== 'function') {
|
|
187
|
+
throw new TypeError('subscribe requires path and handler');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!listeners.has(path)) {
|
|
191
|
+
listeners.set(path, new Set());
|
|
192
|
+
}
|
|
193
|
+
listeners.get(path).add(handler);
|
|
194
|
+
|
|
195
|
+
return () => listeners.get(path)?.delete(handler);
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Destroy store and clear all subscriptions
|
|
200
|
+
*/
|
|
201
|
+
destroy() {
|
|
202
|
+
if (!destroyed) {
|
|
203
|
+
destroyed = true;
|
|
204
|
+
asyncOps.forEach(({ controller }) => controller.abort());
|
|
205
|
+
asyncOps.clear();
|
|
206
|
+
listeners.clear();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export default createEventState;
|
package/eventTest.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* Provides TDD-style testing with type extraction capabilities
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { createEventState } from './
|
|
27
|
+
import { createEventState } from './eventStateNew.js';
|
|
28
28
|
|
|
29
29
|
export function createEventTest(initialState = {}) {
|
|
30
30
|
const store = createEventState(initialState);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventState v2 - 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
|
+
* - Zero dependencies
|
|
13
|
+
* - ~2KB minified
|
|
14
|
+
*
|
|
15
|
+
* Performance characteristics:
|
|
16
|
+
* - 2-9x faster than Zustand for selective subscriptions
|
|
17
|
+
* - Competitive overall performance
|
|
18
|
+
* - Minimal rendering overhead (1.27x faster paint times)
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const store = createEventState({ count: 0, user: { name: 'Alice' } });
|
|
22
|
+
*
|
|
23
|
+
* // Subscribe to specific path (receives value directly)
|
|
24
|
+
* const unsub = store.subscribe('count', (value) => {
|
|
25
|
+
* console.log('Count changed:', value);
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // Update state
|
|
29
|
+
* store.set('count', 1);
|
|
30
|
+
*
|
|
31
|
+
* // Get state
|
|
32
|
+
* const count = store.get('count');
|
|
33
|
+
*
|
|
34
|
+
* // Wildcard subscription
|
|
35
|
+
* store.subscribe('user.*', ({ path, value }) => {
|
|
36
|
+
* console.log(`User field ${path} changed to:`, value);
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* // Global subscription
|
|
40
|
+
* store.subscribe('*', ({ path, value }) => {
|
|
41
|
+
* console.log(`State changed at ${path}:`, value);
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* // Cleanup
|
|
45
|
+
* unsub();
|
|
46
|
+
* store.destroy();
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
export function createEventState(initial = {}) {
|
|
50
|
+
const state = JSON.parse(JSON.stringify(initial));
|
|
51
|
+
const listeners = new Map();
|
|
52
|
+
let destroyed = false;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
/**
|
|
56
|
+
* Get value at path
|
|
57
|
+
* @param {string} path - Dot-separated path (e.g., 'user.profile.name')
|
|
58
|
+
* @returns {*} Value at path, or entire state if no path provided
|
|
59
|
+
*/
|
|
60
|
+
get(path) {
|
|
61
|
+
if (destroyed) throw new Error('Cannot get from destroyed store');
|
|
62
|
+
if (!path) return state;
|
|
63
|
+
return path.split(".").reduce((obj, key) => obj?.[key], state);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Set value at path and notify subscribers
|
|
68
|
+
* @param {string} path - Dot-separated path (e.g., 'user.profile.name')
|
|
69
|
+
* @param {*} value - New value
|
|
70
|
+
* @returns {*} The value that was set
|
|
71
|
+
*/
|
|
72
|
+
set(path, value) {
|
|
73
|
+
if (destroyed) throw new Error('Cannot set on destroyed store');
|
|
74
|
+
if (!path) return value;
|
|
75
|
+
|
|
76
|
+
const parts = path.split(".");
|
|
77
|
+
const key = parts.pop();
|
|
78
|
+
let cur = state;
|
|
79
|
+
|
|
80
|
+
// Navigate to parent object, creating nested objects as needed
|
|
81
|
+
for (const p of parts) {
|
|
82
|
+
if (!cur[p]) cur[p] = {};
|
|
83
|
+
cur = cur[p];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const oldValue = cur[key];
|
|
87
|
+
cur[key] = value;
|
|
88
|
+
|
|
89
|
+
if (!destroyed) {
|
|
90
|
+
const detail = { path, value, oldValue };
|
|
91
|
+
|
|
92
|
+
// Notify exact path subscribers (pass value directly for backwards compatibility)
|
|
93
|
+
const exactListeners = listeners.get(path);
|
|
94
|
+
if (exactListeners) {
|
|
95
|
+
exactListeners.forEach(cb => cb(value));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Notify wildcard subscribers for all parent paths (pass detail object)
|
|
99
|
+
for (let i = 0; i < parts.length; i++) {
|
|
100
|
+
const parentPath = parts.slice(0, i + 1).join('.');
|
|
101
|
+
const wildcardListeners = listeners.get(`${parentPath}.*`);
|
|
102
|
+
if (wildcardListeners) {
|
|
103
|
+
wildcardListeners.forEach(cb => cb(detail));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Notify global subscribers (pass detail object)
|
|
108
|
+
const globalListeners = listeners.get('*');
|
|
109
|
+
if (globalListeners) {
|
|
110
|
+
globalListeners.forEach(cb => cb(detail));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return value;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Subscribe to changes at path
|
|
119
|
+
* @param {string} path - Path to subscribe to (supports wildcards: 'user.*', '*')
|
|
120
|
+
* @param {Function} handler - Callback function receiving { path, value, oldValue }
|
|
121
|
+
* @returns {Function} Unsubscribe function
|
|
122
|
+
*/
|
|
123
|
+
subscribe(path, handler) {
|
|
124
|
+
if (destroyed) throw new Error('Cannot subscribe to destroyed store');
|
|
125
|
+
if (!path || typeof handler !== 'function') {
|
|
126
|
+
throw new TypeError('subscribe requires path and handler');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!listeners.has(path)) {
|
|
130
|
+
listeners.set(path, new Set());
|
|
131
|
+
}
|
|
132
|
+
listeners.get(path).add(handler);
|
|
133
|
+
|
|
134
|
+
return () => listeners.get(path)?.delete(handler);
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Destroy store and clear all subscriptions
|
|
139
|
+
*/
|
|
140
|
+
destroy() {
|
|
141
|
+
if (!destroyed) {
|
|
142
|
+
destroyed = true;
|
|
143
|
+
listeners.clear();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default createEventState;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Provides TDD-style testing with type extraction capabilities
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { createEventState } from '../runtime/core/
|
|
7
|
+
import { createEventState } from '../runtime/core/eventStateNew.js';
|
|
8
8
|
|
|
9
9
|
export function createEventTest(initialState = {}) {
|
|
10
10
|
const store = createEventState(initialState);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventState v2 - 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
|
+
* - Zero dependencies
|
|
13
|
+
* - ~2KB minified
|
|
14
|
+
*
|
|
15
|
+
* Performance characteristics:
|
|
16
|
+
* - 2-9x faster than Zustand for selective subscriptions
|
|
17
|
+
* - Competitive overall performance
|
|
18
|
+
* - Minimal rendering overhead (1.27x faster paint times)
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const store = createEventState({ count: 0, user: { name: 'Alice' } });
|
|
22
|
+
*
|
|
23
|
+
* // Subscribe to specific path (receives value directly)
|
|
24
|
+
* const unsub = store.subscribe('count', (value) => {
|
|
25
|
+
* console.log('Count changed:', value);
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // Update state
|
|
29
|
+
* store.set('count', 1);
|
|
30
|
+
*
|
|
31
|
+
* // Get state
|
|
32
|
+
* const count = store.get('count');
|
|
33
|
+
*
|
|
34
|
+
* // Wildcard subscription
|
|
35
|
+
* store.subscribe('user.*', ({ path, value }) => {
|
|
36
|
+
* console.log(`User field ${path} changed to:`, value);
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* // Global subscription
|
|
40
|
+
* store.subscribe('*', ({ path, value }) => {
|
|
41
|
+
* console.log(`State changed at ${path}:`, value);
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* // Cleanup
|
|
45
|
+
* unsub();
|
|
46
|
+
* store.destroy();
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
export function createEventState(initial = {}) {
|
|
50
|
+
const state = JSON.parse(JSON.stringify(initial));
|
|
51
|
+
const listeners = new Map();
|
|
52
|
+
let destroyed = false;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
/**
|
|
56
|
+
* Get value at path
|
|
57
|
+
* @param {string} path - Dot-separated path (e.g., 'user.profile.name')
|
|
58
|
+
* @returns {*} Value at path, or entire state if no path provided
|
|
59
|
+
*/
|
|
60
|
+
get(path) {
|
|
61
|
+
if (destroyed) throw new Error('Cannot get from destroyed store');
|
|
62
|
+
if (!path) return state;
|
|
63
|
+
return path.split(".").reduce((obj, key) => obj?.[key], state);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Set value at path and notify subscribers
|
|
68
|
+
* @param {string} path - Dot-separated path (e.g., 'user.profile.name')
|
|
69
|
+
* @param {*} value - New value
|
|
70
|
+
* @returns {*} The value that was set
|
|
71
|
+
*/
|
|
72
|
+
set(path, value) {
|
|
73
|
+
if (destroyed) throw new Error('Cannot set on destroyed store');
|
|
74
|
+
if (!path) return value;
|
|
75
|
+
|
|
76
|
+
const parts = path.split(".");
|
|
77
|
+
const key = parts.pop();
|
|
78
|
+
let cur = state;
|
|
79
|
+
|
|
80
|
+
// Navigate to parent object, creating nested objects as needed
|
|
81
|
+
for (const p of parts) {
|
|
82
|
+
if (!cur[p]) cur[p] = {};
|
|
83
|
+
cur = cur[p];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const oldValue = cur[key];
|
|
87
|
+
cur[key] = value;
|
|
88
|
+
|
|
89
|
+
if (!destroyed) {
|
|
90
|
+
const detail = { path, value, oldValue };
|
|
91
|
+
|
|
92
|
+
// Notify exact path subscribers (pass value directly for backwards compatibility)
|
|
93
|
+
const exactListeners = listeners.get(path);
|
|
94
|
+
if (exactListeners) {
|
|
95
|
+
exactListeners.forEach(cb => cb(value));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Notify wildcard subscribers for all parent paths (pass detail object)
|
|
99
|
+
for (let i = 0; i < parts.length; i++) {
|
|
100
|
+
const parentPath = parts.slice(0, i + 1).join('.');
|
|
101
|
+
const wildcardListeners = listeners.get(`${parentPath}.*`);
|
|
102
|
+
if (wildcardListeners) {
|
|
103
|
+
wildcardListeners.forEach(cb => cb(detail));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Notify global subscribers (pass detail object)
|
|
108
|
+
const globalListeners = listeners.get('*');
|
|
109
|
+
if (globalListeners) {
|
|
110
|
+
globalListeners.forEach(cb => cb(detail));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return value;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Subscribe to changes at path
|
|
119
|
+
* @param {string} path - Path to subscribe to (supports wildcards: 'user.*', '*')
|
|
120
|
+
* @param {Function} handler - Callback function receiving { path, value, oldValue }
|
|
121
|
+
* @returns {Function} Unsubscribe function
|
|
122
|
+
*/
|
|
123
|
+
subscribe(path, handler) {
|
|
124
|
+
if (destroyed) throw new Error('Cannot subscribe to destroyed store');
|
|
125
|
+
if (!path || typeof handler !== 'function') {
|
|
126
|
+
throw new TypeError('subscribe requires path and handler');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!listeners.has(path)) {
|
|
130
|
+
listeners.set(path, new Set());
|
|
131
|
+
}
|
|
132
|
+
listeners.get(path).add(handler);
|
|
133
|
+
|
|
134
|
+
return () => listeners.get(path)?.delete(handler);
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Destroy store and clear all subscriptions
|
|
139
|
+
*/
|
|
140
|
+
destroy() {
|
|
141
|
+
if (!destroyed) {
|
|
142
|
+
destroyed = true;
|
|
143
|
+
listeners.clear();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default createEventState;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Provides TDD-style testing with type extraction capabilities
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { createEventState } from '../runtime/core/
|
|
7
|
+
import { createEventState } from '../runtime/core/eventStateNew.js';
|
|
8
8
|
|
|
9
9
|
export function createEventTest(initialState = {}) {
|
|
10
10
|
const store = createEventState(initialState);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventState v2 - 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
|
+
* - Zero dependencies
|
|
13
|
+
* - ~2KB minified
|
|
14
|
+
*
|
|
15
|
+
* Performance characteristics:
|
|
16
|
+
* - 2-9x faster than Zustand for selective subscriptions
|
|
17
|
+
* - Competitive overall performance
|
|
18
|
+
* - Minimal rendering overhead (1.27x faster paint times)
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const store = createEventState({ count: 0, user: { name: 'Alice' } });
|
|
22
|
+
*
|
|
23
|
+
* // Subscribe to specific path (receives value directly)
|
|
24
|
+
* const unsub = store.subscribe('count', (value) => {
|
|
25
|
+
* console.log('Count changed:', value);
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // Update state
|
|
29
|
+
* store.set('count', 1);
|
|
30
|
+
*
|
|
31
|
+
* // Get state
|
|
32
|
+
* const count = store.get('count');
|
|
33
|
+
*
|
|
34
|
+
* // Wildcard subscription
|
|
35
|
+
* store.subscribe('user.*', ({ path, value }) => {
|
|
36
|
+
* console.log(`User field ${path} changed to:`, value);
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* // Global subscription
|
|
40
|
+
* store.subscribe('*', ({ path, value }) => {
|
|
41
|
+
* console.log(`State changed at ${path}:`, value);
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* // Cleanup
|
|
45
|
+
* unsub();
|
|
46
|
+
* store.destroy();
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
export function createEventState(initial = {}) {
|
|
50
|
+
const state = JSON.parse(JSON.stringify(initial));
|
|
51
|
+
const listeners = new Map();
|
|
52
|
+
let destroyed = false;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
/**
|
|
56
|
+
* Get value at path
|
|
57
|
+
* @param {string} path - Dot-separated path (e.g., 'user.profile.name')
|
|
58
|
+
* @returns {*} Value at path, or entire state if no path provided
|
|
59
|
+
*/
|
|
60
|
+
get(path) {
|
|
61
|
+
if (destroyed) throw new Error('Cannot get from destroyed store');
|
|
62
|
+
if (!path) return state;
|
|
63
|
+
return path.split(".").reduce((obj, key) => obj?.[key], state);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Set value at path and notify subscribers
|
|
68
|
+
* @param {string} path - Dot-separated path (e.g., 'user.profile.name')
|
|
69
|
+
* @param {*} value - New value
|
|
70
|
+
* @returns {*} The value that was set
|
|
71
|
+
*/
|
|
72
|
+
set(path, value) {
|
|
73
|
+
if (destroyed) throw new Error('Cannot set on destroyed store');
|
|
74
|
+
if (!path) return value;
|
|
75
|
+
|
|
76
|
+
const parts = path.split(".");
|
|
77
|
+
const key = parts.pop();
|
|
78
|
+
let cur = state;
|
|
79
|
+
|
|
80
|
+
// Navigate to parent object, creating nested objects as needed
|
|
81
|
+
for (const p of parts) {
|
|
82
|
+
if (!cur[p]) cur[p] = {};
|
|
83
|
+
cur = cur[p];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const oldValue = cur[key];
|
|
87
|
+
cur[key] = value;
|
|
88
|
+
|
|
89
|
+
if (!destroyed) {
|
|
90
|
+
const detail = { path, value, oldValue };
|
|
91
|
+
|
|
92
|
+
// Notify exact path subscribers (pass value directly for backwards compatibility)
|
|
93
|
+
const exactListeners = listeners.get(path);
|
|
94
|
+
if (exactListeners) {
|
|
95
|
+
exactListeners.forEach(cb => cb(value));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Notify wildcard subscribers for all parent paths (pass detail object)
|
|
99
|
+
for (let i = 0; i < parts.length; i++) {
|
|
100
|
+
const parentPath = parts.slice(0, i + 1).join('.');
|
|
101
|
+
const wildcardListeners = listeners.get(`${parentPath}.*`);
|
|
102
|
+
if (wildcardListeners) {
|
|
103
|
+
wildcardListeners.forEach(cb => cb(detail));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Notify global subscribers (pass detail object)
|
|
108
|
+
const globalListeners = listeners.get('*');
|
|
109
|
+
if (globalListeners) {
|
|
110
|
+
globalListeners.forEach(cb => cb(detail));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return value;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Subscribe to changes at path
|
|
119
|
+
* @param {string} path - Path to subscribe to (supports wildcards: 'user.*', '*')
|
|
120
|
+
* @param {Function} handler - Callback function receiving { path, value, oldValue }
|
|
121
|
+
* @returns {Function} Unsubscribe function
|
|
122
|
+
*/
|
|
123
|
+
subscribe(path, handler) {
|
|
124
|
+
if (destroyed) throw new Error('Cannot subscribe to destroyed store');
|
|
125
|
+
if (!path || typeof handler !== 'function') {
|
|
126
|
+
throw new TypeError('subscribe requires path and handler');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!listeners.has(path)) {
|
|
130
|
+
listeners.set(path, new Set());
|
|
131
|
+
}
|
|
132
|
+
listeners.get(path).add(handler);
|
|
133
|
+
|
|
134
|
+
return () => listeners.get(path)?.delete(handler);
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Destroy store and clear all subscriptions
|
|
139
|
+
*/
|
|
140
|
+
destroy() {
|
|
141
|
+
if (!destroyed) {
|
|
142
|
+
destroyed = true;
|
|
143
|
+
listeners.clear();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default createEventState;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// NOTE: This module composes the existing './eventState.js' implementation and returns
|
|
4
4
|
// an enhanced facade. The original fine-grained semantics (per-path events) remain intact.
|
|
5
5
|
|
|
6
|
-
import createEventStateBase from '../core/
|
|
6
|
+
import createEventStateBase from '../core/eventStateNew.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Create an enhanced EventState store while preserving the original semantics.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Provides TDD-style testing with type extraction capabilities
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { createEventState } from '../runtime/core/
|
|
7
|
+
import { createEventState } from '../runtime/core/eventStateNew.js';
|
|
8
8
|
|
|
9
9
|
export function createEventTest(initialState = {}) {
|
|
10
10
|
const store = createEventState(initialState);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventState v2 - 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
|
+
* - Zero dependencies
|
|
13
|
+
* - ~2KB minified
|
|
14
|
+
*
|
|
15
|
+
* Performance characteristics:
|
|
16
|
+
* - 2-9x faster than Zustand for selective subscriptions
|
|
17
|
+
* - Competitive overall performance
|
|
18
|
+
* - Minimal rendering overhead (1.27x faster paint times)
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const store = createEventState({ count: 0, user: { name: 'Alice' } });
|
|
22
|
+
*
|
|
23
|
+
* // Subscribe to specific path (receives value directly)
|
|
24
|
+
* const unsub = store.subscribe('count', (value) => {
|
|
25
|
+
* console.log('Count changed:', value);
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // Update state
|
|
29
|
+
* store.set('count', 1);
|
|
30
|
+
*
|
|
31
|
+
* // Get state
|
|
32
|
+
* const count = store.get('count');
|
|
33
|
+
*
|
|
34
|
+
* // Wildcard subscription
|
|
35
|
+
* store.subscribe('user.*', ({ path, value }) => {
|
|
36
|
+
* console.log(`User field ${path} changed to:`, value);
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* // Global subscription
|
|
40
|
+
* store.subscribe('*', ({ path, value }) => {
|
|
41
|
+
* console.log(`State changed at ${path}:`, value);
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* // Cleanup
|
|
45
|
+
* unsub();
|
|
46
|
+
* store.destroy();
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
export function createEventState(initial = {}) {
|
|
50
|
+
const state = JSON.parse(JSON.stringify(initial));
|
|
51
|
+
const listeners = new Map();
|
|
52
|
+
let destroyed = false;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
/**
|
|
56
|
+
* Get value at path
|
|
57
|
+
* @param {string} path - Dot-separated path (e.g., 'user.profile.name')
|
|
58
|
+
* @returns {*} Value at path, or entire state if no path provided
|
|
59
|
+
*/
|
|
60
|
+
get(path) {
|
|
61
|
+
if (destroyed) throw new Error('Cannot get from destroyed store');
|
|
62
|
+
if (!path) return state;
|
|
63
|
+
return path.split(".").reduce((obj, key) => obj?.[key], state);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Set value at path and notify subscribers
|
|
68
|
+
* @param {string} path - Dot-separated path (e.g., 'user.profile.name')
|
|
69
|
+
* @param {*} value - New value
|
|
70
|
+
* @returns {*} The value that was set
|
|
71
|
+
*/
|
|
72
|
+
set(path, value) {
|
|
73
|
+
if (destroyed) throw new Error('Cannot set on destroyed store');
|
|
74
|
+
if (!path) return value;
|
|
75
|
+
|
|
76
|
+
const parts = path.split(".");
|
|
77
|
+
const key = parts.pop();
|
|
78
|
+
let cur = state;
|
|
79
|
+
|
|
80
|
+
// Navigate to parent object, creating nested objects as needed
|
|
81
|
+
for (const p of parts) {
|
|
82
|
+
if (!cur[p]) cur[p] = {};
|
|
83
|
+
cur = cur[p];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const oldValue = cur[key];
|
|
87
|
+
cur[key] = value;
|
|
88
|
+
|
|
89
|
+
if (!destroyed) {
|
|
90
|
+
const detail = { path, value, oldValue };
|
|
91
|
+
|
|
92
|
+
// Notify exact path subscribers (pass value directly for backwards compatibility)
|
|
93
|
+
const exactListeners = listeners.get(path);
|
|
94
|
+
if (exactListeners) {
|
|
95
|
+
exactListeners.forEach(cb => cb(value));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Notify wildcard subscribers for all parent paths (pass detail object)
|
|
99
|
+
for (let i = 0; i < parts.length; i++) {
|
|
100
|
+
const parentPath = parts.slice(0, i + 1).join('.');
|
|
101
|
+
const wildcardListeners = listeners.get(`${parentPath}.*`);
|
|
102
|
+
if (wildcardListeners) {
|
|
103
|
+
wildcardListeners.forEach(cb => cb(detail));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Notify global subscribers (pass detail object)
|
|
108
|
+
const globalListeners = listeners.get('*');
|
|
109
|
+
if (globalListeners) {
|
|
110
|
+
globalListeners.forEach(cb => cb(detail));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return value;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Subscribe to changes at path
|
|
119
|
+
* @param {string} path - Path to subscribe to (supports wildcards: 'user.*', '*')
|
|
120
|
+
* @param {Function} handler - Callback function receiving { path, value, oldValue }
|
|
121
|
+
* @returns {Function} Unsubscribe function
|
|
122
|
+
*/
|
|
123
|
+
subscribe(path, handler) {
|
|
124
|
+
if (destroyed) throw new Error('Cannot subscribe to destroyed store');
|
|
125
|
+
if (!path || typeof handler !== 'function') {
|
|
126
|
+
throw new TypeError('subscribe requires path and handler');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!listeners.has(path)) {
|
|
130
|
+
listeners.set(path, new Set());
|
|
131
|
+
}
|
|
132
|
+
listeners.get(path).add(handler);
|
|
133
|
+
|
|
134
|
+
return () => listeners.get(path)?.delete(handler);
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Destroy store and clear all subscriptions
|
|
139
|
+
*/
|
|
140
|
+
destroy() {
|
|
141
|
+
if (!destroyed) {
|
|
142
|
+
destroyed = true;
|
|
143
|
+
listeners.clear();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default createEventState;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// NOTE: This module composes the existing './eventState.js' implementation and returns
|
|
4
4
|
// an enhanced facade. The original fine-grained semantics (per-path events) remain intact.
|
|
5
5
|
|
|
6
|
-
import createEventStateBase from '../core/
|
|
6
|
+
import createEventStateBase from '../core/eventStateNew.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Create an enhanced EventState store while preserving the original semantics.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Provides TDD-style testing with type extraction capabilities
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { createEventState } from '../runtime/core/
|
|
7
|
+
import { createEventState } from '../runtime/core/eventStateNew.js';
|
|
8
8
|
|
|
9
9
|
export function createEventTest(initialState = {}) {
|
|
10
10
|
const store = createEventState(initialState);
|
package/index.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// Primary: EventState (recommended for application state)
|
|
9
|
-
export { createEventState } from './
|
|
10
|
-
export { createEventState as default } from './
|
|
9
|
+
export { createEventState } from './eventStateNew.js';
|
|
10
|
+
export { createEventState as default } from './eventStateNew.js';
|
|
11
11
|
|
|
12
12
|
// Specialized: CSS State (for CSS variables and theme management)
|
|
13
13
|
export { createCssState } from './cssState.js';
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uistate/core",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.1",
|
|
4
4
|
"description": "Lightweight event-driven state management with slot orchestration and experimental event-sequence testing (eventTest.js available under dual license)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./index.js",
|
|
9
9
|
"./eventState": "./eventState.js",
|
|
10
|
+
"./eventStateNew": "./eventStateNew.js",
|
|
10
11
|
"./cssState": "./cssState.js",
|
|
11
12
|
"./stateSerializer": "./stateSerializer.js",
|
|
12
13
|
"./templateManager": "./templateManager.js"
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
"files": [
|
|
15
16
|
"index.js",
|
|
16
17
|
"eventState.js",
|
|
18
|
+
"eventStateNew.js",
|
|
17
19
|
"cssState.js",
|
|
18
20
|
"stateSerializer.js",
|
|
19
21
|
"templateManager.js",
|