@figliolia/galena 2.3.5 → 3.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.
Files changed (106) hide show
  1. package/README.md +172 -476
  2. package/dist/Galena.cjs +102 -0
  3. package/dist/Galena.d.cts +80 -0
  4. package/dist/Galena.d.cts.map +1 -0
  5. package/dist/Galena.d.mts +80 -0
  6. package/dist/Galena.d.mts.map +1 -0
  7. package/dist/Galena.mjs +103 -0
  8. package/dist/Galena.mjs.map +1 -0
  9. package/dist/Logger.cjs +46 -0
  10. package/dist/Logger.d.cts +35 -0
  11. package/dist/Logger.d.cts.map +1 -0
  12. package/dist/Logger.d.mts +35 -0
  13. package/dist/Logger.d.mts.map +1 -0
  14. package/dist/Logger.mjs +48 -0
  15. package/dist/Logger.mjs.map +1 -0
  16. package/dist/Middleware.cjs +62 -0
  17. package/dist/Middleware.d.cts +65 -0
  18. package/dist/Middleware.d.cts.map +1 -0
  19. package/dist/Middleware.d.mts +65 -0
  20. package/dist/Middleware.d.mts.map +1 -0
  21. package/dist/Middleware.mjs +64 -0
  22. package/dist/Middleware.mjs.map +1 -0
  23. package/dist/Profiler.cjs +41 -0
  24. package/dist/Profiler.d.cts +31 -0
  25. package/dist/Profiler.d.cts.map +1 -0
  26. package/dist/Profiler.d.mts +31 -0
  27. package/dist/Profiler.d.mts.map +1 -0
  28. package/dist/Profiler.mjs +43 -0
  29. package/dist/Profiler.mjs.map +1 -0
  30. package/dist/State.cjs +147 -0
  31. package/dist/State.d.cts +114 -0
  32. package/dist/State.d.cts.map +1 -0
  33. package/dist/State.d.mts +114 -0
  34. package/dist/State.d.mts.map +1 -0
  35. package/dist/State.mjs +148 -0
  36. package/dist/State.mjs.map +1 -0
  37. package/dist/index.cjs +13 -0
  38. package/dist/index.d.cts +7 -0
  39. package/dist/index.d.mts +7 -0
  40. package/dist/index.mjs +6 -0
  41. package/dist/types.d.cts +16 -0
  42. package/dist/types.d.cts.map +1 -0
  43. package/dist/types.d.mts +16 -0
  44. package/dist/types.d.mts.map +1 -0
  45. package/media/Logging.png +0 -0
  46. package/media/Profiling.png +0 -0
  47. package/package.json +38 -59
  48. package/src/Galena.ts +120 -0
  49. package/src/{Middlewares/Logger.ts → Logger.ts} +15 -14
  50. package/src/Middleware.ts +62 -0
  51. package/src/Profiler.ts +53 -0
  52. package/src/State.ts +167 -0
  53. package/src/index.ts +6 -3
  54. package/src/types.ts +28 -0
  55. package/dist/cjs/Galena/Galena.js +0 -223
  56. package/dist/cjs/Galena/Guards.js +0 -40
  57. package/dist/cjs/Galena/Scheduler.js +0 -84
  58. package/dist/cjs/Galena/State.js +0 -314
  59. package/dist/cjs/Galena/index.js +0 -22
  60. package/dist/cjs/Galena/types.js +0 -9
  61. package/dist/cjs/Middleware/Middleware.js +0 -46
  62. package/dist/cjs/Middleware/index.js +0 -20
  63. package/dist/cjs/Middleware/types.js +0 -8
  64. package/dist/cjs/Middlewares/Logger.js +0 -51
  65. package/dist/cjs/Middlewares/Profiler.js +0 -38
  66. package/dist/cjs/Middlewares/index.js +0 -7
  67. package/dist/cjs/index.js +0 -19
  68. package/dist/cjs/package.json +0 -3
  69. package/dist/mjs/Galena/Galena.js +0 -218
  70. package/dist/mjs/Galena/Guards.js +0 -36
  71. package/dist/mjs/Galena/Scheduler.js +0 -79
  72. package/dist/mjs/Galena/State.js +0 -313
  73. package/dist/mjs/Galena/index.js +0 -3
  74. package/dist/mjs/Galena/types.js +0 -6
  75. package/dist/mjs/Middleware/Middleware.js +0 -42
  76. package/dist/mjs/Middleware/index.js +0 -2
  77. package/dist/mjs/Middleware/types.js +0 -5
  78. package/dist/mjs/Middlewares/Logger.js +0 -44
  79. package/dist/mjs/Middlewares/Profiler.js +0 -35
  80. package/dist/mjs/Middlewares/index.js +0 -2
  81. package/dist/mjs/index.js +0 -3
  82. package/dist/mjs/package.json +0 -4
  83. package/dist/types/Galena/Galena.d.ts +0 -160
  84. package/dist/types/Galena/Guards.d.ts +0 -29
  85. package/dist/types/Galena/Scheduler.d.ts +0 -51
  86. package/dist/types/Galena/State.d.ts +0 -235
  87. package/dist/types/Galena/index.d.ts +0 -3
  88. package/dist/types/Galena/types.d.ts +0 -13
  89. package/dist/types/Middleware/Middleware.d.ts +0 -43
  90. package/dist/types/Middleware/index.d.ts +0 -2
  91. package/dist/types/Middleware/types.d.ts +0 -4
  92. package/dist/types/Middlewares/Logger.d.ts +0 -27
  93. package/dist/types/Middlewares/Profiler.d.ts +0 -22
  94. package/dist/types/Middlewares/index.d.ts +0 -2
  95. package/dist/types/index.d.ts +0 -3
  96. package/src/Galena/Galena.ts +0 -252
  97. package/src/Galena/Guards.ts +0 -49
  98. package/src/Galena/Scheduler.ts +0 -85
  99. package/src/Galena/State.ts +0 -344
  100. package/src/Galena/index.ts +0 -3
  101. package/src/Galena/types.ts +0 -18
  102. package/src/Middleware/Middleware.ts +0 -45
  103. package/src/Middleware/index.ts +0 -2
  104. package/src/Middleware/types.ts +0 -4
  105. package/src/Middlewares/Profiler.ts +0 -41
  106. 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
- In Galena, your state architecture is a composition of reactive units that can be declared at any point in your application lifecycle. Your units of state can exist in isolation (similar to React Contexts) or as part of one or more global application states (similar to Redux). Galena offers a global application state solution that supports lazy initialization of state, performant updates, and a rich development API.
3
+ Lightning fast, framework agnostic state, that doesn't glue your state operations to your UI components!
5
4
 
6
- # Getting Started
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
- ## Installation
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
- ## Composing Your Application State
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
- Now that we have our `AppState` instance, let's compose a unit of state for it!
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
- Creating units of state using `AppState.composeState()` will scope your new unit of state to your `Galena` instance. You can then access, subscribe, and update your state using using your `Galena` instance or the unit of `State` returned from `AppState.composeState()`:
17
+ ## Basic Usage
50
18
 
51
- #### State Operations Using Your Galena Instance
19
+ ### The State Model
52
20
 
53
- ```typescript
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
- // Create Your Isolated Unit of State
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 = FeatureState.subscribe((state) => {
105
- // React to FeatureState changes!
28
+ const subscription = MyState.subscribe(value => {
29
+ // do something with changed values
106
30
  });
107
31
 
108
- FeatureState.update((state) => {
109
- // Update feature state!
110
- state.list = [...state.list, state.list.length];
111
- });
32
+ // to unsubscribe
33
+ subscription();
112
34
 
113
- // Clean up subscriptions
114
- FeatureState.unsubscribe(subscription);
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
- The API for isolated units of State is the same as the API for units connected to a `Galena` instance. When using `Galena` it's totally normal to have one or more "global" states along side any number of island states for isolate-able features. The composition patterns that can be used for your application state are virtually limitless!
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
- In `Galena`, your "global" application state exists in the form of operable sub-structures that can be individually subscribed to and mutated. This means, mutating one piece of your `State` does not effect other units of your `State`. This allows for the relief of several performance bottlenecks that are common in state management libraries that offer "global" application states. In `Galena`, you get the performance of island architecture with the option to also have a predictable global application state.
42
+ ### The Galena Model
126
43
 
127
- #### Galena Public Methods
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, Logger, Profiler } from "@figliolia/galena";
131
- import type { State } from "@figliolia/galena";
132
-
133
- const AppState = new Galena(/* middleware */ [new Logger(), new Profiler()]);
134
-
135
- /**
136
- * Get State
137
- *
138
- * Returns the current state tree with each attached
139
- * unit. The object returned is readonly
140
- */
141
- AppState.getState();
142
-
143
- /**
144
- * Compose State
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
- ### State
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
- import { State, Logger, Profiler } from "@figliolia/galena";
229
-
230
- const MyState = new State(/* a unique name */ "myState", /* initial state */);
231
-
232
- /**
233
- * Get State
234
- *
235
- * Returns the current state
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
- * Background Update
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
- * Priority Update
266
- *
267
- * Runs a highest-priority mutation on your state. This method
268
- * bypasses all batching and scheduling optimizations in Galena.
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
- * Reset
281
- *
282
- * Resets the current state back to its initial state
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
- ### Middleware
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
- // Enable logging!
346
- const AppState = new Galena([new Logger()]);
347
- ```
94
+ #### Models
348
95
 
349
- ![Logs](images/Logging.png)
96
+ `State` in Galena is designed for extension and instancing - a need that ultimately motivated the library's development.
350
97
 
351
- #### Profiling Middleware
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 { Galena, Profiler } from "@figliolia/galena";
356
-
357
- const AppState = new Galena([new Profiler()]);
358
- ```
101
+ import { State } from "@figliolia/galena";
359
102
 
360
- ![Profiles](images/Profiling.png)
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
- ### Middleware - Advanced Usage
363
- Similar to a lot of stateful tools, `Galena` also exposes an API for creating your own Middleware. With it, you can do a lot of cool things for both development and productions environments. Let's first look at how to use middleware in `Galena`, then we'll walk through creating our own!
117
+ public incrementScore(byAmount: number) {
118
+ this.mutate(state => {
119
+ state.score + byAmount,
120
+ });
121
+ }
364
122
 
365
- #### Applying Middleware
366
- When applying middleware in `Galena`, you may choose to apply your middleware to *all* of your application state or just some of it. To apply middleware to each of your units of `State`, you can simply initialize `Galena` with the middleware that you enjoy using:
367
- ```typescript
368
- import { Galena, Profiler, Logger } from "@figliolia/galena";
123
+ public goToNextLevel() {
124
+ this.mutate(state => {
125
+ state.level + 1,
126
+ });
127
+ }
369
128
 
370
- export const AppState = new Galena([new Profiler(), new Logger()]);
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
- Alternatively, you may also choose to register a middleware on only some of your state:
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 { Galena, Profiler, Logger } from "@figliolia/galena";
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
- // Lets create an arbitrary unit of state and add profiling to it
383
- export const FrequentlyUpdatedState = AppState.composeState("complexState", {
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
- #### Creating Middleware
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
- In the example below, we'll create a unit of state holding unique identifiers for users that are connections of the current user:
150
+ This instances or robust models can also be used on your `Galena` instances
397
151
 
398
152
  ```typescript
399
- export const CurrentUserState = AppState.composeState("currentUser", {
400
- userID: "1",
401
- username: "currentUser",
402
- connectedUsers: ["2", "3", "4", "5"]
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
- // On each update, let's cache the length of the array
417
- override onBeforeUpdate({ state }: State) {
418
- this.totalArrayElements = state.connectedUsers.length;
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
- Next let's bring this middleware into our application!
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
- ### Let's Talk Architecture
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
- #### Extending State
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
- // UserModel.ts
468
- import { State } from "@figliolia/galena";
177
+ import { Middleware, type State } from "@figliolia/galena";
469
178
 
470
- // Let's extend the `State` class for a hypothetical
471
- // user schema
472
- export class UserModel extends State<{
473
- userID: string;
474
- username: string;
475
- connectedUsers: string[];
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
- public addConnection(userID: string) {
486
- this.update(state => {
487
- state.connectedUsers = [...state.connectedUsers, userID];
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
- public updateUsername(username: string) {
492
- this.update(state => {
493
- state.username = username;
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
- Next, let's use our Model!
223
+ Registering middleware is simple:
500
224
 
501
225
  ```typescript
502
- // AppState.ts
503
- import { Galena, State } from "@figliolia/galena";
504
- import { UserModel } from "./UserModel";
226
+ import { Logger, Profiler } from "@figliolia/galena";
505
227
 
506
- export const AppState = new Galena(/* middleware */);
228
+ // To apply middleware to all instances of `State`
229
+ // attached to a `Galena` instance
507
230
 
508
- // Let's apply our UserModel to AppState
509
- export const UserState = AppState.composeState("currentUser", {
510
- userID: 1,
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
- AppState.get("currentUser").addConnection("6");
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
- Using this extension pattern, each unit of `State` can exist as it's own model with abstractions for proprietary mutations and business logic. Although slightly more complex on the surface, this pattern in very large applications will reduce the complexity of state management significantly. It'll also replicate what one might find at the persistence layer of server-side code - where persisted data structures are often modeled along side their mutation logic when interacting with a database or GQL Resolver. Because of this, the extension pattern's syntactical uniformity may be beneficial for teams that lean fullstack instead of frontend/backend!
532
-
533
- ### Let's talk Performance!
534
- In several areas of the `Galena` readme, there are references to performance and the relief of bottlenecks. In this section I'd like to share some hard numbers relating to areas of `Galena`'s architecture.
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
- ### Support for Frontend Frameworks!
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
- #### Demo Application
555
- To see some basic usage using Galena with React, please check out this [Example App](https://github.com/alexfigliolia/galena-quick-start)
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