@uistate/core 5.1.0 → 5.2.0

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.
Files changed (2) hide show
  1. package/eventStateNew.js +81 -12
  2. package/package.json +1 -1
package/eventStateNew.js CHANGED
@@ -20,7 +20,7 @@
20
20
  * @example
21
21
  * const store = createEventState({ count: 0, user: { name: 'Alice' } });
22
22
  *
23
- * // Subscribe to specific path (receives value directly)
23
+ * // Subscribe to specific path
24
24
  * const unsub = store.subscribe('count', (value) => {
25
25
  * console.log('Count changed:', value);
26
26
  * });
@@ -49,6 +49,7 @@
49
49
  export function createEventState(initial = {}) {
50
50
  const state = JSON.parse(JSON.stringify(initial));
51
51
  const listeners = new Map();
52
+ const asyncOps = new Map();
52
53
  let destroyed = false;
53
54
 
54
55
  return {
@@ -60,7 +61,13 @@ export function createEventState(initial = {}) {
60
61
  get(path) {
61
62
  if (destroyed) throw new Error('Cannot get from destroyed store');
62
63
  if (!path) return state;
63
- return path.split(".").reduce((obj, key) => obj?.[key], state);
64
+ const parts = path.split('.');
65
+ let cur = state;
66
+ for (const p of parts) {
67
+ if (cur == null) return undefined;
68
+ cur = cur[p];
69
+ }
70
+ return cur;
64
71
  },
65
72
 
66
73
  /**
@@ -89,22 +96,25 @@ export function createEventState(initial = {}) {
89
96
  if (!destroyed) {
90
97
  const detail = { path, value, oldValue };
91
98
 
92
- // Notify exact path subscribers (pass value directly for backwards compatibility)
99
+ // Notify exact path subscribers
93
100
  const exactListeners = listeners.get(path);
94
101
  if (exactListeners) {
95
- exactListeners.forEach(cb => cb(value));
102
+ exactListeners.forEach(cb => cb(value, detail));
96
103
  }
97
104
 
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));
105
+ // Notify wildcard subscribers for parent paths
106
+ if (parts.length) {
107
+ let parent = "";
108
+ for (const p of parts) {
109
+ parent = parent ? `${parent}.${p}` : p;
110
+ const wildcardListeners = listeners.get(`${parent}.*`);
111
+ if (wildcardListeners) {
112
+ wildcardListeners.forEach(cb => cb(detail));
113
+ }
104
114
  }
105
115
  }
106
116
 
107
- // Notify global subscribers (pass detail object)
117
+ // Notify global subscribers
108
118
  const globalListeners = listeners.get('*');
109
119
  if (globalListeners) {
110
120
  globalListeners.forEach(cb => cb(detail));
@@ -114,10 +124,67 @@ export function createEventState(initial = {}) {
114
124
  return value;
115
125
  },
116
126
 
127
+ async setAsync(path, fetcher) {
128
+ if (destroyed) throw new Error('Cannot setAsync on destroyed store');
129
+ if (!path) throw new TypeError('setAsync requires a path');
130
+ if (typeof fetcher !== 'function') {
131
+ throw new TypeError('setAsync(path, fetcher) requires a function fetcher');
132
+ }
133
+
134
+ if (asyncOps.has(path)) {
135
+ asyncOps.get(path).controller.abort();
136
+ }
137
+
138
+ const controller = new AbortController();
139
+ asyncOps.set(path, { controller });
140
+
141
+ try {
142
+ this.set(`${path}.status`, 'loading');
143
+ this.set(`${path}.error`, null);
144
+
145
+ const data = await fetcher(controller.signal);
146
+
147
+ if (destroyed) throw new Error('Cannot setAsync on destroyed store');
148
+
149
+ this.set(`${path}.data`, data);
150
+ this.set(`${path}.status`, 'success');
151
+ return data;
152
+ } catch (err) {
153
+ if (err?.name === 'AbortError') {
154
+ this.set(`${path}.status`, 'cancelled');
155
+ const cancelErr = new Error('Request cancelled');
156
+ cancelErr.name = 'AbortError';
157
+ throw cancelErr;
158
+ }
159
+
160
+ this.set(`${path}.status`, 'error');
161
+ this.set(`${path}.error`, err?.message ?? String(err));
162
+ throw err;
163
+ } finally {
164
+ const op = asyncOps.get(path);
165
+ if (op?.controller === controller) {
166
+ asyncOps.delete(path);
167
+ }
168
+ }
169
+ },
170
+
171
+ cancel(path) {
172
+ if (destroyed) throw new Error('Cannot cancel on destroyed store');
173
+ if (!path) throw new TypeError('cancel requires a path');
174
+
175
+ if (asyncOps.has(path)) {
176
+ asyncOps.get(path).controller.abort();
177
+ asyncOps.delete(path);
178
+ this.set(`${path}.status`, 'cancelled');
179
+ }
180
+ },
181
+
117
182
  /**
118
183
  * Subscribe to changes at path
119
184
  * @param {string} path - Path to subscribe to (supports wildcards: 'user.*', '*')
120
- * @param {Function} handler - Callback function receiving { path, value, oldValue }
185
+ * @param {Function} handler - Callback function.
186
+ * - Exact path subscriptions: (value, meta) => void
187
+ * - Wildcard/global subscriptions: (meta) => void
121
188
  * @returns {Function} Unsubscribe function
122
189
  */
123
190
  subscribe(path, handler) {
@@ -140,6 +207,8 @@ export function createEventState(initial = {}) {
140
207
  destroy() {
141
208
  if (!destroyed) {
142
209
  destroyed = true;
210
+ asyncOps.forEach(({ controller }) => controller.abort());
211
+ asyncOps.clear();
143
212
  listeners.clear();
144
213
  }
145
214
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uistate/core",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
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",