zero-query 1.1.1 → 1.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 (154) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +2 -0
  3. package/cli/args.js +33 -33
  4. package/cli/commands/build-api.js +443 -442
  5. package/cli/commands/build.js +254 -247
  6. package/cli/commands/bundle.js +1228 -1224
  7. package/cli/commands/create.js +137 -121
  8. package/cli/commands/dev/devtools/index.js +56 -56
  9. package/cli/commands/dev/devtools/js/components.js +49 -49
  10. package/cli/commands/dev/devtools/js/core.js +423 -423
  11. package/cli/commands/dev/devtools/js/elements.js +421 -421
  12. package/cli/commands/dev/devtools/js/network.js +166 -166
  13. package/cli/commands/dev/devtools/js/performance.js +73 -73
  14. package/cli/commands/dev/devtools/js/router.js +105 -105
  15. package/cli/commands/dev/devtools/js/source.js +132 -132
  16. package/cli/commands/dev/devtools/js/stats.js +35 -35
  17. package/cli/commands/dev/devtools/js/tabs.js +79 -79
  18. package/cli/commands/dev/devtools/panel.html +95 -95
  19. package/cli/commands/dev/devtools/styles.css +244 -244
  20. package/cli/commands/dev/index.js +107 -107
  21. package/cli/commands/dev/logger.js +75 -75
  22. package/cli/commands/dev/overlay.js +858 -858
  23. package/cli/commands/dev/server.js +220 -220
  24. package/cli/commands/dev/validator.js +94 -94
  25. package/cli/commands/dev/watcher.js +172 -172
  26. package/cli/help.js +114 -112
  27. package/cli/index.js +52 -52
  28. package/cli/scaffold/default/LICENSE +21 -21
  29. package/cli/scaffold/default/app/app.js +207 -207
  30. package/cli/scaffold/default/app/components/about.js +201 -201
  31. package/cli/scaffold/default/app/components/api-demo.js +143 -143
  32. package/cli/scaffold/default/app/components/contact-card.js +231 -231
  33. package/cli/scaffold/default/app/components/contacts/contacts.css +706 -706
  34. package/cli/scaffold/default/app/components/contacts/contacts.html +200 -200
  35. package/cli/scaffold/default/app/components/contacts/contacts.js +196 -196
  36. package/cli/scaffold/default/app/components/counter.js +127 -127
  37. package/cli/scaffold/default/app/components/home.js +249 -249
  38. package/cli/scaffold/default/app/components/not-found.js +16 -16
  39. package/cli/scaffold/default/app/components/playground/playground.css +115 -115
  40. package/cli/scaffold/default/app/components/playground/playground.html +161 -161
  41. package/cli/scaffold/default/app/components/playground/playground.js +116 -116
  42. package/cli/scaffold/default/app/components/todos.js +225 -225
  43. package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -97
  44. package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -146
  45. package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -280
  46. package/cli/scaffold/default/app/routes.js +15 -15
  47. package/cli/scaffold/default/app/store.js +101 -101
  48. package/cli/scaffold/default/global.css +552 -552
  49. package/cli/scaffold/default/index.html +99 -99
  50. package/cli/scaffold/minimal/app/app.js +85 -85
  51. package/cli/scaffold/minimal/app/components/about.js +68 -68
  52. package/cli/scaffold/minimal/app/components/counter.js +122 -122
  53. package/cli/scaffold/minimal/app/components/home.js +68 -68
  54. package/cli/scaffold/minimal/app/components/not-found.js +16 -16
  55. package/cli/scaffold/minimal/app/routes.js +9 -9
  56. package/cli/scaffold/minimal/app/store.js +36 -36
  57. package/cli/scaffold/minimal/global.css +300 -300
  58. package/cli/scaffold/minimal/index.html +44 -44
  59. package/cli/scaffold/ssr/app/app.js +41 -41
  60. package/cli/scaffold/ssr/app/components/about.js +55 -55
  61. package/cli/scaffold/ssr/app/components/blog/index.js +65 -65
  62. package/cli/scaffold/ssr/app/components/blog/post.js +86 -86
  63. package/cli/scaffold/ssr/app/components/home.js +37 -37
  64. package/cli/scaffold/ssr/app/components/not-found.js +15 -15
  65. package/cli/scaffold/ssr/app/routes.js +8 -8
  66. package/cli/scaffold/ssr/global.css +228 -228
  67. package/cli/scaffold/ssr/index.html +37 -37
  68. package/cli/scaffold/ssr/package.json +8 -8
  69. package/cli/scaffold/ssr/server/data/posts.js +144 -144
  70. package/cli/scaffold/ssr/server/index.js +213 -213
  71. package/cli/scaffold/webrtc/app/app.js +11 -0
  72. package/cli/scaffold/webrtc/app/components/video-room.js +295 -0
  73. package/cli/scaffold/webrtc/app/lib/room.js +252 -0
  74. package/cli/scaffold/webrtc/assets/.gitkeep +0 -0
  75. package/cli/scaffold/webrtc/global.css +250 -0
  76. package/cli/scaffold/webrtc/index.html +21 -0
  77. package/cli/utils.js +305 -287
  78. package/dist/API.md +661 -0
  79. package/dist/zquery.dist.zip +0 -0
  80. package/dist/zquery.js +10313 -6614
  81. package/dist/zquery.min.js +8 -631
  82. package/index.d.ts +570 -371
  83. package/index.js +311 -240
  84. package/package.json +76 -70
  85. package/src/component.js +1709 -1691
  86. package/src/core.js +921 -921
  87. package/src/diff.js +497 -497
  88. package/src/errors.js +209 -209
  89. package/src/expression.js +922 -922
  90. package/src/http.js +242 -242
  91. package/src/package.json +1 -1
  92. package/src/reactive.js +255 -255
  93. package/src/router.js +843 -843
  94. package/src/ssr.js +418 -418
  95. package/src/store.js +318 -318
  96. package/src/utils.js +515 -515
  97. package/src/webrtc/e2ee.js +351 -0
  98. package/src/webrtc/errors.js +116 -0
  99. package/src/webrtc/ice.js +301 -0
  100. package/src/webrtc/index.js +131 -0
  101. package/src/webrtc/joinToken.js +119 -0
  102. package/src/webrtc/observe.js +172 -0
  103. package/src/webrtc/peer.js +351 -0
  104. package/src/webrtc/reactive.js +268 -0
  105. package/src/webrtc/room.js +625 -0
  106. package/src/webrtc/sdp.js +302 -0
  107. package/src/webrtc/sfu/index.js +43 -0
  108. package/src/webrtc/sfu/livekit.js +131 -0
  109. package/src/webrtc/sfu/mediasoup.js +150 -0
  110. package/src/webrtc/signaling.js +373 -0
  111. package/src/webrtc/turn.js +237 -0
  112. package/tests/_helpers/webrtcFakes.js +289 -0
  113. package/tests/audit.test.js +4158 -4158
  114. package/tests/cli.test.js +1136 -1103
  115. package/tests/compare.test.js +497 -486
  116. package/tests/component.test.js +3969 -3938
  117. package/tests/core.test.js +1910 -1910
  118. package/tests/dev-server.test.js +489 -489
  119. package/tests/diff.test.js +1416 -1416
  120. package/tests/docs.test.js +1664 -1650
  121. package/tests/electron-features.test.js +864 -864
  122. package/tests/errors.test.js +619 -619
  123. package/tests/expression.test.js +1056 -1056
  124. package/tests/http.test.js +648 -648
  125. package/tests/reactive.test.js +819 -819
  126. package/tests/router.test.js +2327 -2327
  127. package/tests/ssr.test.js +870 -870
  128. package/tests/store.test.js +830 -830
  129. package/tests/test-minifier.js +153 -153
  130. package/tests/test-ssr.js +27 -27
  131. package/tests/utils.test.js +1377 -1377
  132. package/tests/webrtc/e2ee.test.js +283 -0
  133. package/tests/webrtc/ice.test.js +202 -0
  134. package/tests/webrtc/joinToken.test.js +89 -0
  135. package/tests/webrtc/observe.test.js +111 -0
  136. package/tests/webrtc/peer.test.js +373 -0
  137. package/tests/webrtc/reactive.test.js +235 -0
  138. package/tests/webrtc/room.test.js +406 -0
  139. package/tests/webrtc/sdp.test.js +151 -0
  140. package/tests/webrtc/sfu-livekit.test.js +119 -0
  141. package/tests/webrtc/sfu.test.js +160 -0
  142. package/tests/webrtc/signaling.test.js +251 -0
  143. package/tests/webrtc/turn.test.js +256 -0
  144. package/types/collection.d.ts +383 -383
  145. package/types/component.d.ts +186 -186
  146. package/types/errors.d.ts +135 -135
  147. package/types/http.d.ts +92 -92
  148. package/types/misc.d.ts +201 -201
  149. package/types/reactive.d.ts +98 -98
  150. package/types/router.d.ts +190 -190
  151. package/types/ssr.d.ts +102 -102
  152. package/types/store.d.ts +146 -146
  153. package/types/utils.d.ts +245 -245
  154. package/types/webrtc.d.ts +653 -0
package/src/store.js CHANGED
@@ -1,318 +1,318 @@
1
- /**
2
- * zQuery Store - Global reactive state management
3
- *
4
- * A lightweight Redux/Vuex-inspired store with:
5
- * - Reactive state via Proxy
6
- * - Named actions for mutations
7
- * - Key-specific subscriptions
8
- * - Computed getters
9
- * - Middleware support
10
- * - DevTools-friendly (logs actions in dev mode)
11
- *
12
- * Usage:
13
- * const store = $.store({
14
- * state: { count: 0, user: null },
15
- * actions: {
16
- * increment(state) { state.count++; },
17
- * setUser(state, user) { state.user = user; }
18
- * },
19
- * getters: {
20
- * doubleCount: (state) => state.count * 2
21
- * }
22
- * });
23
- *
24
- * store.dispatch('increment');
25
- * store.subscribe('count', (val, old) => console.log(val));
26
- */
27
-
28
- import { reactive } from './reactive.js';
29
- import { reportError, ErrorCode, ZQueryError } from './errors.js';
30
-
31
- class Store {
32
- constructor(config = {}) {
33
- this._subscribers = new Map(); // key → Set<fn>
34
- this._wildcards = new Set(); // subscribe to all changes
35
- this._actions = config.actions || {};
36
- this._getters = config.getters || {};
37
- this._middleware = [];
38
- this._history = []; // action log
39
- this._maxHistory = config.maxHistory || 1000;
40
- this._debug = config.debug || false;
41
- this._batching = false;
42
- this._batchQueue = []; // pending notifications during batch
43
- this._undoStack = [];
44
- this._redoStack = [];
45
- this._maxUndo = config.maxUndo || 50;
46
-
47
- // Store initial state for reset
48
- const initial = typeof config.state === 'function' ? config.state() : { ...(config.state || {}) };
49
- this._initialState = JSON.parse(JSON.stringify(initial));
50
-
51
- this.state = reactive(initial, (key, value, old) => {
52
- if (this._batching) {
53
- this._batchQueue.push({ key, value, old });
54
- return;
55
- }
56
- this._notifySubscribers(key, value, old);
57
- });
58
-
59
- // Build getters as computed properties
60
- this.getters = {};
61
- for (const [name, fn] of Object.entries(this._getters)) {
62
- Object.defineProperty(this.getters, name, {
63
- get: () => fn(this.state.__raw || this.state),
64
- enumerable: true
65
- });
66
- }
67
- }
68
-
69
- /** @private Notify key-specific and wildcard subscribers */
70
- _notifySubscribers(key, value, old) {
71
- const subs = this._subscribers.get(key);
72
- if (subs) subs.forEach(fn => {
73
- try { fn(key, value, old); }
74
- catch (err) { reportError(ErrorCode.STORE_SUBSCRIBE, `Subscriber for "${key}" threw`, { key }, err); }
75
- });
76
- this._wildcards.forEach(fn => {
77
- try { fn(key, value, old); }
78
- catch (err) { reportError(ErrorCode.STORE_SUBSCRIBE, 'Wildcard subscriber threw', { key }, err); }
79
- });
80
- }
81
-
82
- /**
83
- * Batch multiple state changes - subscribers fire once at the end
84
- * with only the latest value per key.
85
- */
86
- batch(fn) {
87
- this._batching = true;
88
- this._batchQueue = [];
89
- let result;
90
- try {
91
- result = fn(this.state);
92
- } finally {
93
- this._batching = false;
94
- // Deduplicate: keep only the last change per key
95
- const last = new Map();
96
- for (const entry of this._batchQueue) {
97
- last.set(entry.key, entry);
98
- }
99
- this._batchQueue = [];
100
- for (const { key, value, old } of last.values()) {
101
- this._notifySubscribers(key, value, old);
102
- }
103
- }
104
- return result;
105
- }
106
-
107
- /**
108
- * Save a snapshot for undo. Call before making changes you want to be undoable.
109
- */
110
- checkpoint() {
111
- const snap = JSON.parse(JSON.stringify(this.state.__raw || this.state));
112
- this._undoStack.push(snap);
113
- if (this._undoStack.length > this._maxUndo) {
114
- this._undoStack.splice(0, this._undoStack.length - this._maxUndo);
115
- }
116
- this._redoStack = [];
117
- }
118
-
119
- /**
120
- * Undo to the last checkpoint
121
- * @returns {boolean} true if undo was performed
122
- */
123
- undo() {
124
- if (this._undoStack.length === 0) return false;
125
- const current = JSON.parse(JSON.stringify(this.state.__raw || this.state));
126
- this._redoStack.push(current);
127
- const prev = this._undoStack.pop();
128
- this.replaceState(prev);
129
- return true;
130
- }
131
-
132
- /**
133
- * Redo the last undone state change
134
- * @returns {boolean} true if redo was performed
135
- */
136
- redo() {
137
- if (this._redoStack.length === 0) return false;
138
- const current = JSON.parse(JSON.stringify(this.state.__raw || this.state));
139
- this._undoStack.push(current);
140
- const next = this._redoStack.pop();
141
- this.replaceState(next);
142
- return true;
143
- }
144
-
145
- /** Check if undo is available */
146
- get canUndo() { return this._undoStack.length > 0; }
147
-
148
- /** Check if redo is available */
149
- get canRedo() { return this._redoStack.length > 0; }
150
-
151
- /**
152
- * Dispatch a named action
153
- * @param {string} name - action name
154
- * @param {...any} args - payload
155
- */
156
- dispatch(name, ...args) {
157
- const action = this._actions[name];
158
- if (!action) {
159
- reportError(ErrorCode.STORE_ACTION, `Unknown action "${name}"`, { action: name, args });
160
- return;
161
- }
162
-
163
- // Run middleware
164
- for (const mw of this._middleware) {
165
- try {
166
- const result = mw(name, args, this.state);
167
- if (result === false) return; // blocked by middleware
168
- } catch (err) {
169
- reportError(ErrorCode.STORE_MIDDLEWARE, `Middleware threw during "${name}"`, { action: name }, err);
170
- return;
171
- }
172
- }
173
-
174
- if (this._debug) {
175
- console.log(`%c[Store] ${name}`, 'color: #4CAF50; font-weight: bold;', ...args);
176
- }
177
-
178
- try {
179
- const result = action(this.state, ...args);
180
- this._history.push({ action: name, args, timestamp: Date.now() });
181
- // Cap history to prevent unbounded memory growth
182
- if (this._history.length > this._maxHistory) {
183
- this._history.splice(0, this._history.length - this._maxHistory);
184
- }
185
- return result;
186
- } catch (err) {
187
- reportError(ErrorCode.STORE_ACTION, `Action "${name}" threw`, { action: name, args }, err);
188
- }
189
- }
190
-
191
- /**
192
- * Subscribe to changes on a specific state key, multiple keys, or all changes.
193
- *
194
- * Signatures:
195
- * subscribe(callback) → wildcard, fires on every change
196
- * subscribe('key', callback) → fires when 'key' changes
197
- * subscribe(['a','b'], callback) → fires when any listed key changes
198
- *
199
- * @param {string|string[]|Function} keyOrFn - state key, array of keys, or function for all changes
200
- * @param {Function} [fn] - callback (key, value, oldValue)
201
- * @returns {Function} - unsubscribe
202
- */
203
- subscribe(keyOrFn, fn) {
204
- if (typeof keyOrFn === 'function') {
205
- // Wildcard - listen to all changes
206
- this._wildcards.add(keyOrFn);
207
- return () => this._wildcards.delete(keyOrFn);
208
- }
209
-
210
- // Multi-key subscription: subscribe(['files', 'isProcessing'], callback)
211
- if (Array.isArray(keyOrFn)) {
212
- const keys = keyOrFn;
213
- const handler = (key, value, old) => {
214
- if (keys.includes(key)) fn(key, value, old);
215
- };
216
- this._wildcards.add(handler);
217
- return () => this._wildcards.delete(handler);
218
- }
219
-
220
- if (!this._subscribers.has(keyOrFn)) {
221
- this._subscribers.set(keyOrFn, new Set());
222
- }
223
- this._subscribers.get(keyOrFn).add(fn);
224
- return () => this._subscribers.get(keyOrFn)?.delete(fn);
225
- }
226
-
227
- /**
228
- * Get current state snapshot (plain object)
229
- */
230
- snapshot() {
231
- return JSON.parse(JSON.stringify(this.state.__raw || this.state));
232
- }
233
-
234
- /**
235
- * Replace entire state
236
- */
237
- replaceState(newState) {
238
- const raw = this.state.__raw || this.state;
239
- for (const key of Object.keys(raw)) {
240
- delete this.state[key];
241
- }
242
- Object.assign(this.state, newState);
243
- }
244
-
245
- /**
246
- * Add middleware: fn(actionName, args, state) → false to block
247
- */
248
- use(fn) {
249
- this._middleware.push(fn);
250
- return this;
251
- }
252
-
253
- /**
254
- * Get action history
255
- */
256
- get history() {
257
- return [...this._history];
258
- }
259
-
260
- /**
261
- * Reset state to initial values. If no argument, resets to the original state.
262
- */
263
- reset(initialState) {
264
- this.replaceState(initialState || JSON.parse(JSON.stringify(this._initialState)));
265
- this._history = [];
266
- this._undoStack = [];
267
- this._redoStack = [];
268
- }
269
- }
270
-
271
-
272
- // ---------------------------------------------------------------------------
273
- // Factory
274
- // ---------------------------------------------------------------------------
275
- let _stores = new Map();
276
-
277
- export function createStore(name, config) {
278
- // If called with just config (no name), use 'default'
279
- if (typeof name === 'object') {
280
- config = name;
281
- name = 'default';
282
- }
283
- const store = new Store(config);
284
- _stores.set(name, store);
285
- return store;
286
- }
287
-
288
- export function getStore(name = 'default') {
289
- return _stores.get(name) || null;
290
- }
291
-
292
-
293
- // ---------------------------------------------------------------------------
294
- // Store-Component Connector
295
- // ---------------------------------------------------------------------------
296
-
297
- /**
298
- * Create a store connector descriptor for use in component definitions.
299
- * When used in a component's `stores` config, auto-subscribes to the
300
- * listed keys on mount and cleans up on destroy.
301
- *
302
- * Usage:
303
- * $.component('my-comp', {
304
- * stores: {
305
- * app: connectStore(appStore, ['files', 'isProcessing']),
306
- * },
307
- * render() {
308
- * return `<div>${this.stores.app.files.length} files</div>`;
309
- * }
310
- * });
311
- *
312
- * @param {Store} store - the store instance to connect
313
- * @param {string[]} keys - state keys to sync
314
- * @returns {{ _zqConnector: true, store: Store, keys: string[] }}
315
- */
316
- export function connectStore(store, keys) {
317
- return { _zqConnector: true, store, keys };
318
- }
1
+ /**
2
+ * zQuery Store - Global reactive state management
3
+ *
4
+ * A lightweight Redux/Vuex-inspired store with:
5
+ * - Reactive state via Proxy
6
+ * - Named actions for mutations
7
+ * - Key-specific subscriptions
8
+ * - Computed getters
9
+ * - Middleware support
10
+ * - DevTools-friendly (logs actions in dev mode)
11
+ *
12
+ * Usage:
13
+ * const store = $.store({
14
+ * state: { count: 0, user: null },
15
+ * actions: {
16
+ * increment(state) { state.count++; },
17
+ * setUser(state, user) { state.user = user; }
18
+ * },
19
+ * getters: {
20
+ * doubleCount: (state) => state.count * 2
21
+ * }
22
+ * });
23
+ *
24
+ * store.dispatch('increment');
25
+ * store.subscribe('count', (val, old) => console.log(val));
26
+ */
27
+
28
+ import { reactive } from './reactive.js';
29
+ import { reportError, ErrorCode, ZQueryError } from './errors.js';
30
+
31
+ class Store {
32
+ constructor(config = {}) {
33
+ this._subscribers = new Map(); // key → Set<fn>
34
+ this._wildcards = new Set(); // subscribe to all changes
35
+ this._actions = config.actions || {};
36
+ this._getters = config.getters || {};
37
+ this._middleware = [];
38
+ this._history = []; // action log
39
+ this._maxHistory = config.maxHistory || 1000;
40
+ this._debug = config.debug || false;
41
+ this._batching = false;
42
+ this._batchQueue = []; // pending notifications during batch
43
+ this._undoStack = [];
44
+ this._redoStack = [];
45
+ this._maxUndo = config.maxUndo || 50;
46
+
47
+ // Store initial state for reset
48
+ const initial = typeof config.state === 'function' ? config.state() : { ...(config.state || {}) };
49
+ this._initialState = JSON.parse(JSON.stringify(initial));
50
+
51
+ this.state = reactive(initial, (key, value, old) => {
52
+ if (this._batching) {
53
+ this._batchQueue.push({ key, value, old });
54
+ return;
55
+ }
56
+ this._notifySubscribers(key, value, old);
57
+ });
58
+
59
+ // Build getters as computed properties
60
+ this.getters = {};
61
+ for (const [name, fn] of Object.entries(this._getters)) {
62
+ Object.defineProperty(this.getters, name, {
63
+ get: () => fn(this.state.__raw || this.state),
64
+ enumerable: true
65
+ });
66
+ }
67
+ }
68
+
69
+ /** @private Notify key-specific and wildcard subscribers */
70
+ _notifySubscribers(key, value, old) {
71
+ const subs = this._subscribers.get(key);
72
+ if (subs) subs.forEach(fn => {
73
+ try { fn(key, value, old); }
74
+ catch (err) { reportError(ErrorCode.STORE_SUBSCRIBE, `Subscriber for "${key}" threw`, { key }, err); }
75
+ });
76
+ this._wildcards.forEach(fn => {
77
+ try { fn(key, value, old); }
78
+ catch (err) { reportError(ErrorCode.STORE_SUBSCRIBE, 'Wildcard subscriber threw', { key }, err); }
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Batch multiple state changes - subscribers fire once at the end
84
+ * with only the latest value per key.
85
+ */
86
+ batch(fn) {
87
+ this._batching = true;
88
+ this._batchQueue = [];
89
+ let result;
90
+ try {
91
+ result = fn(this.state);
92
+ } finally {
93
+ this._batching = false;
94
+ // Deduplicate: keep only the last change per key
95
+ const last = new Map();
96
+ for (const entry of this._batchQueue) {
97
+ last.set(entry.key, entry);
98
+ }
99
+ this._batchQueue = [];
100
+ for (const { key, value, old } of last.values()) {
101
+ this._notifySubscribers(key, value, old);
102
+ }
103
+ }
104
+ return result;
105
+ }
106
+
107
+ /**
108
+ * Save a snapshot for undo. Call before making changes you want to be undoable.
109
+ */
110
+ checkpoint() {
111
+ const snap = JSON.parse(JSON.stringify(this.state.__raw || this.state));
112
+ this._undoStack.push(snap);
113
+ if (this._undoStack.length > this._maxUndo) {
114
+ this._undoStack.splice(0, this._undoStack.length - this._maxUndo);
115
+ }
116
+ this._redoStack = [];
117
+ }
118
+
119
+ /**
120
+ * Undo to the last checkpoint
121
+ * @returns {boolean} true if undo was performed
122
+ */
123
+ undo() {
124
+ if (this._undoStack.length === 0) return false;
125
+ const current = JSON.parse(JSON.stringify(this.state.__raw || this.state));
126
+ this._redoStack.push(current);
127
+ const prev = this._undoStack.pop();
128
+ this.replaceState(prev);
129
+ return true;
130
+ }
131
+
132
+ /**
133
+ * Redo the last undone state change
134
+ * @returns {boolean} true if redo was performed
135
+ */
136
+ redo() {
137
+ if (this._redoStack.length === 0) return false;
138
+ const current = JSON.parse(JSON.stringify(this.state.__raw || this.state));
139
+ this._undoStack.push(current);
140
+ const next = this._redoStack.pop();
141
+ this.replaceState(next);
142
+ return true;
143
+ }
144
+
145
+ /** Check if undo is available */
146
+ get canUndo() { return this._undoStack.length > 0; }
147
+
148
+ /** Check if redo is available */
149
+ get canRedo() { return this._redoStack.length > 0; }
150
+
151
+ /**
152
+ * Dispatch a named action
153
+ * @param {string} name - action name
154
+ * @param {...any} args - payload
155
+ */
156
+ dispatch(name, ...args) {
157
+ const action = this._actions[name];
158
+ if (!action) {
159
+ reportError(ErrorCode.STORE_ACTION, `Unknown action "${name}"`, { action: name, args });
160
+ return;
161
+ }
162
+
163
+ // Run middleware
164
+ for (const mw of this._middleware) {
165
+ try {
166
+ const result = mw(name, args, this.state);
167
+ if (result === false) return; // blocked by middleware
168
+ } catch (err) {
169
+ reportError(ErrorCode.STORE_MIDDLEWARE, `Middleware threw during "${name}"`, { action: name }, err);
170
+ return;
171
+ }
172
+ }
173
+
174
+ if (this._debug) {
175
+ console.log(`%c[Store] ${name}`, 'color: #4CAF50; font-weight: bold;', ...args);
176
+ }
177
+
178
+ try {
179
+ const result = action(this.state, ...args);
180
+ this._history.push({ action: name, args, timestamp: Date.now() });
181
+ // Cap history to prevent unbounded memory growth
182
+ if (this._history.length > this._maxHistory) {
183
+ this._history.splice(0, this._history.length - this._maxHistory);
184
+ }
185
+ return result;
186
+ } catch (err) {
187
+ reportError(ErrorCode.STORE_ACTION, `Action "${name}" threw`, { action: name, args }, err);
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Subscribe to changes on a specific state key, multiple keys, or all changes.
193
+ *
194
+ * Signatures:
195
+ * subscribe(callback) → wildcard, fires on every change
196
+ * subscribe('key', callback) → fires when 'key' changes
197
+ * subscribe(['a','b'], callback) → fires when any listed key changes
198
+ *
199
+ * @param {string|string[]|Function} keyOrFn - state key, array of keys, or function for all changes
200
+ * @param {Function} [fn] - callback (key, value, oldValue)
201
+ * @returns {Function} - unsubscribe
202
+ */
203
+ subscribe(keyOrFn, fn) {
204
+ if (typeof keyOrFn === 'function') {
205
+ // Wildcard - listen to all changes
206
+ this._wildcards.add(keyOrFn);
207
+ return () => this._wildcards.delete(keyOrFn);
208
+ }
209
+
210
+ // Multi-key subscription: subscribe(['files', 'isProcessing'], callback)
211
+ if (Array.isArray(keyOrFn)) {
212
+ const keys = keyOrFn;
213
+ const handler = (key, value, old) => {
214
+ if (keys.includes(key)) fn(key, value, old);
215
+ };
216
+ this._wildcards.add(handler);
217
+ return () => this._wildcards.delete(handler);
218
+ }
219
+
220
+ if (!this._subscribers.has(keyOrFn)) {
221
+ this._subscribers.set(keyOrFn, new Set());
222
+ }
223
+ this._subscribers.get(keyOrFn).add(fn);
224
+ return () => this._subscribers.get(keyOrFn)?.delete(fn);
225
+ }
226
+
227
+ /**
228
+ * Get current state snapshot (plain object)
229
+ */
230
+ snapshot() {
231
+ return JSON.parse(JSON.stringify(this.state.__raw || this.state));
232
+ }
233
+
234
+ /**
235
+ * Replace entire state
236
+ */
237
+ replaceState(newState) {
238
+ const raw = this.state.__raw || this.state;
239
+ for (const key of Object.keys(raw)) {
240
+ delete this.state[key];
241
+ }
242
+ Object.assign(this.state, newState);
243
+ }
244
+
245
+ /**
246
+ * Add middleware: fn(actionName, args, state) → false to block
247
+ */
248
+ use(fn) {
249
+ this._middleware.push(fn);
250
+ return this;
251
+ }
252
+
253
+ /**
254
+ * Get action history
255
+ */
256
+ get history() {
257
+ return [...this._history];
258
+ }
259
+
260
+ /**
261
+ * Reset state to initial values. If no argument, resets to the original state.
262
+ */
263
+ reset(initialState) {
264
+ this.replaceState(initialState || JSON.parse(JSON.stringify(this._initialState)));
265
+ this._history = [];
266
+ this._undoStack = [];
267
+ this._redoStack = [];
268
+ }
269
+ }
270
+
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // Factory
274
+ // ---------------------------------------------------------------------------
275
+ let _stores = new Map();
276
+
277
+ export function createStore(name, config) {
278
+ // If called with just config (no name), use 'default'
279
+ if (typeof name === 'object') {
280
+ config = name;
281
+ name = 'default';
282
+ }
283
+ const store = new Store(config);
284
+ _stores.set(name, store);
285
+ return store;
286
+ }
287
+
288
+ export function getStore(name = 'default') {
289
+ return _stores.get(name) || null;
290
+ }
291
+
292
+
293
+ // ---------------------------------------------------------------------------
294
+ // Store-Component Connector
295
+ // ---------------------------------------------------------------------------
296
+
297
+ /**
298
+ * Create a store connector descriptor for use in component definitions.
299
+ * When used in a component's `stores` config, auto-subscribes to the
300
+ * listed keys on mount and cleans up on destroy.
301
+ *
302
+ * Usage:
303
+ * $.component('my-comp', {
304
+ * stores: {
305
+ * app: connectStore(appStore, ['files', 'isProcessing']),
306
+ * },
307
+ * render() {
308
+ * return `<div>${this.stores.app.files.length} files</div>`;
309
+ * }
310
+ * });
311
+ *
312
+ * @param {Store} store - the store instance to connect
313
+ * @param {string[]} keys - state keys to sync
314
+ * @returns {{ _zqConnector: true, store: Store, keys: string[] }}
315
+ */
316
+ export function connectStore(store, keys) {
317
+ return { _zqConnector: true, store, keys };
318
+ }