@uistate/core 5.1.0 → 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.
Files changed (2) hide show
  1. package/eventStateNew.js +74 -11
  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 {
@@ -89,22 +90,25 @@ export function createEventState(initial = {}) {
89
90
  if (!destroyed) {
90
91
  const detail = { path, value, oldValue };
91
92
 
92
- // Notify exact path subscribers (pass value directly for backwards compatibility)
93
+ // Notify exact path subscribers
93
94
  const exactListeners = listeners.get(path);
94
95
  if (exactListeners) {
95
- exactListeners.forEach(cb => cb(value));
96
+ exactListeners.forEach(cb => cb(value, detail));
96
97
  }
97
98
 
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));
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
+ }
104
108
  }
105
109
  }
106
110
 
107
- // Notify global subscribers (pass detail object)
111
+ // Notify global subscribers
108
112
  const globalListeners = listeners.get('*');
109
113
  if (globalListeners) {
110
114
  globalListeners.forEach(cb => cb(detail));
@@ -114,10 +118,67 @@ export function createEventState(initial = {}) {
114
118
  return value;
115
119
  },
116
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
+
117
176
  /**
118
177
  * Subscribe to changes at path
119
178
  * @param {string} path - Path to subscribe to (supports wildcards: 'user.*', '*')
120
- * @param {Function} handler - Callback function receiving { path, value, oldValue }
179
+ * @param {Function} handler - Callback function.
180
+ * - Exact path subscriptions: (value, meta) => void
181
+ * - Wildcard/global subscriptions: (meta) => void
121
182
  * @returns {Function} Unsubscribe function
122
183
  */
123
184
  subscribe(path, handler) {
@@ -140,6 +201,8 @@ export function createEventState(initial = {}) {
140
201
  destroy() {
141
202
  if (!destroyed) {
142
203
  destroyed = true;
204
+ asyncOps.forEach(({ controller }) => controller.abort());
205
+ asyncOps.clear();
143
206
  listeners.clear();
144
207
  }
145
208
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uistate/core",
3
- "version": "5.1.0",
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",