seqda 1.0.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.
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/package.json +39 -0
- package/src/index.js +239 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Wyatt Greenway
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# `seqda` - Sequential Data Store
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
NPM:
|
|
6
|
+
```bash
|
|
7
|
+
npm i --save seqda
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Yarn:
|
|
11
|
+
```bash
|
|
12
|
+
yarn add seqda
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## About
|
|
16
|
+
|
|
17
|
+
`seqda` is a Redux-like global store. Unlike Redux, it doesn't take boiler-plate with the mass of a black-hole to setup, and has a much simpler interface.
|
|
18
|
+
|
|
19
|
+
There are no actions, dispatches, reducers, or selectors per-se. Instead, there are just methods: getters and setters that the user defines. **All** methods are cached, so calling the same method over and over again with the same state and the same argument will simply return the same previous cached result. If you need to invalidate the cache (i.e. on a setter, when you are for some reason continually providing the same input), simply add another randomized argument to invalidate the cache. The cache is always automatically invalidated for all methods in a scope when the state is updated (but only for the scope that had its state updated).
|
|
20
|
+
|
|
21
|
+
## Creating a data store
|
|
22
|
+
|
|
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
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
const { createStore } = require('seqda');
|
|
27
|
+
const MyStore = createStore({
|
|
28
|
+
todos: { // This is a "scope"
|
|
29
|
+
_: [], // This is the "default value" for this scope
|
|
30
|
+
|
|
31
|
+
// Then you simply define methods to interact with this data
|
|
32
|
+
add({ get, set }, todo /* ...args, as provided by the user */) {
|
|
33
|
+
// get = fetch the current data from
|
|
34
|
+
// the store for this scope
|
|
35
|
+
|
|
36
|
+
// set = update the data on the store
|
|
37
|
+
// for this scope
|
|
38
|
+
|
|
39
|
+
set([ ...get(), todo ]);
|
|
40
|
+
},
|
|
41
|
+
update({ get, set, store }, todoID, todo) {
|
|
42
|
+
let foundTodo = store.todos.get(todoID);
|
|
43
|
+
if (!foundTodo)
|
|
44
|
+
return;
|
|
45
|
+
|
|
46
|
+
let todos = get();
|
|
47
|
+
if (!todos)
|
|
48
|
+
return;
|
|
49
|
+
|
|
50
|
+
let index = todos.findIndex((todo) => (todo === foundTodo));
|
|
51
|
+
if (index < 0)
|
|
52
|
+
return;
|
|
53
|
+
|
|
54
|
+
todos = todos.slice();
|
|
55
|
+
todos[index] = todo;
|
|
56
|
+
|
|
57
|
+
set(todos);
|
|
58
|
+
},
|
|
59
|
+
remove({ get, set }, todo) {
|
|
60
|
+
set(get().filter((item) => (item !== todo)));
|
|
61
|
+
},
|
|
62
|
+
get({ get }, todoID) {
|
|
63
|
+
if (arguments.length === 1)
|
|
64
|
+
return get();
|
|
65
|
+
|
|
66
|
+
return get().find((todo) => (todo.id === todoID));
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
// Define another scope
|
|
70
|
+
config: {
|
|
71
|
+
_: {
|
|
72
|
+
configValue1: null,
|
|
73
|
+
configValue2: null,
|
|
74
|
+
},
|
|
75
|
+
// You can also define sub-scopes
|
|
76
|
+
userConfig: {
|
|
77
|
+
_: {
|
|
78
|
+
firstName: '',
|
|
79
|
+
lastName: '',
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// Methods go here
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Methods go here
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// We can add a todo by calling our method
|
|
90
|
+
// (notice that the "context") argments ({ get, set })
|
|
91
|
+
// are provided internally by seqda
|
|
92
|
+
|
|
93
|
+
MyStore.todos.add(/* todo */ { todo: 'Do things!', id: 1 });
|
|
94
|
+
|
|
95
|
+
console.log(MyStore.getState());
|
|
96
|
+
|
|
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
|
+
```
|
|
111
|
+
|
|
112
|
+
## The entire store is frozen
|
|
113
|
+
|
|
114
|
+
In `seqda`, all values in the store are always frozen with `Object.freeze`. This ensures that your store stays immutable, and can only be updated with the provided scope methods. For example, if you try to set something directly on the returned state, it will fail:
|
|
115
|
+
|
|
116
|
+
```javascript
|
|
117
|
+
let state = MyStore.getState();
|
|
118
|
+
state.setSomething = toAValue;
|
|
119
|
+
|
|
120
|
+
// throw new TypeError('Cannot add property setSomething, object is not extensible');
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Method cache
|
|
124
|
+
|
|
125
|
+
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
|
+
|
|
127
|
+
The cache is invalidated as soon as 1) the internal state for a scope is updated, or 2) the arguments to the method call change.
|
|
128
|
+
|
|
129
|
+
Let's see an example of this in action:
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
const { createStore } = require('seqda');
|
|
133
|
+
|
|
134
|
+
const MyStore = createStore({
|
|
135
|
+
citizens: {
|
|
136
|
+
_: [],
|
|
137
|
+
getByState({ get }, shortStateName) {
|
|
138
|
+
return get().filter((citizen) => (citizen.state === shortStateName));
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
states: {
|
|
142
|
+
_: [],
|
|
143
|
+
get({ get }, stateName) {
|
|
144
|
+
if (!stateName)
|
|
145
|
+
return get(); // if no stateName was provided, then return all states
|
|
146
|
+
|
|
147
|
+
return get().find((state) => (state.name === stateName));
|
|
148
|
+
},
|
|
149
|
+
getCitizensForState({ get, store }, stateName) {
|
|
150
|
+
// First, get the state requested from the store
|
|
151
|
+
let state = store.states.get(stateName);
|
|
152
|
+
|
|
153
|
+
// Next get the citizens for this state
|
|
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.
|
|
157
|
+
let citizens = store.citizens.getByState(state.shortName);
|
|
158
|
+
return citizens;
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Update events
|
|
165
|
+
|
|
166
|
+
`seqda` emits an `'update'` event when the store has been updated. Unlike Redux, the `'update'` event is only triggered on the *next frame* in the Javascript engine (essentially on `nextTick`). The update event also reports which areas of the store have been updated, unlike Redux. This allows many store updates to happen sequentially, with the update event only being fired once. Let's see an example of this in action.
|
|
167
|
+
|
|
168
|
+
*Note: When the scope name of the update event is `'*'`, this means that the entire store has been updated. This can happen for example when the store is hydrated with a `.hydrate` call.*
|
|
169
|
+
|
|
170
|
+
```javascript
|
|
171
|
+
const { createStore } = require('seqda');
|
|
172
|
+
const MyStore = createStore({
|
|
173
|
+
todos: {
|
|
174
|
+
_: [],
|
|
175
|
+
add({ get, set }, todo) {
|
|
176
|
+
set([ ...get(), todo ]);
|
|
177
|
+
},
|
|
178
|
+
remove({ get, set }, todo) {
|
|
179
|
+
set(get().filter((item) => (item !== todo)));
|
|
180
|
+
},
|
|
181
|
+
get({ get }, todoID) {
|
|
182
|
+
if (arguments.length === 1)
|
|
183
|
+
return get();
|
|
184
|
+
|
|
185
|
+
return get().find((todo) => (todo.id === todoID));
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
MyStore.on('update', ({ store, modified }) => {
|
|
191
|
+
// I am called on `nextTick`, frame 2
|
|
192
|
+
console.log('modified: ', modified);
|
|
193
|
+
|
|
194
|
+
// modified: [ 'todos' ]
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// frame 1
|
|
198
|
+
MyStore.todos.add({ todo: 'Do something!', id: 1 });
|
|
199
|
+
MyStore.todos.add({ todo: 'Do another thing!', id: 2 });
|
|
200
|
+
|
|
201
|
+
//... now onto frame2, where the "update" event is fired
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Fetch events
|
|
205
|
+
|
|
206
|
+
`seqda` also reports which scopes are being fetched. Simply listen for the "fetchScope" event to know which areas of the store have been accessed for any given operation.
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
const { createStore } = require('seqda');
|
|
210
|
+
|
|
211
|
+
const MyStore = createStore({
|
|
212
|
+
todos: {
|
|
213
|
+
_: [],
|
|
214
|
+
add({ get, set }, todo) {
|
|
215
|
+
set([ ...get(), todo ]);
|
|
216
|
+
},
|
|
217
|
+
remove({ get, set }, todo) {
|
|
218
|
+
set(get().filter((item) => (item !== todo)));
|
|
219
|
+
},
|
|
220
|
+
get({ get }, todoID) {
|
|
221
|
+
if (arguments.length === 1)
|
|
222
|
+
return get();
|
|
223
|
+
|
|
224
|
+
return get().find((todo) => (todo.id === todoID));
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
MyStore.todos.add({ todo: 'Do something!', id: 1 });
|
|
230
|
+
MyStore.todos.add({ todo: 'Do another thing!', id: 2 });
|
|
231
|
+
|
|
232
|
+
MyStore.on('fetchScope', ({ store, scopeName }) => {
|
|
233
|
+
console.log('scope fetched: ', scopeName);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
MyStore.todos.get();
|
|
237
|
+
|
|
238
|
+
// output:
|
|
239
|
+
// scoped fetched: todos
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Async methods
|
|
243
|
+
|
|
244
|
+
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
|
+
|
|
246
|
+
```javascript
|
|
247
|
+
const { createStore } = require('seqda');
|
|
248
|
+
|
|
249
|
+
const MyStore = createStore({
|
|
250
|
+
users: {
|
|
251
|
+
_: [],
|
|
252
|
+
async getUser({ get, set }, userID) {
|
|
253
|
+
let users = get();
|
|
254
|
+
let user = users[userID];
|
|
255
|
+
|
|
256
|
+
if (!user) {
|
|
257
|
+
user = await API.getUserByID(userID);
|
|
258
|
+
set({ ...users, [user.id]: user });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return user;
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
let user = await MyStore.getUser(1);
|
|
267
|
+
```
|
|
268
|
+
|
|
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, so you will always be awaiting on the result, so the cached promise--if returned from cache--will provide the same result.
|
|
270
|
+
|
|
271
|
+
```javascript
|
|
272
|
+
// Caches the promise
|
|
273
|
+
let user = await MyStore.getUser(1);
|
|
274
|
+
|
|
275
|
+
// Returns the cached promise
|
|
276
|
+
user = await MyStore.getUser(1);
|
|
277
|
+
|
|
278
|
+
// Result = same
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Seqda is built for speed an simplicity
|
|
282
|
+
|
|
283
|
+
Unlike in Redux, where when an action is dispatched, the entire store is recalculated (which can be very heavy for large stores), `seqda` will only update the specific area of the store (and all its parent scopes) that was requested to be updated. This means that for each update operation, only the updated scope (and all parent) scopes are updated, making it much more efficient than Redux.
|
|
284
|
+
|
|
285
|
+
Also, as already mentioned, the `seqda` `'update'` event is only fired once after all the store updates have settled (on `nextTick` after updates). This is also for efficiency purposes. For example, if you have client components (i.e. React) listening for store updates, then unlike Redux, where the component's state will potentially update dozens of times for a single store dispatch, in `seqda` all your components will only update their state once after the store has settled.
|
|
286
|
+
|
|
287
|
+
The `seqda` interface was designed to be simple and intuitive. All you really need to understand is scopes, and how to interact with them via `get`, `set`, and fetching other values from the store via `store`. The rest is up to you! Feel free to create complicated caching selectors, or any other useful tools you want. You just need to provide methods to interact with your scopes, and the rest is left to your creative freedom!
|
|
288
|
+
|
|
289
|
+
## Middleware
|
|
290
|
+
|
|
291
|
+
Middleware is not currently supported, but I would be happy to add it (or to accept a PR) if anyone needs middleware.
|
|
292
|
+
|
|
293
|
+
## Hydrating the store from a stored state
|
|
294
|
+
|
|
295
|
+
To hydrate your store from a stored state, simply call `MyStore.hydrate(storedState)`.
|
|
296
|
+
|
|
297
|
+
```javascript
|
|
298
|
+
|
|
299
|
+
let storedState = JSON.stringify(MyStore.getState());
|
|
300
|
+
|
|
301
|
+
MyStore.hydrate(JSON.parse(storedState));
|
|
302
|
+
|
|
303
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "seqda",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Sequential Data Store",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node ./node_modules/.bin/jasmine",
|
|
9
|
+
"test-debug": "node --inspect-brk ./node_modules/.bin/jasmine",
|
|
10
|
+
"test-watch": "watch 'clear ; node ./node_modules/.bin/jasmine' . --wait=2 --interval=1"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/th317erd/seqda.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"redux",
|
|
18
|
+
"store",
|
|
19
|
+
"data",
|
|
20
|
+
"react-state",
|
|
21
|
+
"vuex",
|
|
22
|
+
"state"
|
|
23
|
+
],
|
|
24
|
+
"author": "Wyatt Greenway",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/th317erd/seqda/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/th317erd/seqda#readme",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@spothero/eslint-plugin-spothero": "github:spothero/eslint-plugin-spothero",
|
|
32
|
+
"eslint": "^8.18.0",
|
|
33
|
+
"jasmine": "^4.2.1"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"events": "^3.3.0",
|
|
37
|
+
"nife": "^1.8.1"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Nife = require('nife');
|
|
4
|
+
const EventEmitter = require('events');
|
|
5
|
+
|
|
6
|
+
const QUEUE_CHANGE_EVENT = Symbol.for('__seqdaQueueChangeEvent');
|
|
7
|
+
const QUEUE_CHANGE_INFO = Symbol.for('__seqdaQueueChangeInfo');
|
|
8
|
+
const INTERNAL_STATE = Symbol.for('__seqdaInternalState');
|
|
9
|
+
|
|
10
|
+
function queueChangeEvent(path) {
|
|
11
|
+
let info = this[QUEUE_CHANGE_INFO];
|
|
12
|
+
if (!info.promise) {
|
|
13
|
+
info.promise = Promise.resolve();
|
|
14
|
+
info.promise.then(() => {
|
|
15
|
+
this.emit('update', { store: this, modified: Array.from(Object.keys(info.eventQueue)) });
|
|
16
|
+
info.eventQueue = {};
|
|
17
|
+
info.promise = null;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!info.eventQueue)
|
|
22
|
+
info.eventQueue = {};
|
|
23
|
+
|
|
24
|
+
info.eventQueue[path] = true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function copyKeysToArray(_value, source) {
|
|
28
|
+
let value = _value;
|
|
29
|
+
|
|
30
|
+
if (Array.isArray(value) && source) {
|
|
31
|
+
let keys = Object.keys(source);
|
|
32
|
+
for (let i = 0, il = keys.length; i < il; i++) {
|
|
33
|
+
let key = keys[i];
|
|
34
|
+
if ((/^\d+$/).test(key))
|
|
35
|
+
continue;
|
|
36
|
+
|
|
37
|
+
value[key] = source[key];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function clone(value) {
|
|
45
|
+
if (!value)
|
|
46
|
+
return value;
|
|
47
|
+
|
|
48
|
+
if (value && typeof value === 'object') {
|
|
49
|
+
if (Array.isArray(value))
|
|
50
|
+
return copyKeysToArray(value.slice(), value);
|
|
51
|
+
|
|
52
|
+
return Object.assign({}, value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function setPath(_context, path, value) {
|
|
59
|
+
let context = clone(_context);
|
|
60
|
+
let pathParts = path.split('.');
|
|
61
|
+
let current = context;
|
|
62
|
+
|
|
63
|
+
for (let i = 0, il = pathParts.length; i < il; i++) {
|
|
64
|
+
let pathPart = pathParts[i];
|
|
65
|
+
|
|
66
|
+
if ((i + 1) >= il) {
|
|
67
|
+
let finalValue;
|
|
68
|
+
|
|
69
|
+
if (Array.isArray(value))
|
|
70
|
+
finalValue = copyKeysToArray(value, current[pathPart]);
|
|
71
|
+
else
|
|
72
|
+
finalValue = value;
|
|
73
|
+
|
|
74
|
+
if (finalValue && typeof finalValue === 'object')
|
|
75
|
+
Object.freeze(finalValue);
|
|
76
|
+
|
|
77
|
+
current[pathPart] = finalValue;
|
|
78
|
+
} else {
|
|
79
|
+
current[pathPart] = clone(current[pathPart]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (current && typeof current === 'object')
|
|
83
|
+
Object.freeze(current);
|
|
84
|
+
|
|
85
|
+
current = current[pathPart];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return context;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getPath(...parts) {
|
|
92
|
+
return parts.filter(Boolean).join('.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function createStoreSubsection(store, sectionTemplate, path) {
|
|
96
|
+
const isCacheInvalid = (scopeName, args) => {
|
|
97
|
+
let thisCache = cache[scopeName];
|
|
98
|
+
if (!thisCache)
|
|
99
|
+
return true;
|
|
100
|
+
|
|
101
|
+
let cacheArgs = thisCache.args;
|
|
102
|
+
if (cacheArgs.length !== args.length)
|
|
103
|
+
return true;
|
|
104
|
+
|
|
105
|
+
for (let i = 0, il = args.length; i < il; i++) {
|
|
106
|
+
if (cacheArgs[i] !== args[i])
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return false;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const setCache = (scopeName, args, result) => {
|
|
114
|
+
cache[scopeName] = {
|
|
115
|
+
args,
|
|
116
|
+
result,
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const createScopeMethod = (scopeName, func) => {
|
|
121
|
+
return (...args) => {
|
|
122
|
+
if (isCacheInvalid(scopeName, args)) {
|
|
123
|
+
let result = func({ get, set, store }, ...args);
|
|
124
|
+
setCache(scopeName, args, result);
|
|
125
|
+
return result;
|
|
126
|
+
} else {
|
|
127
|
+
return cache[scopeName].result;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const get = () => {
|
|
133
|
+
store.emit('fetchScope', { store, scopeName: path });
|
|
134
|
+
let currentState = Nife.get(store[INTERNAL_STATE], path);
|
|
135
|
+
return currentState;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const set = (value) => {
|
|
139
|
+
let currentState = Nife.get(store[INTERNAL_STATE], path);
|
|
140
|
+
if (value && typeof value === 'object' && value === currentState)
|
|
141
|
+
throw new Error(`Error: "${getPath(path)}" the state value is the same, but it is required to be different.`);
|
|
142
|
+
|
|
143
|
+
let previousState = currentState;
|
|
144
|
+
store[INTERNAL_STATE] = setPath(store[INTERNAL_STATE], path, value);
|
|
145
|
+
|
|
146
|
+
cache = {};
|
|
147
|
+
|
|
148
|
+
if (store[QUEUE_CHANGE_EVENT])
|
|
149
|
+
store[QUEUE_CHANGE_EVENT](path, value, previousState);
|
|
150
|
+
|
|
151
|
+
return value;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
if (path && !Object.prototype.hasOwnProperty.call(sectionTemplate, '_'))
|
|
155
|
+
throw new Error(`Error: "${getPath}._" default value must be defined.`);
|
|
156
|
+
|
|
157
|
+
const scope = (!path) ? store : {};
|
|
158
|
+
let keys = Object.keys(sectionTemplate || {});
|
|
159
|
+
let subScopes = [];
|
|
160
|
+
let cache = {};
|
|
161
|
+
|
|
162
|
+
if (path)
|
|
163
|
+
set(clone(sectionTemplate._));
|
|
164
|
+
|
|
165
|
+
for (let i = 0, il = keys.length; i < il; i++) {
|
|
166
|
+
let key = keys[i];
|
|
167
|
+
if (key === '_')
|
|
168
|
+
continue;
|
|
169
|
+
|
|
170
|
+
let value = sectionTemplate[key];
|
|
171
|
+
if (Nife.instanceOf(value, 'object')) {
|
|
172
|
+
scope[key] = createStoreSubsection(store, value, getPath(path, key));
|
|
173
|
+
subScopes.push(key);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (typeof value !== 'function')
|
|
178
|
+
throw new TypeError(`Error: Value of "${getPath(path, key)}" is invalid. All properties must be functions, or sub scopes.`);
|
|
179
|
+
|
|
180
|
+
scope[key] = createScopeMethod(key, value);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!path)
|
|
184
|
+
return scope; // We can't freeze the store
|
|
185
|
+
else
|
|
186
|
+
return Object.freeze(scope);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function createStore(template) {
|
|
190
|
+
if (!Nife.instanceOf(template, 'object'))
|
|
191
|
+
throw new TypeError('createStore: provided "template" must be an object.');
|
|
192
|
+
|
|
193
|
+
const store = new EventEmitter();
|
|
194
|
+
|
|
195
|
+
Object.defineProperty(store, INTERNAL_STATE, {
|
|
196
|
+
writable: true,
|
|
197
|
+
enumerable: false,
|
|
198
|
+
configurable: true,
|
|
199
|
+
value: {},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
let constructedStore = createStoreSubsection(store, template);
|
|
203
|
+
|
|
204
|
+
Object.defineProperties(constructedStore, {
|
|
205
|
+
'getState': {
|
|
206
|
+
writable: false,
|
|
207
|
+
enumberable: false,
|
|
208
|
+
configurable: false,
|
|
209
|
+
value: () => constructedStore[INTERNAL_STATE],
|
|
210
|
+
},
|
|
211
|
+
'hydrate': {
|
|
212
|
+
writable: false,
|
|
213
|
+
enumberable: false,
|
|
214
|
+
configurable: false,
|
|
215
|
+
value: (value) => {
|
|
216
|
+
constructedStore[INTERNAL_STATE] = Object.freeze(clone(value));
|
|
217
|
+
queueChangeEvent.call(constructedStore, '*');
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
[QUEUE_CHANGE_EVENT]: {
|
|
221
|
+
writable: false,
|
|
222
|
+
enumberable: false,
|
|
223
|
+
configurable: false,
|
|
224
|
+
value: queueChangeEvent.bind(constructedStore),
|
|
225
|
+
},
|
|
226
|
+
[QUEUE_CHANGE_INFO]: {
|
|
227
|
+
writable: true,
|
|
228
|
+
enumberable: false,
|
|
229
|
+
configurable: false,
|
|
230
|
+
value: {},
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return constructedStore;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = {
|
|
238
|
+
createStore,
|
|
239
|
+
};
|