@figliolia/galena 2.3.5 → 3.0.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/README.md +172 -476
- package/dist/Galena.cjs +102 -0
- package/dist/Galena.d.cts +80 -0
- package/dist/Galena.d.cts.map +1 -0
- package/dist/Galena.d.mts +80 -0
- package/dist/Galena.d.mts.map +1 -0
- package/dist/Galena.mjs +103 -0
- package/dist/Galena.mjs.map +1 -0
- package/dist/GalenaProvider.cjs +50 -0
- package/dist/GalenaProvider.d.cts +35 -0
- package/dist/GalenaProvider.d.cts.map +1 -0
- package/dist/GalenaProvider.d.mts +35 -0
- package/dist/GalenaProvider.d.mts.map +1 -0
- package/dist/GalenaProvider.mjs +52 -0
- package/dist/GalenaProvider.mjs.map +1 -0
- package/dist/Logger.cjs +46 -0
- package/dist/Logger.d.cts +35 -0
- package/dist/Logger.d.cts.map +1 -0
- package/dist/Logger.d.mts +35 -0
- package/dist/Logger.d.mts.map +1 -0
- package/dist/Logger.mjs +48 -0
- package/dist/Logger.mjs.map +1 -0
- package/dist/Middleware.cjs +62 -0
- package/dist/Middleware.d.cts +65 -0
- package/dist/Middleware.d.cts.map +1 -0
- package/dist/Middleware.d.mts +65 -0
- package/dist/Middleware.d.mts.map +1 -0
- package/dist/Middleware.mjs +64 -0
- package/dist/Middleware.mjs.map +1 -0
- package/dist/Profiler.cjs +41 -0
- package/dist/Profiler.d.cts +31 -0
- package/dist/Profiler.d.cts.map +1 -0
- package/dist/Profiler.d.mts +31 -0
- package/dist/Profiler.d.mts.map +1 -0
- package/dist/Profiler.mjs +43 -0
- package/dist/Profiler.mjs.map +1 -0
- package/dist/State.cjs +147 -0
- package/dist/State.d.cts +114 -0
- package/dist/State.d.cts.map +1 -0
- package/dist/State.d.mts +114 -0
- package/dist/State.d.mts.map +1 -0
- package/dist/State.mjs +148 -0
- package/dist/State.mjs.map +1 -0
- package/dist/commonHooks.cjs +17 -0
- package/dist/commonHooks.mjs +18 -0
- package/dist/commonHooks.mjs.map +1 -0
- package/dist/connect-multi.cjs +42 -0
- package/dist/connect-multi.d.cts +10 -0
- package/dist/connect-multi.d.cts.map +1 -0
- package/dist/connect-multi.d.mts +10 -0
- package/dist/connect-multi.d.mts.map +1 -0
- package/dist/connect-multi.mjs +44 -0
- package/dist/connect-multi.mjs.map +1 -0
- package/dist/connect.cjs +41 -0
- package/dist/connect.d.cts +9 -0
- package/dist/connect.d.cts.map +1 -0
- package/dist/connect.d.mts +9 -0
- package/dist/connect.d.mts.map +1 -0
- package/dist/connect.mjs +43 -0
- package/dist/connect.mjs.map +1 -0
- package/dist/createUseState.cjs +47 -0
- package/dist/createUseState.d.cts +46 -0
- package/dist/createUseState.d.cts.map +1 -0
- package/dist/createUseState.d.mts +46 -0
- package/dist/createUseState.d.mts.map +1 -0
- package/dist/createUseState.mjs +49 -0
- package/dist/createUseState.mjs.map +1 -0
- package/dist/index.cjs +23 -0
- package/dist/index.d.cts +12 -0
- package/dist/index.d.mts +12 -0
- package/dist/index.mjs +11 -0
- package/dist/types.d.cts +23 -0
- package/dist/types.d.cts.map +1 -0
- package/dist/types.d.mts +23 -0
- package/dist/types.d.mts.map +1 -0
- package/dist/useState.cjs +45 -0
- package/dist/useState.d.cts +43 -0
- package/dist/useState.d.cts.map +1 -0
- package/dist/useState.d.mts +43 -0
- package/dist/useState.d.mts.map +1 -0
- package/dist/useState.mjs +47 -0
- package/dist/useState.mjs.map +1 -0
- package/media/Logging.png +0 -0
- package/media/Profiling.png +0 -0
- package/package.json +38 -59
- package/src/Galena.ts +120 -0
- package/src/{Middlewares/Logger.ts → Logger.ts} +15 -14
- package/src/Middleware.ts +62 -0
- package/src/Profiler.ts +53 -0
- package/src/State.ts +167 -0
- package/src/index.ts +6 -3
- package/src/types.ts +28 -0
- package/dist/cjs/Galena/Galena.js +0 -223
- package/dist/cjs/Galena/Guards.js +0 -40
- package/dist/cjs/Galena/Scheduler.js +0 -84
- package/dist/cjs/Galena/State.js +0 -314
- package/dist/cjs/Galena/index.js +0 -22
- package/dist/cjs/Galena/types.js +0 -9
- package/dist/cjs/Middleware/Middleware.js +0 -46
- package/dist/cjs/Middleware/index.js +0 -20
- package/dist/cjs/Middleware/types.js +0 -8
- package/dist/cjs/Middlewares/Logger.js +0 -51
- package/dist/cjs/Middlewares/Profiler.js +0 -38
- package/dist/cjs/Middlewares/index.js +0 -7
- package/dist/cjs/index.js +0 -19
- package/dist/cjs/package.json +0 -3
- package/dist/mjs/Galena/Galena.js +0 -218
- package/dist/mjs/Galena/Guards.js +0 -36
- package/dist/mjs/Galena/Scheduler.js +0 -79
- package/dist/mjs/Galena/State.js +0 -313
- package/dist/mjs/Galena/index.js +0 -3
- package/dist/mjs/Galena/types.js +0 -6
- package/dist/mjs/Middleware/Middleware.js +0 -42
- package/dist/mjs/Middleware/index.js +0 -2
- package/dist/mjs/Middleware/types.js +0 -5
- package/dist/mjs/Middlewares/Logger.js +0 -44
- package/dist/mjs/Middlewares/Profiler.js +0 -35
- package/dist/mjs/Middlewares/index.js +0 -2
- package/dist/mjs/index.js +0 -3
- package/dist/mjs/package.json +0 -4
- package/dist/types/Galena/Galena.d.ts +0 -160
- package/dist/types/Galena/Guards.d.ts +0 -29
- package/dist/types/Galena/Scheduler.d.ts +0 -51
- package/dist/types/Galena/State.d.ts +0 -235
- package/dist/types/Galena/index.d.ts +0 -3
- package/dist/types/Galena/types.d.ts +0 -13
- package/dist/types/Middleware/Middleware.d.ts +0 -43
- package/dist/types/Middleware/index.d.ts +0 -2
- package/dist/types/Middleware/types.d.ts +0 -4
- package/dist/types/Middlewares/Logger.d.ts +0 -27
- package/dist/types/Middlewares/Profiler.d.ts +0 -22
- package/dist/types/Middlewares/index.d.ts +0 -2
- package/dist/types/index.d.ts +0 -3
- package/src/Galena/Galena.ts +0 -252
- package/src/Galena/Guards.ts +0 -49
- package/src/Galena/Scheduler.ts +0 -85
- package/src/Galena/State.ts +0 -344
- package/src/Galena/index.ts +0 -3
- package/src/Galena/types.ts +0 -18
- package/src/Middleware/Middleware.ts +0 -45
- package/src/Middleware/index.ts +0 -2
- package/src/Middleware/types.ts +0 -4
- package/src/Middlewares/Profiler.ts +0 -41
- package/src/Middlewares/index.ts +0 -2
package/README.md
CHANGED
|
@@ -1,555 +1,251 @@
|
|
|
1
1
|
# Galena
|
|
2
|
-
Lightning fast platform agnostic state! Galena is a one-stop-shop for creating reactive state that supports your global application or individually isolated features. Galena operates on the premise of pub-sub and mutability allowing for significantly higher performance over more traditional state management utilities.
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
Lightning fast, framework agnostic state, that doesn't glue your state operations to your UI components!
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
Galena was originally designed to manage game state in TypeScript application with over 1000 stateful values. Existing state management utilities such as Zustand and Redux became cumbersome when modeling a huge amount of state and operations.
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install --save @figliolia/galena
|
|
11
|
-
# or
|
|
12
|
-
yarn add @figliolia/galena
|
|
13
|
-
```
|
|
7
|
+
With Redux, the action-driven model became a juggling act of action names and tracing changes between them. With Zustand, the `create()` function felt limiting in the usage of JavaScript language constructs. It also lacked a construct for global application state.
|
|
14
8
|
|
|
15
|
-
##
|
|
16
|
-
### Global Application State Architecture
|
|
17
|
-
Creating a "global" application state begins with initializing a `Galena` instance. Your `Galena` instances act as a container from which units of state can be composed:
|
|
18
|
-
|
|
19
|
-
```typescript
|
|
20
|
-
// AppState.ts
|
|
21
|
-
import { Galena, Logger, Profiler } from "@figliolia/galena";
|
|
22
|
-
import type { Middleware } from "@figliolia/galena";
|
|
23
|
-
|
|
24
|
-
const middleware: Middleware[] = [];
|
|
25
|
-
|
|
26
|
-
if(process.env.NODE_ENV === "development") {
|
|
27
|
-
middleware.push(new Logger(), new Profiler())
|
|
28
|
-
}
|
|
9
|
+
## Installation
|
|
29
10
|
|
|
30
|
-
// Initialize Galena State!
|
|
31
|
-
export const AppState = new Galena(middleware);
|
|
32
11
|
```
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
// NavigationState.ts
|
|
38
|
-
import { AppState } from "./AppState.ts";
|
|
39
|
-
|
|
40
|
-
// Compose a unit of state attached to your Galena Instance
|
|
41
|
-
export const NavigationState = AppState.composeState("navigation", {
|
|
42
|
-
// initial state
|
|
43
|
-
currentRoute: "/",
|
|
44
|
-
userID: "123",
|
|
45
|
-
permittedRoutes: ["**/*"]
|
|
46
|
-
});
|
|
12
|
+
npm i @figliolia/galena
|
|
13
|
+
# with react
|
|
14
|
+
npm i @figliolia/react-galena
|
|
47
15
|
```
|
|
48
16
|
|
|
49
|
-
|
|
17
|
+
## Basic Usage
|
|
50
18
|
|
|
51
|
-
|
|
19
|
+
### The State Model
|
|
52
20
|
|
|
53
|
-
|
|
54
|
-
// BusinessLogic.ts
|
|
55
|
-
import { AppState } from "./AppState.ts";
|
|
56
|
-
|
|
57
|
-
const subscription = AppState.subscribe("navigation", state => {
|
|
58
|
-
// React to changes to Navigation state
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// Set Navigation State!
|
|
62
|
-
AppState.update("navigation", state => {
|
|
63
|
-
state.currentRoute = "/contact-us";
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Clean up subscriptions
|
|
67
|
-
AppState.unsubscribe("navigation", subscription);
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
#### State Operations Using Your Unit of State
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
|
-
// BusinessLogic.ts
|
|
74
|
-
import { NavigationState } from "./NavigationState";
|
|
75
|
-
|
|
76
|
-
const subscription = NavigationState.subscribe(state => {
|
|
77
|
-
// React to changes to Navigation state
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// Set Navigation State!
|
|
81
|
-
NavigationState.update(state => {
|
|
82
|
-
state.currentRoute = "/contact-us";
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// Clean up subscriptions
|
|
86
|
-
NavigationState.unsubscribe(subscription);
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
Running mutations on individual units of state will automatically update your `Galena` instance's state! Your `Galena` instance will internally track changes to each unit of state that it composes.
|
|
90
|
-
|
|
91
|
-
### Isolated State Architecture
|
|
92
|
-
|
|
93
|
-
You may also create units of state that are *not* connected to a "global" `Galena` instance. To promote flexibility for developers to organize their state however they wish, `Galena` exports its `State` object for usage directly:
|
|
21
|
+
The instancable `State` object in Galena is easy to get started with and setup. It's effectively reactive wrapper around any value passed into it.
|
|
94
22
|
|
|
95
23
|
```typescript
|
|
96
24
|
import { State } from "@figliolia/galena";
|
|
97
25
|
|
|
98
|
-
|
|
99
|
-
const FeatureState = new State("myFeature", {
|
|
100
|
-
// initial state
|
|
101
|
-
list: [1, 2, 3];
|
|
102
|
-
});
|
|
26
|
+
const MyState = new State(/* any value */, /* middleware */);
|
|
103
27
|
|
|
104
|
-
const subscription =
|
|
105
|
-
//
|
|
28
|
+
const subscription = MyState.subscribe(value => {
|
|
29
|
+
// do something with changed values
|
|
106
30
|
});
|
|
107
31
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
state.list = [...state.list, state.list.length];
|
|
111
|
-
});
|
|
32
|
+
// to unsubscribe
|
|
33
|
+
subscription();
|
|
112
34
|
|
|
113
|
-
|
|
114
|
-
|
|
35
|
+
MyState.set(/* new value */);
|
|
36
|
+
MyState.update(previousValue => /* new value */);
|
|
37
|
+
MyState.reset(); // reset state back to the initial value
|
|
115
38
|
```
|
|
116
39
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
## API Reference
|
|
120
|
-
### Galena
|
|
121
|
-
Instances of `Galena` behave as a container for one or more units of `State`. Your `Galena` instance is designed to replicate the "global" application state pattern, but without the overhead of
|
|
122
|
-
1. Declaring all of your units of state early in your application lifecycle
|
|
123
|
-
2. Making complex mutations to large state objects.
|
|
40
|
+
Instances of `State` are ultimately what compose all reactivity in Galena. They can exist as islands compose larger stateful model.
|
|
124
41
|
|
|
125
|
-
|
|
42
|
+
### The Galena Model
|
|
126
43
|
|
|
127
|
-
|
|
44
|
+
Creating Global application state(s) in Galena is simple. They are effectively just connected instances of your `State`'s.
|
|
128
45
|
|
|
129
46
|
```typescript
|
|
130
|
-
import { Galena,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
* Creates a unit of `State` connected to your `Galena` instance.
|
|
147
|
-
* Returns a unit of `State`
|
|
148
|
-
*/
|
|
149
|
-
AppState.composeState("nameOfState" /* unique name */, /* initial state */, /* Optional Model */);
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Get
|
|
153
|
-
*
|
|
154
|
-
* Returns a connected unit of `State` by name
|
|
155
|
-
*/
|
|
156
|
-
AppState.get("nameOfState");
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Update
|
|
160
|
-
*
|
|
161
|
-
* Mutates a unit of state by name. This method by default
|
|
162
|
-
* uses internal batching in order to optimize the dispatching
|
|
163
|
-
* of state changes to consumers. This method is the most
|
|
164
|
-
* performant way to mutate state in Galena!
|
|
165
|
-
*/
|
|
166
|
-
AppState.update("nameOfState", (state) => {});
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Background Update
|
|
170
|
-
*
|
|
171
|
-
* Runs a higher-priority mutation on a unit of state. This method
|
|
172
|
-
* will bypass batching in favor of a scheduled propagation of
|
|
173
|
-
* changes to subscribers of your state. This method is great for
|
|
174
|
-
* prioritizing state updates driven by frequent user-input such
|
|
175
|
-
* as typing into a form or game logic.
|
|
176
|
-
*/
|
|
177
|
-
AppState.backgroundUpdate("nameOfState", (state) => {});
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Priority Update
|
|
181
|
-
*
|
|
182
|
-
* Runs a highest-priority mutation on a unit of state. This method
|
|
183
|
-
* bypasses all batching and scheduling optimizations in Galena.
|
|
184
|
-
* When using `priorityUpdate()` your state changes are immediately
|
|
185
|
-
* propagated to your state subscribers ahead of all scheduled and
|
|
186
|
-
* batched updates. This method is great for usage with external
|
|
187
|
-
* scheduling mechanisms such as `requestAnimationFrame`, `intervals`,
|
|
188
|
-
* and/or `timeouts`
|
|
189
|
-
*/
|
|
190
|
-
AppState.priorityUpdate("nameOfState", (state) => {});
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Subscribe
|
|
194
|
-
*
|
|
195
|
-
* Registers a subscription on a unit of state
|
|
196
|
-
*/
|
|
197
|
-
const subscription = AppState.subscribe("nameOfState", (state) => {});
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Unsubscribe
|
|
201
|
-
*
|
|
202
|
-
* Closes an open subscription given a subscription ID
|
|
203
|
-
* returned by `new Galena().subscribe()`
|
|
204
|
-
*/
|
|
205
|
-
AppState.unsubscribe(subscription);
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Subscribe All
|
|
209
|
-
*
|
|
210
|
-
* Registers a global subscription on each State registered to
|
|
211
|
-
* your Galena instance. Your callback will be invoked any
|
|
212
|
-
* time a unit of state is updated
|
|
213
|
-
*/
|
|
214
|
-
const subscription = AppState.subscribeAll(appState => {});
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Unsubscribe All
|
|
218
|
-
*
|
|
219
|
-
* Closes an open global subscription by subscription ID
|
|
220
|
-
*/
|
|
221
|
-
AppState.unsubscribeAll(subscription);
|
|
47
|
+
import { Galena, State } from "@figliolia/galena";
|
|
48
|
+
|
|
49
|
+
const AppState = new Galena(
|
|
50
|
+
{
|
|
51
|
+
navigation: new State({
|
|
52
|
+
currentRoute: "/",
|
|
53
|
+
navigationMenuOpen: false,
|
|
54
|
+
}),
|
|
55
|
+
user: new State({
|
|
56
|
+
userID: "<id>",
|
|
57
|
+
membershipTier: "free",
|
|
58
|
+
friends: ["<id1>", "<id2>"],
|
|
59
|
+
}),
|
|
60
|
+
// ...and so on
|
|
61
|
+
} /* middleware */,
|
|
62
|
+
);
|
|
222
63
|
```
|
|
223
64
|
|
|
224
|
-
|
|
225
|
-
While instances of `Galena` behave as a container for units of state, the `State` interface serves as the unit itself. The `State` interface has a predictable API designed to make composing your states simple and effective. Whether you compose your state using a "global" state or island architecture, the underlying API for your units of state look like the following:
|
|
65
|
+
From here, operations on any slice of state are type-aware and operable via a single construct:
|
|
226
66
|
|
|
227
67
|
```typescript
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
*/
|
|
237
|
-
MyState.getState();
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Update
|
|
241
|
-
*
|
|
242
|
-
* Mutates the unit of state using the callback provided. This method
|
|
243
|
-
* by default uses internal task batching in order to optimize
|
|
244
|
-
* dispatching state changes to consumers. This method is the most
|
|
245
|
-
* performant way to mutate state in Galena!
|
|
246
|
-
*/
|
|
247
|
-
MyState.update((currentState, initialState) => {
|
|
248
|
-
currentState.someValue = "new value!"
|
|
249
|
-
});
|
|
68
|
+
const subscriber = AppState.subscribe(
|
|
69
|
+
({
|
|
70
|
+
state, // The entire state object at the time of change
|
|
71
|
+
updated, // This individual State instance that was updated
|
|
72
|
+
}) => {
|
|
73
|
+
// react to state changes
|
|
74
|
+
},
|
|
75
|
+
);
|
|
250
76
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
*
|
|
254
|
-
* Runs a higher-priority mutation on your state. This method
|
|
255
|
-
* will bypass batching in favor of a scheduled propagation of
|
|
256
|
-
* changes to subscribers of your state. This method is great for
|
|
257
|
-
* prioritizing state updates driven by frequent user-input such
|
|
258
|
-
* as typing into a form or game logic.
|
|
259
|
-
*/
|
|
260
|
-
MyState.backgroundUpdate((currentState, initialState) => {
|
|
261
|
-
currentState.someValue = "new value!"
|
|
262
|
-
});
|
|
77
|
+
// to unsubscribe
|
|
78
|
+
subscription();
|
|
263
79
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
* When using `priorityUpdate()` your state changes are immediately
|
|
270
|
-
* propagated to your state subscribers ahead of all scheduled and
|
|
271
|
-
* batched updates. This method is great for usage with external
|
|
272
|
-
* scheduling mechanisms such as `requestAnimationFrame`, `intervals`,
|
|
273
|
-
* and/or `timeouts`
|
|
274
|
-
*/
|
|
275
|
-
MyState.priorityUpdate((currentState, initialState) => {
|
|
276
|
-
currentState.someValue = "new value!"
|
|
277
|
-
});
|
|
80
|
+
// to operate
|
|
81
|
+
AppState.get("user").update(state => ({
|
|
82
|
+
...state,
|
|
83
|
+
friends: [...state.friends, "<new-friend-id>"],
|
|
84
|
+
}));
|
|
278
85
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
*/
|
|
284
|
-
MyState.reset();
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Register Middleware
|
|
288
|
-
*
|
|
289
|
-
* Applies any number of Middleware instances to your State
|
|
290
|
-
*/
|
|
291
|
-
MyState.registerMiddleware(/* Middleware */ new Logger(), new Profiler());
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Subscribe
|
|
295
|
-
*
|
|
296
|
-
* Given a callback, invokes the callback each time your state
|
|
297
|
-
* changes. Returns a subscription ID
|
|
298
|
-
*/
|
|
299
|
-
const subscription = MyState.subscribe(state => {});
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Unsubscribe
|
|
303
|
-
*
|
|
304
|
-
* Closes an open subscription given a subscription ID
|
|
305
|
-
* returned by `new State().subscribe()`
|
|
306
|
-
*/
|
|
307
|
-
MyState.unsubscribe(subscription);
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Mutation (protected)
|
|
311
|
-
*
|
|
312
|
-
* This method can be used to wrap arbitrary functions that
|
|
313
|
-
* when invoked will:
|
|
314
|
-
* 1. Notify your subscriptions with the latest state
|
|
315
|
-
* 2. Execute any registered middleware (such as loggers or
|
|
316
|
-
* profiling tools)
|
|
317
|
-
*
|
|
318
|
-
* Using this method, developers can trigger or extend `Galena`'s
|
|
319
|
-
* internals for dispatching events and mutating state to create
|
|
320
|
-
* proprietary processes for an individual unit of state.
|
|
321
|
-
*
|
|
322
|
-
* In order to access this method, you'll need to extend `State`
|
|
323
|
-
* using:
|
|
324
|
-
*
|
|
325
|
-
* ```typescript
|
|
326
|
-
* class MyState extends State {
|
|
327
|
-
* proprietaryMutation = this.mutation((...anyArgs) => {
|
|
328
|
-
* // any logic
|
|
329
|
-
* });
|
|
330
|
-
* }
|
|
331
|
-
* ```
|
|
332
|
-
*/
|
|
333
|
-
MyState.mutation(/* callback */);
|
|
86
|
+
AppState.get("navigation").update(state => ({
|
|
87
|
+
...state,
|
|
88
|
+
navigationMenuOpen: true,
|
|
89
|
+
}));
|
|
334
90
|
```
|
|
335
91
|
|
|
336
|
-
###
|
|
337
|
-
Galena supports developers creating enhancements for their usage of `Galena`. Out of the box `Galena` comes with middleware for Logging and Profiling that can be used for making development with `Galena` more intuitive. To opt into `Galena`'s built-in middleware, simply pass them to your `Galena` instance when calling `new Galena()`
|
|
338
|
-
|
|
339
|
-
#### Logging Middleware
|
|
340
|
-
Galena comes with a redux-style state transition logger that prints to the console each time state updates. The Logger will log the previous state, the current state, and tell you which unit of `State` has changed.
|
|
341
|
-
|
|
342
|
-
```typescript
|
|
343
|
-
import { Galena, Logger } from "@figliolia/galena";
|
|
92
|
+
### Beyond the Basics
|
|
344
93
|
|
|
345
|
-
|
|
346
|
-
const AppState = new Galena([new Logger()]);
|
|
347
|
-
```
|
|
94
|
+
#### Models
|
|
348
95
|
|
|
349
|
-
|
|
96
|
+
`State` in Galena is designed for extension and instancing - a need that ultimately motivated the library's development.
|
|
350
97
|
|
|
351
|
-
|
|
352
|
-
Galena also comes with a Profiler that can track the duration of all state transitions. When a state transition exceeds 16ms, a warning is printed to the console notifying the developer of a potential bottleneck in his or her application. By default the Profiler will log each time a state transition exceeds one full frame (16ms). This threshold can be adjusted by calling `new Profiler(/* any number of milliseconds */)`
|
|
98
|
+
Let's take a look at a working example
|
|
353
99
|
|
|
354
100
|
```typescript
|
|
355
|
-
import {
|
|
356
|
-
|
|
357
|
-
const AppState = new Galena([new Profiler()]);
|
|
358
|
-
```
|
|
101
|
+
import { State } from "@figliolia/galena";
|
|
359
102
|
|
|
360
|
-
|
|
103
|
+
export class MyGameState extends State<IMyGameState> {
|
|
104
|
+
constructor(
|
|
105
|
+
public readonly playerID: string,
|
|
106
|
+
initialState?: Partial<IMyGameState>,
|
|
107
|
+
) {
|
|
108
|
+
super({
|
|
109
|
+
// ...default values for state
|
|
110
|
+
score: 0,
|
|
111
|
+
level: 1,
|
|
112
|
+
// overrides for the current instance
|
|
113
|
+
...initialState,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
361
116
|
|
|
362
|
-
|
|
363
|
-
|
|
117
|
+
public incrementScore(byAmount: number) {
|
|
118
|
+
this.mutate(state => {
|
|
119
|
+
state.score + byAmount,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
364
122
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
123
|
+
public goToNextLevel() {
|
|
124
|
+
this.mutate(state => {
|
|
125
|
+
state.level + 1,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
369
128
|
|
|
370
|
-
|
|
129
|
+
private mutate(fn: (state: IMyGameState) => void) {
|
|
130
|
+
state.update(previous => {
|
|
131
|
+
const clone = {...previous};
|
|
132
|
+
fn(clone);
|
|
133
|
+
return clone;
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
}
|
|
371
137
|
```
|
|
372
|
-
Using this method, whenever you create a new unit of state using `AppState.composeState()`, your `Profiler` and `Logger` will automatically register themselves on your new unit of State.
|
|
373
138
|
|
|
374
|
-
|
|
139
|
+
This extension of state, can now be used any number of times throughout your application simply by calling
|
|
375
140
|
|
|
376
141
|
```typescript
|
|
377
|
-
import {
|
|
378
|
-
|
|
379
|
-
// Let's add logging to all of our units of State
|
|
380
|
-
export const AppState = new Galena([new Logger()]);
|
|
142
|
+
import { MyGameState } from "./MyGameState";
|
|
381
143
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
bigData: new Array(10000).fill("")
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
// Profiling is applied only to this unit of state
|
|
388
|
-
FrequentlyUpdatedState.registerMiddleware(new Profiler());
|
|
144
|
+
const player1 = new MyGameState("<playerID>");
|
|
145
|
+
const player2 = new MyGameState("<playerID>");
|
|
389
146
|
```
|
|
390
147
|
|
|
391
|
-
|
|
392
|
-
Galena's middleware architecture operates on fixed set of events that are triggered each time a state mutation takes place. When state is mutated in your application, the `Middleware`'s `onBeforeUpdate()` and `onUpdate()` methods are called. Using these events, you can compose logic for auditing and enhancing your state updates! Let's take a look at a real world example.
|
|
393
|
-
|
|
394
|
-
Let's say we have an application that does not use typescript and we want to achieve type-safety for a unit of our application state. To achieve this, we'll create a middleware that validates changes to our state in real time.
|
|
148
|
+
Each instance has a shared API and defined set of state operations that make for predictable operability
|
|
395
149
|
|
|
396
|
-
|
|
150
|
+
This instances or robust models can also be used on your `Galena` instances
|
|
397
151
|
|
|
398
152
|
```typescript
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
153
|
+
import { Galena, State } from "@figliolia/galena";
|
|
154
|
+
import { MyGameState } from "./MyGameState";
|
|
155
|
+
|
|
156
|
+
const MyAppState = new Galena({
|
|
157
|
+
navigation: new State({
|
|
158
|
+
currentScreen: "/",
|
|
159
|
+
navigationMenuOpen: false,
|
|
160
|
+
}),
|
|
161
|
+
player1: new MyGameState(),
|
|
162
|
+
player2: new MyGameState(),
|
|
403
163
|
});
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
Next, let's create our own custom middleware for ensuring that all entries in the `connectedUsers` array are strings:
|
|
407
|
-
|
|
408
|
-
```typescript
|
|
409
|
-
import { Middleware } from "@figliolia/galena";
|
|
410
|
-
|
|
411
|
-
// Let's extend the Middleware class from the Galena library
|
|
412
|
-
export class ConnectedUsersMiddleware extends Middleware {
|
|
413
|
-
// A cache for the length of the array we want to audit
|
|
414
|
-
private totalArrayElements: number | null = null;
|
|
415
164
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// When an update to state occurs let's check if the length
|
|
422
|
-
// of the connectedUsers array has changed
|
|
423
|
-
override onUpdate({ state }: State) {
|
|
424
|
-
const connectedUsers = state.connectedUsers
|
|
425
|
-
if(
|
|
426
|
-
this.totalArrayElements === null ||
|
|
427
|
-
connectedUsers.length === this.totalArrayElements
|
|
428
|
-
) {
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
// If the length of user connections has changed, let's validate that
|
|
432
|
-
// the new connection is, in fact, a string.
|
|
433
|
-
const newConnection = connectedUsers[connectedUsers.length - 1];
|
|
434
|
-
if(typeof newConnection !== "string") {
|
|
435
|
-
// If we find anything other than a string, let's log or throw an error
|
|
436
|
-
console.error(`A ${typeof newConnection} was added to the current user's connection array! This can create a bug in production!`)
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
165
|
+
// Operate
|
|
166
|
+
MyAppState.get("player1").incrementScore(100);
|
|
167
|
+
MyAppState.get("player1").raiseLevel();
|
|
440
168
|
```
|
|
441
169
|
|
|
442
|
-
|
|
443
|
-
```typescript
|
|
444
|
-
import { State } from "@figliolia/galena";
|
|
445
|
-
import { ConnectedUsersMiddleware } from "./ConnectedUsersMiddleware";
|
|
446
|
-
|
|
447
|
-
export const CurrentUserState = AppState.composeState("currentUser", {
|
|
448
|
-
userID: 1,
|
|
449
|
-
username: "currentUser",
|
|
450
|
-
connectedUsers: ["2", "3", "4", "5"]
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
if(process.env.NODE_ENV !== "production") {
|
|
454
|
-
// Let's apply our custom middleware in development environments
|
|
455
|
-
CurrentUserState.registerMiddleware(new ConnectedUsersMiddleware());
|
|
456
|
-
}
|
|
457
|
-
```
|
|
170
|
+
### Middleware
|
|
458
171
|
|
|
459
|
-
|
|
460
|
-
The `Galena` library is designed to promote extension of its features. In doing so, it's possible to achieve a very strong Model/Controller layer for your applications. I'm going to demonstrate a few techniques for not only utilizing `Galena` as is, but building proprietary Models and Controllers for your applications.
|
|
172
|
+
Middleware provides a developer API for building out robust tooling for your state.
|
|
461
173
|
|
|
462
|
-
|
|
463
|
-
Galena's `State` interface is designed to be an out-of-the-box solution for housing any portion of your application's state. There are benefits however, to extending its functionality to compose proprietary models for your units of state:
|
|
174
|
+
Building an registering middleware is simple. Let's build a redux-style logger:
|
|
464
175
|
|
|
465
|
-
##### Creating State Models
|
|
466
176
|
```typescript
|
|
467
|
-
|
|
468
|
-
import { State } from "@figliolia/galena";
|
|
177
|
+
import { Middleware, type State } from "@figliolia/galena";
|
|
469
178
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}> {
|
|
477
|
-
constructor() {
|
|
478
|
-
super("User State", {
|
|
479
|
-
userID: "",
|
|
480
|
-
username: "",
|
|
481
|
-
connectedUsers: []
|
|
482
|
-
})
|
|
179
|
+
export class Logger<T = any> extends Middleware<T> {
|
|
180
|
+
private previousState: T | null = null;
|
|
181
|
+
|
|
182
|
+
override onBeforeUpdate(state: State<T>) {
|
|
183
|
+
// capture the previous state before an update takes place
|
|
184
|
+
this.previousState = state.getSnapshot();
|
|
483
185
|
}
|
|
484
186
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
187
|
+
override onUpdate(state: State<T>) {
|
|
188
|
+
// Log the time of mutation
|
|
189
|
+
console.log(
|
|
190
|
+
"%cMutation:",
|
|
191
|
+
"color: rgb(187, 186, 186); font-weight: bold",
|
|
192
|
+
"@",
|
|
193
|
+
this.time,
|
|
194
|
+
);
|
|
195
|
+
// Log the previous state
|
|
196
|
+
console.log(
|
|
197
|
+
" %cPrevious State",
|
|
198
|
+
"color: #26ad65; font-weight: bold",
|
|
199
|
+
this.previousState,
|
|
200
|
+
);
|
|
201
|
+
// Log the new state
|
|
202
|
+
console.log(
|
|
203
|
+
" %cNext State ",
|
|
204
|
+
"color: rgb(17, 118, 249); font-weight: bold",
|
|
205
|
+
state.getSnapshot(),
|
|
206
|
+
);
|
|
489
207
|
}
|
|
490
208
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
209
|
+
private get time() {
|
|
210
|
+
const date = new Date();
|
|
211
|
+
const mHours = date.getHours();
|
|
212
|
+
const hours = mHours > 12 ? mHours - 12 : mHours;
|
|
213
|
+
const mins = date.getMinutes();
|
|
214
|
+
const minutes = mins.toString().length === 1 ? `0${mins}` : mins;
|
|
215
|
+
const secs = date.getSeconds();
|
|
216
|
+
const seconds = secs.toString().length === 1 ? `0${secs}` : secs;
|
|
217
|
+
const milliseconds = date.getMilliseconds();
|
|
218
|
+
return `${hours}:${minutes}:${seconds}:${milliseconds}`;
|
|
495
219
|
}
|
|
496
220
|
}
|
|
497
221
|
```
|
|
498
222
|
|
|
499
|
-
|
|
223
|
+
Registering middleware is simple:
|
|
500
224
|
|
|
501
225
|
```typescript
|
|
502
|
-
|
|
503
|
-
import { Galena, State } from "@figliolia/galena";
|
|
504
|
-
import { UserModel } from "./UserModel";
|
|
226
|
+
import { Logger, Profiler } from "@figliolia/galena";
|
|
505
227
|
|
|
506
|
-
|
|
228
|
+
// To apply middleware to all instances of `State`
|
|
229
|
+
// attached to a `Galena` instance
|
|
507
230
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
username: "currentUser",
|
|
512
|
-
connectedUsers: ["2", "3", "4", "5"]
|
|
513
|
-
}, UserModel); // Specify the UserModel here so that our new unit is created using the `UserModel` instead of `State`
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
Now that we have our current user in our Galena State, we can create subscriptions and updates!
|
|
517
|
-
|
|
518
|
-
```tsx
|
|
519
|
-
import { AppState } from "./AppState";
|
|
520
|
-
|
|
521
|
-
const subscriptionID = AppState.subscribe("currentUser", state => {
|
|
522
|
-
// React to changes to the current user!
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
// The UserModel's custom methods are available on your Galena state!
|
|
526
|
-
AppState.get("currentUser").updateUsername("awesomeUser");
|
|
231
|
+
const MyAppState = new Galena({
|
|
232
|
+
// state
|
|
233
|
+
}, new Logger(), new Profiler());
|
|
527
234
|
|
|
528
|
-
|
|
235
|
+
// To apply middleware to a single of `State`
|
|
236
|
+
const MyState = new State(
|
|
237
|
+
/* reactive value */,
|
|
238
|
+
new Logger(),
|
|
239
|
+
new Profiler()
|
|
240
|
+
);
|
|
529
241
|
```
|
|
530
242
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
#### In Place Mutations
|
|
537
|
-
In Galena, state mutations can occur in-place (`O(1)` space). While you can use immutable data structures if you like, it's not required when using this library - by design. This is because even the most basic immutable state updates are about 4-5x slower than mutable state updates. This `4-5x` balloons even larger the more your state grows. When building `Galena`, I wanted to remove the notion of immutability wherever possible.
|
|
538
|
-
|
|
539
|
-
#### Composition of State
|
|
540
|
-
To further promote efficient state mutations, `Galena`'s composition architecture allows units of state to be operable without effecting adjacent units of state. This means you can can safely make *extremely* frequent updates to your state and be sure that your updates are scoped to specific units - and not your *entire* application state. This optimization extends to subscriptions as well. The only subscriptions that will ever be triggered when state changes, are the ones directly bound to the unit that is changing.
|
|
541
|
-
|
|
542
|
-
#### Benchmarking
|
|
543
|
-
Using 2 identical applications, I've profiled the performance of Galena vs. Redux using 10,000 state updates and 10 connected React Components that'll rerender on each state change. The results looked like the following:
|
|
544
|
-
|
|
545
|
-
##### Galena vs. Redux with no middleware
|
|
546
|
-
1. Redux - `33.6ms` to complete 10,000 state updates
|
|
547
|
-
2. Galena - `5.5ms` to complete 10,000 state updates
|
|
548
|
-
|
|
549
|
-
As the application scales with more state updates and connected components, the spread between `Galena` and Redux grows even further. Although I don't believe most applications will ever require 10,000 immediate state updates (unless building a game-like experience), `Galena` does relieve the bottle-necks of popular state management utilities quite well.
|
|
243
|
+
In your console you'll now see logs like the following:
|
|
244
|
+
<img src="media/Logging.png" />
|
|
245
|
+
And Profiler warnings such as thing one
|
|
246
|
+
<img src="media/Profiling.png" />
|
|
550
247
|
|
|
551
|
-
###
|
|
552
|
-
`Galena` provides bindings for React through [react-galena](https://www.npmjs.com/package/@figliolia/react-galena). This package provides factories for generating HOC's and hooks from your Galena instances and units of State!
|
|
248
|
+
### Frameworks
|
|
553
249
|
|
|
554
|
-
|
|
555
|
-
|
|
250
|
+
With State management tools, naturally comes frontend frameworks. Galena provides bindings for `React` through
|
|
251
|
+
the [react-galena](https://github.com/alexfigliolia/react-galena) library
|