seqda 1.1.3 → 2.1.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/.eslintrc.cjs +95 -0
- package/README.md +220 -79
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.js +27 -16
- package/webpack.config.js +0 -50
package/.eslintrc.cjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* eslint-disable no-magic-numbers */
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
'env': {
|
|
7
|
+
'browser': true,
|
|
8
|
+
'commonjs': true,
|
|
9
|
+
'es2021': true,
|
|
10
|
+
},
|
|
11
|
+
'extends': [
|
|
12
|
+
'eslint:recommended',
|
|
13
|
+
],
|
|
14
|
+
'parserOptions': {
|
|
15
|
+
'ecmaVersion': 'latest',
|
|
16
|
+
'sourceType': 'module',
|
|
17
|
+
},
|
|
18
|
+
'plugins': [
|
|
19
|
+
'@spothero/eslint-plugin-spothero',
|
|
20
|
+
],
|
|
21
|
+
'rules': {
|
|
22
|
+
'@spothero/spothero/ternary-parentheses': 'error',
|
|
23
|
+
'arrow-parens': 'error',
|
|
24
|
+
'arrow-spacing': [ 'error', { before: true, after: true } ],
|
|
25
|
+
'block-scoped-var': 'warn',
|
|
26
|
+
'block-spacing': 'error',
|
|
27
|
+
'brace-style': [ 'error', '1tbs' ],
|
|
28
|
+
'camelcase': 'warn',
|
|
29
|
+
'comma-dangle': [ 'error', 'always-multiline' ],
|
|
30
|
+
'comma-spacing': [ 'error', { before: false, after: true } ],
|
|
31
|
+
'comma-style': [ 'error', 'last' ],
|
|
32
|
+
'curly': [ 'error', 'multi-or-nest', 'consistent' ],
|
|
33
|
+
'default-case-last': 'error',
|
|
34
|
+
'default-param-last': 'error',
|
|
35
|
+
'eqeqeq': [ 'error', 'smart' ],
|
|
36
|
+
'func-call-spacing': [ 'error', 'never' ],
|
|
37
|
+
'guard-for-in': 'error',
|
|
38
|
+
'implicit-arrow-linebreak': [ 'error', 'beside' ],
|
|
39
|
+
'indent': [ 'error', 2, { 'SwitchCase': 1 } ],
|
|
40
|
+
'jsx-quotes': [ 'error', 'prefer-double' ],
|
|
41
|
+
'key-spacing': [ 'error', { beforeColon: false, afterColon: true, mode: 'minimum', 'align': 'value' } ],
|
|
42
|
+
'keyword-spacing': [ 'error', { before: true, after: true } ],
|
|
43
|
+
'linebreak-style': [ 'error', 'unix' ],
|
|
44
|
+
'lines-between-class-members': 'error',
|
|
45
|
+
'max-classes-per-file': [ 'error', 3 ],
|
|
46
|
+
'new-cap': [ 'error', { 'properties': false } ],
|
|
47
|
+
'new-parens': 'error',
|
|
48
|
+
'no-array-constructor': 'warn',
|
|
49
|
+
'no-caller': 'error',
|
|
50
|
+
'no-confusing-arrow': 'error',
|
|
51
|
+
'no-empty': 'warn',
|
|
52
|
+
'no-eq-null': 0,
|
|
53
|
+
'no-eval': 'error',
|
|
54
|
+
'no-extend-native': 'error',
|
|
55
|
+
'no-extra-label': 'error',
|
|
56
|
+
'no-floating-decimal': 'error',
|
|
57
|
+
'no-global-assign': 'error',
|
|
58
|
+
'no-implied-eval': 'error',
|
|
59
|
+
'no-labels': 'error',
|
|
60
|
+
'no-lone-blocks': 'warn',
|
|
61
|
+
'no-loop-func': 0,
|
|
62
|
+
'no-magic-numbers': [ 'warn', { ignoreArrayIndexes: true, ignoreDefaultValues: true, ignore: [ -1, 0, 1, 2, 16, 32, 64, 128, 256, 1024, 2048, 200, 301, 302, 400, 401, 404, 500 ] } ],
|
|
63
|
+
'no-nested-ternary': 'error',
|
|
64
|
+
'no-param-reassign': 'error',
|
|
65
|
+
'no-promise-executor-return': 'error',
|
|
66
|
+
'no-return-assign': 'error',
|
|
67
|
+
'no-sequences': 'error',
|
|
68
|
+
'no-shadow': 0,
|
|
69
|
+
'no-throw-literal': 'warn',
|
|
70
|
+
'no-trailing-spaces': 'error',
|
|
71
|
+
'no-unmodified-loop-condition': 'warn',
|
|
72
|
+
'no-unreachable-loop': 'warn',
|
|
73
|
+
'no-unreachable': 'warn',
|
|
74
|
+
'no-unused-private-class-members': 'warn',
|
|
75
|
+
'no-unused-vars': 'warn',
|
|
76
|
+
'no-whitespace-before-property': 'error',
|
|
77
|
+
'nonblock-statement-body-position': [ 'error', 'below' ],
|
|
78
|
+
'one-var': [ 'error', 'never' ],
|
|
79
|
+
'quotes': [ 'error', 'single' ],
|
|
80
|
+
'radix': 'error',
|
|
81
|
+
'rest-spread-spacing': [ 'error', 'never' ],
|
|
82
|
+
'semi-spacing': [ 'error', { before: false, after: true } ],
|
|
83
|
+
'semi-style': [ 'error', 'last' ],
|
|
84
|
+
'semi': 'error',
|
|
85
|
+
'space-before-blocks': 'error',
|
|
86
|
+
'space-infix-ops': 'error',
|
|
87
|
+
'space-unary-ops': [ 'error', { words: false, nonwords: false } ],
|
|
88
|
+
'strict': 'error',
|
|
89
|
+
'switch-colon-spacing': [ 'error', { before: false, after: true } ],
|
|
90
|
+
'template-curly-spacing': 'error',
|
|
91
|
+
'template-tag-spacing': 'error',
|
|
92
|
+
'wrap-iife': [ 'error', 'inside' ],
|
|
93
|
+
'yoda': 'error',
|
|
94
|
+
},
|
|
95
|
+
};
|
package/README.md
CHANGED
|
@@ -23,7 +23,8 @@ There are no actions, dispatches, reducers, or selectors per-se. Instead, there
|
|
|
23
23
|
In `seqda` there are a few key principles that will be mentioned throughout this document. Let's create a simple store to explain these principles and terminology:
|
|
24
24
|
|
|
25
25
|
```javascript
|
|
26
|
-
|
|
26
|
+
import { createStore } from 'seqda';
|
|
27
|
+
|
|
27
28
|
const MyStore = createStore({
|
|
28
29
|
todos: { // This is a "scope"
|
|
29
30
|
_: [], // This is the "default value" for this scope
|
|
@@ -84,52 +85,77 @@ const MyStore = createStore({
|
|
|
84
85
|
|
|
85
86
|
// Methods go here
|
|
86
87
|
}
|
|
87
|
-
})
|
|
88
|
+
});
|
|
88
89
|
|
|
89
90
|
// We can add a todo by calling our method
|
|
90
|
-
// (notice that the "context"
|
|
91
|
-
// are provided internally by seqda
|
|
91
|
+
// (notice that the "context" arguments ({ get, set })
|
|
92
|
+
// are provided internally by seqda)
|
|
92
93
|
|
|
93
|
-
MyStore.todos.add(
|
|
94
|
+
MyStore.todos.add({ todo: 'Do things!', id: 1 });
|
|
94
95
|
|
|
95
96
|
console.log(MyStore.getState());
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
97
|
+
// {
|
|
98
|
+
// "todos": [
|
|
99
|
+
// { "todo": "Do things!", "id": 1 }
|
|
100
|
+
// ],
|
|
101
|
+
// "config": {
|
|
102
|
+
// "configValue1": null,
|
|
103
|
+
// "configValue2": null,
|
|
104
|
+
// "userConfig": {
|
|
105
|
+
// "firstName": "",
|
|
106
|
+
// "lastName": ""
|
|
107
|
+
// }
|
|
108
|
+
// }
|
|
109
|
+
// }
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
##
|
|
112
|
+
## Immutability
|
|
113
113
|
|
|
114
|
-
In `seqda`,
|
|
114
|
+
In `seqda`, the store's internal state tree is frozen with `Object.freeze`. This ensures structural immutability — the state can only be updated through scope methods via `set()`.
|
|
115
115
|
|
|
116
116
|
```javascript
|
|
117
117
|
let state = MyStore.getState();
|
|
118
118
|
state.setSomething = toAValue;
|
|
119
|
+
// TypeError: Cannot add property setSomething, object is not extensible
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Important:** The freeze is *shallow* — it applies to the state tree nodes (objects and arrays at each path level) but does **not** deep-freeze objects stored as values inside those containers. For example, if you store an object inside an array scope, the array is frozen (you can't push/pop), but the object itself remains mutable:
|
|
119
123
|
|
|
120
|
-
|
|
124
|
+
```javascript
|
|
125
|
+
import { createStore } from 'seqda';
|
|
126
|
+
|
|
127
|
+
const store = createStore({
|
|
128
|
+
items: {
|
|
129
|
+
_: [],
|
|
130
|
+
add({ get, set }, item) {
|
|
131
|
+
set([...get(), item]);
|
|
132
|
+
},
|
|
133
|
+
get({ get }) {
|
|
134
|
+
return get();
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
let item = { name: 'test', mutable: true };
|
|
140
|
+
store.items.add(item);
|
|
141
|
+
|
|
142
|
+
let items = store.items.get();
|
|
143
|
+
items.push('fail'); // TypeError — array is frozen
|
|
144
|
+
items[0].name = 'modified'; // Works — item object is NOT frozen
|
|
121
145
|
```
|
|
122
146
|
|
|
147
|
+
This is by design. It keeps seqda lightweight and allows consumers to manage their own object immutability strategy (e.g., `Object.freeze` at the application level, or treating objects as immutable by convention).
|
|
148
|
+
|
|
123
149
|
## Method cache
|
|
124
150
|
|
|
125
151
|
All scope methods in `seqda` are cached by default. For this reason, it is fine to have getters that contain complex logic and filtering.
|
|
126
152
|
|
|
127
|
-
The cache is invalidated as soon as 1) the internal state for a scope is updated
|
|
153
|
+
The cache is invalidated as soon as 1) the internal state for a scope is updated via `set()`, or 2) the arguments to the method call change.
|
|
128
154
|
|
|
129
155
|
Let's see an example of this in action:
|
|
130
156
|
|
|
131
157
|
```javascript
|
|
132
|
-
|
|
158
|
+
import { createStore } from 'seqda';
|
|
133
159
|
|
|
134
160
|
const MyStore = createStore({
|
|
135
161
|
citizens: {
|
|
@@ -142,18 +168,14 @@ const MyStore = createStore({
|
|
|
142
168
|
_: [],
|
|
143
169
|
get({ get }, stateName) {
|
|
144
170
|
if (!stateName)
|
|
145
|
-
return get();
|
|
171
|
+
return get();
|
|
146
172
|
|
|
147
173
|
return get().find((state) => (state.name === stateName));
|
|
148
174
|
},
|
|
149
175
|
getCitizensForState({ get, store }, stateName) {
|
|
150
|
-
// First, get the state requested from the store
|
|
151
176
|
let state = store.states.get(stateName);
|
|
152
|
-
|
|
153
|
-
//
|
|
154
|
-
// This is now cached, so as long as the
|
|
155
|
-
// arguments (shortStateName) remain the
|
|
156
|
-
// same, we can quickly call this over and over.
|
|
177
|
+
// Cached — as long as shortStateName stays the same,
|
|
178
|
+
// repeated calls return instantly.
|
|
157
179
|
let citizens = store.citizens.getByState(state.shortName);
|
|
158
180
|
return citizens;
|
|
159
181
|
}
|
|
@@ -163,50 +185,102 @@ const MyStore = createStore({
|
|
|
163
185
|
|
|
164
186
|
## Update events
|
|
165
187
|
|
|
166
|
-
`seqda` emits an `'update'` event when the store has been updated. Unlike Redux, the `'update'` event is only triggered on the *next
|
|
188
|
+
`seqda` emits an `'update'` event when the store has been updated. Unlike Redux, the `'update'` event is only triggered on the *next microtask* (via `Promise.resolve().then(...)`). The update event reports which scopes were modified, and provides a frozen read-only snapshot of the previous state. This allows many store updates to happen sequentially, with only one event fired.
|
|
167
189
|
|
|
168
|
-
*Note: When the scope name
|
|
190
|
+
*Note: When the scope name in the `modified` array is `'*'`, the entire store has been updated (e.g., via `.hydrate()`).*
|
|
169
191
|
|
|
170
192
|
```javascript
|
|
171
|
-
|
|
193
|
+
import { createStore } from 'seqda';
|
|
194
|
+
|
|
172
195
|
const MyStore = createStore({
|
|
173
196
|
todos: {
|
|
174
197
|
_: [],
|
|
175
198
|
add({ get, set }, todo) {
|
|
176
199
|
set([ ...get(), todo ]);
|
|
177
200
|
},
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
},
|
|
181
|
-
get({ get }, todoID) {
|
|
182
|
-
if (arguments.length === 1)
|
|
183
|
-
return get();
|
|
184
|
-
|
|
185
|
-
return get().find((todo) => (todo.id === todoID));
|
|
201
|
+
get({ get }) {
|
|
202
|
+
return get();
|
|
186
203
|
},
|
|
187
204
|
},
|
|
188
205
|
});
|
|
189
206
|
|
|
190
|
-
MyStore.on('update', ({ store, modified }) => {
|
|
191
|
-
|
|
192
|
-
|
|
207
|
+
MyStore.on('update', ({ store, previousStore, modified }) => {
|
|
208
|
+
console.log('modified scopes:', modified);
|
|
209
|
+
// modified scopes: [ 'todos' ]
|
|
193
210
|
|
|
194
|
-
//
|
|
211
|
+
// previousStore is a frozen read-only clone of the state
|
|
212
|
+
// before this batch of updates:
|
|
213
|
+
console.log('before:', previousStore.todos.get());
|
|
214
|
+
console.log('after:', store.todos.get());
|
|
195
215
|
});
|
|
196
216
|
|
|
197
|
-
//
|
|
217
|
+
// Both adds happen in the same synchronous block —
|
|
218
|
+
// only ONE update event fires, listing 'todos' once.
|
|
198
219
|
MyStore.todos.add({ todo: 'Do something!', id: 1 });
|
|
199
220
|
MyStore.todos.add({ todo: 'Do another thing!', id: 2 });
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Sub-scope paths in `modified`
|
|
224
|
+
|
|
225
|
+
When a sub-scope is updated, the `modified` array contains the dot-separated path to that specific sub-scope:
|
|
226
|
+
|
|
227
|
+
```javascript
|
|
228
|
+
import { createStore } from 'seqda';
|
|
229
|
+
|
|
230
|
+
const store = createStore({
|
|
231
|
+
data: {
|
|
232
|
+
_: [],
|
|
233
|
+
config: {
|
|
234
|
+
_: { theme: 'dark' },
|
|
235
|
+
set({ get, set }, values) {
|
|
236
|
+
set({ ...get(), ...values });
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
store.on('update', ({ modified }) => {
|
|
243
|
+
console.log(modified);
|
|
244
|
+
// [ 'data.config' ] — the specific sub-scope path
|
|
245
|
+
});
|
|
200
246
|
|
|
201
|
-
|
|
247
|
+
store.data.config.set({ theme: 'light' });
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Custom events
|
|
251
|
+
|
|
252
|
+
The seqda store IS a Node.js `EventEmitter`. You can emit your own custom events through it alongside seqda's built-in events. Custom events fire **synchronously** (unlike seqda's batched `update` event):
|
|
253
|
+
|
|
254
|
+
```javascript
|
|
255
|
+
import { createStore } from 'seqda';
|
|
256
|
+
|
|
257
|
+
const store = createStore({
|
|
258
|
+
items: {
|
|
259
|
+
_: {},
|
|
260
|
+
put({ get, set }, item) {
|
|
261
|
+
set({ ...get(), [item.id]: item });
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Subscribe to a custom namespaced event
|
|
267
|
+
store.on('item:added:abc123', (data) => {
|
|
268
|
+
console.log('Item added:', data.item);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Your wrapper can emit custom events synchronously
|
|
272
|
+
// during operations, while seqda handles state batching:
|
|
273
|
+
let item = { id: 'abc123', name: 'test' };
|
|
274
|
+
store.items.put(item);
|
|
275
|
+
store.emit(`item:added:${item.id}`, { item });
|
|
202
276
|
```
|
|
203
277
|
|
|
204
278
|
## Fetch events
|
|
205
279
|
|
|
206
|
-
`seqda`
|
|
280
|
+
`seqda` can report which scopes are being read. Enable with `{ emitOnFetch: true }` and listen for the `'fetchScope'` event:
|
|
207
281
|
|
|
208
282
|
```javascript
|
|
209
|
-
|
|
283
|
+
import { createStore } from 'seqda';
|
|
210
284
|
|
|
211
285
|
const MyStore = createStore({
|
|
212
286
|
todos: {
|
|
@@ -214,29 +288,20 @@ const MyStore = createStore({
|
|
|
214
288
|
add({ get, set }, todo) {
|
|
215
289
|
set([ ...get(), todo ]);
|
|
216
290
|
},
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
},
|
|
220
|
-
get({ get }, todoID) {
|
|
221
|
-
if (arguments.length === 1)
|
|
222
|
-
return get();
|
|
223
|
-
|
|
224
|
-
return get().find((todo) => (todo.id === todoID));
|
|
291
|
+
get({ get }) {
|
|
292
|
+
return get();
|
|
225
293
|
},
|
|
226
294
|
},
|
|
227
|
-
});
|
|
295
|
+
}, { emitOnFetch: true });
|
|
228
296
|
|
|
229
297
|
MyStore.todos.add({ todo: 'Do something!', id: 1 });
|
|
230
|
-
MyStore.todos.add({ todo: 'Do another thing!', id: 2 });
|
|
231
298
|
|
|
232
299
|
MyStore.on('fetchScope', ({ store, scopeName }) => {
|
|
233
|
-
console.log('scope fetched:
|
|
300
|
+
console.log('scope fetched:', scopeName);
|
|
234
301
|
});
|
|
235
302
|
|
|
236
303
|
MyStore.todos.get();
|
|
237
|
-
|
|
238
|
-
// output:
|
|
239
|
-
// scoped fetched: todos
|
|
304
|
+
// output: scope fetched: todos
|
|
240
305
|
```
|
|
241
306
|
|
|
242
307
|
## Async methods
|
|
@@ -244,7 +309,7 @@ MyStore.todos.get();
|
|
|
244
309
|
There is nothing in `seqda` preventing you from using async methods. The store will only update once `set` is called inside a method, and `set` won't be called until your asynchronous code is complete.
|
|
245
310
|
|
|
246
311
|
```javascript
|
|
247
|
-
|
|
312
|
+
import { createStore } from 'seqda';
|
|
248
313
|
|
|
249
314
|
const MyStore = createStore({
|
|
250
315
|
users: {
|
|
@@ -263,41 +328,117 @@ const MyStore = createStore({
|
|
|
263
328
|
},
|
|
264
329
|
});
|
|
265
330
|
|
|
266
|
-
let user = await MyStore.getUser(1);
|
|
331
|
+
let user = await MyStore.users.getUser(1);
|
|
267
332
|
```
|
|
268
333
|
|
|
269
|
-
Keep in mind that methods inside `seqda` are not asynchronous in nature, so the result of the above `getUser` call will cache the returned promise (not the resolved value of that promise). Now this shouldn't be an issue, because if you have an asynchronous method,
|
|
334
|
+
Keep in mind that methods inside `seqda` are not asynchronous in nature, so the result of the above `getUser` call will cache the returned promise (not the resolved value of that promise). Now this shouldn't be an issue, because if you have an asynchronous method, you will always be awaiting on the result, so the cached promise--if returned from cache--will provide the same result.
|
|
270
335
|
|
|
271
336
|
```javascript
|
|
272
337
|
// Caches the promise
|
|
273
|
-
let user = await MyStore.getUser(1);
|
|
338
|
+
let user = await MyStore.users.getUser(1);
|
|
274
339
|
|
|
275
340
|
// Returns the cached promise
|
|
276
|
-
user = await MyStore.getUser(1);
|
|
341
|
+
user = await MyStore.users.getUser(1);
|
|
277
342
|
|
|
278
343
|
// Result = same
|
|
279
344
|
```
|
|
280
345
|
|
|
281
|
-
##
|
|
346
|
+
## Performance
|
|
347
|
+
|
|
348
|
+
Unlike Redux, where dispatching an action recalculates the entire store, `seqda` only updates the specific scope (and its parent path) that was modified. Combined with per-method caching and batched update events, this makes `seqda` efficient for high-frequency updates.
|
|
349
|
+
|
|
350
|
+
The `'update'` event fires once per microtask tick after all synchronous writes settle. If you have UI components listening for store updates, they re-render once after the batch — not once per write.
|
|
351
|
+
|
|
352
|
+
## Cloning stores
|
|
353
|
+
|
|
354
|
+
You can clone a store with `cloneStore()`. Cloned stores are fully independent — mutations in the clone don't affect the original.
|
|
355
|
+
|
|
356
|
+
```javascript
|
|
357
|
+
import { createStore, cloneStore } from 'seqda';
|
|
358
|
+
|
|
359
|
+
const store = createStore({
|
|
360
|
+
todos: {
|
|
361
|
+
_: [],
|
|
362
|
+
add({ get, set }, todo) {
|
|
363
|
+
set([...get(), todo]);
|
|
364
|
+
},
|
|
365
|
+
get({ get }) {
|
|
366
|
+
return get();
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
store.todos.add({ id: 1, text: 'Original' });
|
|
372
|
+
|
|
373
|
+
// Mutable clone
|
|
374
|
+
let clone = cloneStore(store);
|
|
375
|
+
clone.todos.add({ id: 2, text: 'Clone only' });
|
|
376
|
+
|
|
377
|
+
console.log(store.todos.get().length); // 1
|
|
378
|
+
console.log(clone.todos.get().length); // 2
|
|
379
|
+
|
|
380
|
+
// Read-only clone (set() calls are silently ignored)
|
|
381
|
+
let snapshot = cloneStore(store, true);
|
|
382
|
+
snapshot.todos.add({ id: 3, text: 'Ignored' });
|
|
383
|
+
console.log(snapshot.todos.get().length); // 1
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Hydrating the store
|
|
387
|
+
|
|
388
|
+
To restore a store from a saved state, use `hydrate()`. This replaces the entire internal state atomically and emits an update with `modified: ['*']`.
|
|
282
389
|
|
|
283
|
-
|
|
390
|
+
```javascript
|
|
391
|
+
let savedState = JSON.stringify(MyStore.getState());
|
|
284
392
|
|
|
285
|
-
|
|
393
|
+
// Later...
|
|
394
|
+
MyStore.hydrate(JSON.parse(savedState));
|
|
395
|
+
```
|
|
286
396
|
|
|
287
|
-
|
|
397
|
+
`hydrate()` also invalidates all scope method caches, so any subsequent calls to cached methods will re-read from the new state.
|
|
288
398
|
|
|
289
399
|
## Middleware
|
|
290
400
|
|
|
291
401
|
Middleware is not currently supported, but I would be happy to add it (or to accept a PR) if anyone needs middleware.
|
|
292
402
|
|
|
293
|
-
##
|
|
403
|
+
## API Reference
|
|
294
404
|
|
|
295
|
-
|
|
405
|
+
### `createStore(template, options?)`
|
|
296
406
|
|
|
297
|
-
|
|
407
|
+
Creates a new seqda store.
|
|
298
408
|
|
|
299
|
-
|
|
409
|
+
- **`template`** — Object defining scopes. Each scope has a `_` default value and named methods.
|
|
410
|
+
- **`options.emitOnFetch`** — `boolean` (default: `false`). When `true`, emits `'fetchScope'` events on scope reads.
|
|
300
411
|
|
|
301
|
-
|
|
412
|
+
Returns the store instance (an `EventEmitter` with scope methods attached).
|
|
302
413
|
|
|
303
|
-
|
|
414
|
+
### Store instance
|
|
415
|
+
|
|
416
|
+
| Method/Property | Description |
|
|
417
|
+
|---|---|
|
|
418
|
+
| `store.getState()` | Returns the current frozen internal state object |
|
|
419
|
+
| `store.hydrate(state)` | Replaces entire state, emits update with `modified: ['*']` |
|
|
420
|
+
| `store.on(event, listener)` | Subscribe to events (inherited from EventEmitter) |
|
|
421
|
+
| `store.off(event, listener)` | Unsubscribe from events |
|
|
422
|
+
| `store.emit(event, data)` | Emit custom events |
|
|
423
|
+
|
|
424
|
+
### Scope method context
|
|
425
|
+
|
|
426
|
+
Every scope method receives a context object as its first argument:
|
|
427
|
+
|
|
428
|
+
| Property | Description |
|
|
429
|
+
|---|---|
|
|
430
|
+
| `get()` | Read the current state for this scope |
|
|
431
|
+
| `set(value)` | Write a new value for this scope (must be a different reference) |
|
|
432
|
+
| `store` | Reference to the root store — access other scopes |
|
|
433
|
+
|
|
434
|
+
### Events
|
|
435
|
+
|
|
436
|
+
| Event | Payload | Timing |
|
|
437
|
+
|---|---|---|
|
|
438
|
+
| `'update'` | `{ store, previousStore, modified }` | Async (next microtask), batched |
|
|
439
|
+
| `'fetchScope'` | `{ store, scopeName }` | Sync (immediate), opt-in |
|
|
440
|
+
| Custom events | User-defined | Sync (immediate) |
|
|
441
|
+
|
|
442
|
+
### `cloneStore(store, readOnly?)`
|
|
443
|
+
|
|
444
|
+
Creates a deep clone of the store. If `readOnly` is `true`, all `set()` calls are silently ignored.
|