zero-query 1.0.9 → 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.
- package/LICENSE +21 -21
- package/README.md +2 -0
- package/cli/args.js +33 -33
- package/cli/commands/build-api.js +443 -0
- package/cli/commands/build.js +254 -216
- package/cli/commands/bundle.js +1228 -1183
- package/cli/commands/create.js +137 -121
- package/cli/commands/dev/devtools/index.js +56 -56
- package/cli/commands/dev/devtools/js/components.js +49 -49
- package/cli/commands/dev/devtools/js/core.js +423 -423
- package/cli/commands/dev/devtools/js/elements.js +421 -421
- package/cli/commands/dev/devtools/js/network.js +166 -166
- package/cli/commands/dev/devtools/js/performance.js +73 -73
- package/cli/commands/dev/devtools/js/router.js +105 -105
- package/cli/commands/dev/devtools/js/source.js +132 -132
- package/cli/commands/dev/devtools/js/stats.js +35 -35
- package/cli/commands/dev/devtools/js/tabs.js +79 -79
- package/cli/commands/dev/devtools/panel.html +95 -95
- package/cli/commands/dev/devtools/styles.css +244 -244
- package/cli/commands/dev/index.js +107 -107
- package/cli/commands/dev/logger.js +75 -75
- package/cli/commands/dev/overlay.js +858 -858
- package/cli/commands/dev/server.js +220 -167
- package/cli/commands/dev/validator.js +94 -94
- package/cli/commands/dev/watcher.js +172 -172
- package/cli/help.js +114 -112
- package/cli/index.js +52 -52
- package/cli/scaffold/default/LICENSE +21 -21
- package/cli/scaffold/default/app/app.js +207 -207
- package/cli/scaffold/default/app/components/about.js +201 -201
- package/cli/scaffold/default/app/components/api-demo.js +143 -143
- package/cli/scaffold/default/app/components/contact-card.js +231 -231
- package/cli/scaffold/default/app/components/contacts/contacts.css +706 -706
- package/cli/scaffold/default/app/components/contacts/contacts.html +200 -200
- package/cli/scaffold/default/app/components/contacts/contacts.js +196 -196
- package/cli/scaffold/default/app/components/counter.js +127 -127
- package/cli/scaffold/default/app/components/home.js +249 -249
- package/cli/scaffold/default/app/components/not-found.js +16 -16
- package/cli/scaffold/default/app/components/playground/playground.css +115 -115
- package/cli/scaffold/default/app/components/playground/playground.html +161 -161
- package/cli/scaffold/default/app/components/playground/playground.js +116 -116
- package/cli/scaffold/default/app/components/todos.js +225 -225
- package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -97
- package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -146
- package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -280
- package/cli/scaffold/default/app/routes.js +15 -15
- package/cli/scaffold/default/app/store.js +101 -101
- package/cli/scaffold/default/global.css +552 -552
- package/cli/scaffold/default/index.html +99 -99
- package/cli/scaffold/minimal/app/app.js +85 -85
- package/cli/scaffold/minimal/app/components/about.js +68 -68
- package/cli/scaffold/minimal/app/components/counter.js +122 -122
- package/cli/scaffold/minimal/app/components/home.js +68 -68
- package/cli/scaffold/minimal/app/components/not-found.js +16 -16
- package/cli/scaffold/minimal/app/routes.js +9 -9
- package/cli/scaffold/minimal/app/store.js +36 -36
- package/cli/scaffold/minimal/global.css +300 -300
- package/cli/scaffold/minimal/index.html +44 -44
- package/cli/scaffold/ssr/app/app.js +41 -41
- package/cli/scaffold/ssr/app/components/about.js +55 -55
- package/cli/scaffold/ssr/app/components/blog/index.js +65 -65
- package/cli/scaffold/ssr/app/components/blog/post.js +86 -86
- package/cli/scaffold/ssr/app/components/home.js +37 -37
- package/cli/scaffold/ssr/app/components/not-found.js +15 -15
- package/cli/scaffold/ssr/app/routes.js +8 -8
- package/cli/scaffold/ssr/global.css +228 -228
- package/cli/scaffold/ssr/index.html +37 -37
- package/cli/scaffold/ssr/package.json +8 -8
- package/cli/scaffold/ssr/server/data/posts.js +144 -144
- package/cli/scaffold/ssr/server/index.js +213 -213
- package/cli/scaffold/webrtc/app/app.js +11 -0
- package/cli/scaffold/webrtc/app/components/video-room.js +295 -0
- package/cli/scaffold/webrtc/app/lib/room.js +252 -0
- package/cli/scaffold/webrtc/assets/.gitkeep +0 -0
- package/cli/scaffold/webrtc/global.css +250 -0
- package/cli/scaffold/webrtc/index.html +21 -0
- package/cli/utils.js +305 -287
- package/dist/API.md +7264 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +10313 -6252
- package/dist/zquery.min.js +8 -601
- package/index.d.ts +570 -365
- package/index.js +311 -232
- package/package.json +76 -69
- package/src/component.js +1709 -1454
- package/src/core.js +921 -921
- package/src/diff.js +497 -497
- package/src/errors.js +209 -209
- package/src/expression.js +922 -922
- package/src/http.js +242 -242
- package/src/package.json +1 -1
- package/src/reactive.js +255 -254
- package/src/router.js +843 -773
- package/src/ssr.js +418 -418
- package/src/store.js +318 -272
- package/src/utils.js +515 -515
- package/src/webrtc/e2ee.js +351 -0
- package/src/webrtc/errors.js +116 -0
- package/src/webrtc/ice.js +301 -0
- package/src/webrtc/index.js +131 -0
- package/src/webrtc/joinToken.js +119 -0
- package/src/webrtc/observe.js +172 -0
- package/src/webrtc/peer.js +351 -0
- package/src/webrtc/reactive.js +268 -0
- package/src/webrtc/room.js +625 -0
- package/src/webrtc/sdp.js +302 -0
- package/src/webrtc/sfu/index.js +43 -0
- package/src/webrtc/sfu/livekit.js +131 -0
- package/src/webrtc/sfu/mediasoup.js +150 -0
- package/src/webrtc/signaling.js +373 -0
- package/src/webrtc/turn.js +237 -0
- package/tests/_helpers/webrtcFakes.js +289 -0
- package/tests/audit.test.js +4158 -4158
- package/tests/cli.test.js +1136 -1023
- package/tests/compare.test.js +497 -0
- package/tests/component.test.js +3969 -3938
- package/tests/core.test.js +1910 -1910
- package/tests/dev-server.test.js +489 -0
- package/tests/diff.test.js +1416 -1416
- package/tests/docs.test.js +1664 -0
- package/tests/electron-features.test.js +864 -0
- package/tests/errors.test.js +619 -619
- package/tests/expression.test.js +1056 -1056
- package/tests/http.test.js +648 -648
- package/tests/reactive.test.js +819 -819
- package/tests/router.test.js +2327 -2327
- package/tests/ssr.test.js +870 -870
- package/tests/store.test.js +830 -830
- package/tests/test-minifier.js +153 -153
- package/tests/test-ssr.js +27 -27
- package/tests/utils.test.js +1377 -1377
- package/tests/webrtc/e2ee.test.js +283 -0
- package/tests/webrtc/ice.test.js +202 -0
- package/tests/webrtc/joinToken.test.js +89 -0
- package/tests/webrtc/observe.test.js +111 -0
- package/tests/webrtc/peer.test.js +373 -0
- package/tests/webrtc/reactive.test.js +235 -0
- package/tests/webrtc/room.test.js +406 -0
- package/tests/webrtc/sdp.test.js +151 -0
- package/tests/webrtc/sfu-livekit.test.js +119 -0
- package/tests/webrtc/sfu.test.js +160 -0
- package/tests/webrtc/signaling.test.js +251 -0
- package/tests/webrtc/turn.test.js +256 -0
- package/types/collection.d.ts +383 -383
- package/types/component.d.ts +186 -186
- package/types/errors.d.ts +135 -135
- package/types/http.d.ts +92 -92
- package/types/misc.d.ts +201 -201
- package/types/reactive.d.ts +98 -98
- package/types/router.d.ts +190 -190
- package/types/ssr.d.ts +102 -102
- package/types/store.d.ts +146 -145
- package/types/utils.d.ts +245 -245
- package/types/webrtc.d.ts +653 -0
package/src/store.js
CHANGED
|
@@ -1,272 +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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
this.
|
|
125
|
-
const
|
|
126
|
-
this.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
this.
|
|
138
|
-
const
|
|
139
|
-
this.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
*
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
*
|
|
229
|
-
*/
|
|
230
|
-
|
|
231
|
-
this.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
this.
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
+
}
|