@figliolia/galena 2.2.2 → 2.2.4

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 (43) hide show
  1. package/README.md +12 -4
  2. package/dist/cjs/package.json +3 -0
  3. package/dist/mjs/Galena/Galena.js +225 -0
  4. package/dist/mjs/Galena/Guards.js +36 -0
  5. package/dist/mjs/Galena/Scheduler.js +79 -0
  6. package/dist/mjs/Galena/State.js +285 -0
  7. package/dist/mjs/Galena/index.js +3 -0
  8. package/dist/mjs/Galena/types.js +6 -0
  9. package/dist/mjs/Middleware/Middleware.js +42 -0
  10. package/dist/mjs/Middleware/index.js +2 -0
  11. package/dist/mjs/Middleware/types.js +5 -0
  12. package/dist/mjs/Middlewares/Logger.js +44 -0
  13. package/dist/mjs/Middlewares/Profiler.js +35 -0
  14. package/dist/mjs/Middlewares/index.js +2 -0
  15. package/dist/mjs/index.js +3 -0
  16. package/dist/mjs/package.json +4 -0
  17. package/package.json +13 -4
  18. /package/dist/{Galena → cjs/Galena}/Galena.js +0 -0
  19. /package/dist/{Galena → cjs/Galena}/Guards.js +0 -0
  20. /package/dist/{Galena → cjs/Galena}/Scheduler.js +0 -0
  21. /package/dist/{Galena → cjs/Galena}/State.js +0 -0
  22. /package/dist/{Galena → cjs/Galena}/index.js +0 -0
  23. /package/dist/{Galena → cjs/Galena}/types.js +0 -0
  24. /package/dist/{Middleware → cjs/Middleware}/Middleware.js +0 -0
  25. /package/dist/{Middleware → cjs/Middleware}/index.js +0 -0
  26. /package/dist/{Middleware → cjs/Middleware}/types.js +0 -0
  27. /package/dist/{Middlewares → cjs/Middlewares}/Logger.js +0 -0
  28. /package/dist/{Middlewares → cjs/Middlewares}/Profiler.js +0 -0
  29. /package/dist/{Middlewares → cjs/Middlewares}/index.js +0 -0
  30. /package/dist/{index.js → cjs/index.js} +0 -0
  31. /package/dist/{Galena → types/Galena}/Galena.d.ts +0 -0
  32. /package/dist/{Galena → types/Galena}/Guards.d.ts +0 -0
  33. /package/dist/{Galena → types/Galena}/Scheduler.d.ts +0 -0
  34. /package/dist/{Galena → types/Galena}/State.d.ts +0 -0
  35. /package/dist/{Galena → types/Galena}/index.d.ts +0 -0
  36. /package/dist/{Galena → types/Galena}/types.d.ts +0 -0
  37. /package/dist/{Middleware → types/Middleware}/Middleware.d.ts +0 -0
  38. /package/dist/{Middleware → types/Middleware}/index.d.ts +0 -0
  39. /package/dist/{Middleware → types/Middleware}/types.d.ts +0 -0
  40. /package/dist/{Middlewares → types/Middlewares}/Logger.d.ts +0 -0
  41. /package/dist/{Middlewares → types/Middlewares}/Profiler.d.ts +0 -0
  42. /package/dist/{Middlewares → types/Middlewares}/index.d.ts +0 -0
  43. /package/dist/{index.d.ts → types/index.d.ts} +0 -0
package/README.md CHANGED
@@ -107,7 +107,7 @@ const subscription = FeatureState.subscribe((state) => {
107
107
 
108
108
  FeatureState.update((state) => {
109
109
  // Update feature state!
110
- state.list.push(state.list.length);
110
+ state.list = [...state.list, state.list.length];
111
111
  });
112
112
 
113
113
  // Clean up subscriptions
@@ -393,7 +393,7 @@ In the example below, we'll create a unit of state holding unique identifiers fo
393
393
 
394
394
  ```typescript
395
395
  export const CurrentUserState = AppState.composeState("currentUser", {
396
- userID: 1,
396
+ userID: "1",
397
397
  username: "currentUser",
398
398
  connectedUsers: ["2", "3", "4", "5"]
399
399
  });
@@ -470,9 +470,17 @@ export class UserModel extends State<{
470
470
  username: string;
471
471
  connectedUsers: string[];
472
472
  }> {
473
+ constructor() {
474
+ super("User State", {
475
+ userID: "",
476
+ username: "",
477
+ connectedUsers: []
478
+ })
479
+ }
480
+
473
481
  public addConnection(userID: string) {
474
482
  this.update(state => {
475
- state.connectedUsers.push(userID);
483
+ state.connectedUsers = [...state.connectedUsers, userID];
476
484
  });
477
485
  }
478
486
 
@@ -537,7 +545,7 @@ Using 2 identical applications, I've profiled the performance of Galena vs. Redu
537
545
  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.
538
546
 
539
547
  ### Support for Frontend Frameworks!
540
- `Galena` provides bindings for React through [react-galena](https://github.com/alexfigliolia/react-galena). This package provides factories for generating HOC's and hooks from your Galena instances and units of State!
548
+ `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!
541
549
 
542
550
  #### Demo Application
543
551
  To see some basic usage using Galena with React, please check out this [Example App](https://github.com/alexfigliolia/galena-quick-start)
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,225 @@
1
+ import { AutoIncrementingID } from "@figliolia/event-emitter";
2
+ import { State } from "./State.js";
3
+ import { Guards } from "./Guards.js";
4
+ /**
5
+ * ## Galena
6
+ *
7
+ * A performant global state solution that scales
8
+ *
9
+ * ### Creating State
10
+ *
11
+ * ```typescript
12
+ * // AppState.ts
13
+ * import { Galena } from "@figliolia/galena";
14
+ *
15
+ * const AppState = new Galena([...middleware]);
16
+ *
17
+ * const NavigationState = AppState.composeState("navigation", {
18
+ * currentRoute: "/",
19
+ * userID: "12345",
20
+ * permittedRoutes: ["/*"]
21
+ * });
22
+ * ```
23
+ *
24
+ * ### Subscribing to State Changes
25
+ * #### Using the Galena Instance
26
+ * ```typescript
27
+ * import { AppState } from "./AppState";
28
+ *
29
+ * AppState.subscribe(appState => {
30
+ * const navState = appState.get("navigation");
31
+ * const { currentRoute } = navState.state;
32
+ * // do something with state changes!
33
+ * });
34
+ * ```
35
+ * #### Using the State Instance
36
+ * ```typescript
37
+ * NavigationState.subscribe(navigation => {
38
+ * const { currentRoute } = navigation
39
+ * // do something with state changes!
40
+ * });
41
+ * ```
42
+ *
43
+ * #### Using Global Subscriptions
44
+ * ```typescript
45
+ * NavigationState.subscribeAll(nextState => {
46
+ * const { currentRoute } = nextState.navigation
47
+ * // do something with state changes!
48
+ * });
49
+ * ```
50
+ *
51
+ * ### Mutating State
52
+ * ```typescript
53
+ * NavigationState.update(state => {
54
+ * state.currentRoute = "/profile";
55
+ * // You can mutate state without creating new objects!
56
+ * });
57
+ * ```
58
+ */
59
+ export class Galena extends Guards {
60
+ state = {};
61
+ subscriptions = new Map();
62
+ middleware = [];
63
+ IDs = new AutoIncrementingID();
64
+ constructor(middleware = []) {
65
+ super();
66
+ this.middleware = middleware;
67
+ }
68
+ /**
69
+ * Compose State
70
+ *
71
+ * Creates a new `State` instance and returns it. Your new state
72
+ * becomes immediately available on your `Galena` instance and
73
+ * is wired into your middleware. All existing subscriptions to
74
+ * state will automatically receive updates when your new unit of
75
+ * state updates
76
+ */
77
+ composeState(name, initialState, Model = (State)) {
78
+ this.guardDuplicateStates(name, this.state);
79
+ const state = new Model(name, initialState);
80
+ state.registerMiddleware(...this.middleware);
81
+ this.mutable[name] = state;
82
+ this.reIndexSubscriptions(name);
83
+ return state;
84
+ }
85
+ /**
86
+ * Get State
87
+ *
88
+ * Returns a mutable state instance
89
+ */
90
+ getState() {
91
+ return this.state;
92
+ }
93
+ /**
94
+ * Get
95
+ *
96
+ * Returns a unit of `State` by name
97
+ */
98
+ get(name) {
99
+ this.warnForUndefinedStates(name, this.state);
100
+ return this.state[name];
101
+ }
102
+ /**
103
+ * Mutable
104
+ *
105
+ * Returns a mutable state instance
106
+ */
107
+ get mutable() {
108
+ return this.state;
109
+ }
110
+ /**
111
+ * Update
112
+ *
113
+ * Runs a mutation on the specified unit of state
114
+ */
115
+ update(name, mutation) {
116
+ return this.get(name).update(mutation);
117
+ }
118
+ /**
119
+ * Background Update
120
+ *
121
+ * Runs a higher priority mutation on the specified unit of
122
+ * state
123
+ */
124
+ backgroundUpdate(name, mutation) {
125
+ return this.get(name).backgroundUpdate(mutation);
126
+ }
127
+ /**
128
+ * Priority Update
129
+ *
130
+ * Runs an immediate priority mutation on the specified unit
131
+ * of state
132
+ */
133
+ priorityUpdate(name, mutation) {
134
+ return this.get(name).priorityUpdate(mutation);
135
+ }
136
+ /**
137
+ * Subscribe
138
+ *
139
+ * Given the name of a unit of state, this method registers
140
+ * a subscription on the target state instance. The callback
141
+ * you provide will execute each time state changes. Returns
142
+ * a unique identifier for your subscription. To clean up your
143
+ * subscription, call `Galena.unsubscribe()` with the ID returned
144
+ * by this method
145
+ */
146
+ subscribe(name, callback) {
147
+ return this.get(name).subscribe(callback);
148
+ }
149
+ /**
150
+ * Unsubscribe
151
+ *
152
+ * Given a subscription ID returned from the `subscribe` method,
153
+ * this method removes and cleans up the corresponding subscription
154
+ */
155
+ unsubscribe(name, ID) {
156
+ return this.get(name).unsubscribe(ID);
157
+ }
158
+ /**
159
+ * Subscribe All
160
+ *
161
+ * Registers a callback on each registered `State` instance and
162
+ * is invoked each time your state changes. Using `Galena`'s
163
+ * `subscribeAll` method, although performant, can be less
164
+ * performant than subscribing directly to a target `State`
165
+ * instance using `Galena.subscribe()`. To clean up your
166
+ * subscription, call `Galena.unsubscribeAll()` with the ID
167
+ * returned
168
+ */
169
+ subscribeAll(callback) {
170
+ const stateSubscriptions = [];
171
+ for (const key in this.state) {
172
+ stateSubscriptions.push([
173
+ key,
174
+ this.state[key].subscribe(() => {
175
+ callback(this.state);
176
+ }),
177
+ ]);
178
+ }
179
+ const subscriptionID = this.IDs.get();
180
+ this.subscriptions.set(subscriptionID, stateSubscriptions);
181
+ return subscriptionID;
182
+ }
183
+ /**
184
+ * Unsubscribe
185
+ *
186
+ * Given a subscription ID returned from the `subscribeAll()` method,
187
+ * this method removes and cleans up the corresponding subscription
188
+ */
189
+ unsubscribeAll(ID) {
190
+ const IDs = this.subscriptions.get(ID);
191
+ if (IDs) {
192
+ for (const [state, ID] of IDs) {
193
+ this.state[state].unsubscribe(ID);
194
+ this.subscriptions.delete(ID);
195
+ }
196
+ }
197
+ }
198
+ /**
199
+ * ReIndex Subscriptions
200
+ *
201
+ * When units of state are created lazily, this method updates
202
+ * each existing subscription to receive mutations occurring on
203
+ * recently created `State` instances that post-date prior
204
+ * subscriptions
205
+ */
206
+ reIndexSubscriptions(name) {
207
+ for (const [ID, unitSubscriptions] of this.subscriptions) {
208
+ for (const [state, subscriptionID] of unitSubscriptions) {
209
+ const callback = this.state[state]["emitter"]
210
+ .get(state)
211
+ ?.get(subscriptionID);
212
+ if (callback) {
213
+ unitSubscriptions.push([
214
+ name,
215
+ this.state[name].subscribe(() => {
216
+ void callback(this.state);
217
+ }),
218
+ ]);
219
+ this.subscriptions.set(ID, unitSubscriptions);
220
+ break;
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Guards
3
+ *
4
+ * Development-only warnings and runtime errors designed to
5
+ * guard developers against possible pitfalls when using
6
+ * Galena. This interface provides composable error and
7
+ * warning methods that can be used to prevent invalid usage
8
+ * of the library
9
+ */
10
+ export class Guards {
11
+ /**
12
+ * Warn For Undefined States
13
+ *
14
+ * In Galena, it's normal to lazy initialize a unit of state
15
+ * in attached to a `Galena` instance. This warning lets
16
+ * developers know that they are attempting to manipulate a
17
+ * unit of state that has not yet been initialized
18
+ */
19
+ warnForUndefinedStates(name, state) {
20
+ if (!(name in state)) {
21
+ console.warn(`A unit of state with the name "${name}" does not yet exist on this Galena instance. If this is expected, you can ignore this warning`);
22
+ }
23
+ }
24
+ /**
25
+ * Guard Duplicate States
26
+ *
27
+ * Throws an error if a developer attempts to create
28
+ * more than one state with the same name on a single
29
+ * `Galena` instance
30
+ */
31
+ guardDuplicateStates(name, state) {
32
+ if (name in state) {
33
+ console.warn(`A unit of state with the name "${name}" already exists on this Galena instance. Please re-name this new unit of state to something unique`);
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,79 @@
1
+ import { Priority } from "./types.js";
2
+ /**
3
+ * Scheduler
4
+ *
5
+ * Scheduling dispatched events to state consumers is how Galena
6
+ * out-performs just about every state management library out there.
7
+ * The scheduler offers the ability to dispatch state updates on 3
8
+ * priorities:
9
+ *
10
+ * 1. Immediate - Immediate synchronous task execution and propagation of
11
+ * changes to consumers
12
+ * 2. Microtask - Immediate task execution and scheduled propagation of
13
+ * changes to consumers
14
+ * 3. Batched - Immediate task execution and batched propagation of
15
+ * changes to consumers
16
+ *
17
+ * This module manages the propagation of changes to State consumers
18
+ * by implementing the three priorities outlined above
19
+ */
20
+ export class Scheduler {
21
+ task = null;
22
+ schedule = null;
23
+ constructor() {
24
+ this.executeTasks = this.executeTasks.bind(this);
25
+ }
26
+ /**
27
+ * Schedule Task
28
+ *
29
+ * Given a task (the emission of state changes to consumers)
30
+ * and a priority, this method executes the task on the priority
31
+ * level specified
32
+ */
33
+ scheduleTask(task, priority) {
34
+ this.task = task;
35
+ switch (priority) {
36
+ case Priority.IMMEDIATE:
37
+ return this.executeTasks();
38
+ case Priority.MICROTASK:
39
+ return Promise.resolve().then(() => {
40
+ return this.executeTasks();
41
+ });
42
+ case Priority.BATCHED:
43
+ default:
44
+ if (!this.schedule) {
45
+ this.createSchedule();
46
+ }
47
+ }
48
+ }
49
+ /**
50
+ * Create Schedule
51
+ *
52
+ * Schedules the execution of the current task after 5 milliseconds
53
+ */
54
+ createSchedule() {
55
+ this.clearSchedule();
56
+ this.schedule = setTimeout(this.executeTasks, 5);
57
+ }
58
+ /**
59
+ * Clear Schedule
60
+ *
61
+ * Clears the schedule if it exists
62
+ */
63
+ clearSchedule() {
64
+ if (this.schedule !== null) {
65
+ clearTimeout(this.schedule);
66
+ this.schedule = null;
67
+ }
68
+ }
69
+ /**
70
+ * Execute Tasks
71
+ *
72
+ * Clears the schedule if it exists and executes the current task
73
+ */
74
+ executeTasks() {
75
+ this.clearSchedule();
76
+ this.task?.();
77
+ this.task = null;
78
+ }
79
+ }
@@ -0,0 +1,285 @@
1
+ import { MiddlewareEvents } from "../Middleware/types.js";
2
+ import { EventEmitter } from "@figliolia/event-emitter";
3
+ import { Priority } from "./types.js";
4
+ import { Scheduler } from "./Scheduler.js";
5
+ /**
6
+ * ### State
7
+ *
8
+ * The root of all reactivity in Galena. State instances can
9
+ * operate in isolation by calling `new State(...args)` or as
10
+ * part of your application's larger global state by using
11
+ * `new Galena().composeState()`.
12
+ *
13
+ * `State` instances operate on the premise of pub-sub and mutability.
14
+ * This provides significant performance improvement over more traditional
15
+ * state management tools because
16
+ *
17
+ * 1. Mutations can occur in O(1) space
18
+ * 2. Mutations can be batched when dispatching updates to subscribers
19
+ *
20
+ * When deciding how many `State` instances are required for your
21
+ * applications needs, we suggest creating and organizing state in
22
+ * accordance with your application logic. Meaning, you might have a
23
+ * `State` instance for navigation/routing, another `State` instance
24
+ * for storing user information, and so on. Performance can improve
25
+ * significantly when state is dispersed amongst multiple instances
26
+ *
27
+ * #### Creating State Instances
28
+ *
29
+ * ```typescript
30
+ * const MyState = new State("MyState", {
31
+ * someData: true,
32
+ * listItems: [1, 2, 3, 4];
33
+ * // ...etc
34
+ * });
35
+ * ```
36
+ *
37
+ * #### Updating State
38
+ * ##### Synchronous updates
39
+ * ```typescript
40
+ * MyState.update((state) => {
41
+ * state.listItems.push(5);
42
+ * });
43
+ * ```
44
+ * ##### Asynchronous updates
45
+ * ```typescript
46
+ * MyState.update(async (state) => {
47
+ * const listItems = await fetch("/list-items");
48
+ * state.listItems = listItems;
49
+ * });
50
+ * ```
51
+ *
52
+ * #### Subscribing to State Changes
53
+ * ```typescript
54
+ * MyState.subscribe((state) => {
55
+ * const { listItems } = state
56
+ * // Do something with your list items!
57
+ * });
58
+ * ```
59
+ */
60
+ export class State extends Scheduler {
61
+ state;
62
+ name;
63
+ initialState;
64
+ middleware = [];
65
+ emitter = new EventEmitter();
66
+ constructor(name, initialState) {
67
+ super();
68
+ this.name = name;
69
+ this.state = initialState;
70
+ this.initialState = State.clone(initialState);
71
+ }
72
+ /**
73
+ * Get State
74
+ *
75
+ * Returns a readonly snapshot of the current state
76
+ */
77
+ getState() {
78
+ return this.state;
79
+ }
80
+ /**
81
+ * Update
82
+ *
83
+ * Mutates state and notifies any open subscriptions. This method
84
+ * by default uses task batching for optimized performance. In almost
85
+ * every use-case, this method is the correct way to mutate state. If
86
+ * you need to bypass batching for higher-priority state updates, you
87
+ * can use `State.priorityUpdate()` or `State.backgroundUpdate()`
88
+ *
89
+ * ##### Synchronous updates
90
+ * ```typescript
91
+ * MyState.update((state, initialState) => {
92
+ * state.listItems.push(5);
93
+ * });
94
+ * ```
95
+ * ##### Asynchronous updates
96
+ * ```typescript
97
+ * MyState.update(async (state, initialState) => {
98
+ * const listItems = await fetch("/list-items");
99
+ * state.listItems = listItems;
100
+ * });
101
+ * ```
102
+ */
103
+ update = this.mutation((func) => {
104
+ return func(this.state, this.initialState);
105
+ }, Priority.BATCHED);
106
+ /**
107
+ * Background Update
108
+ *
109
+ * Mutates state and notifies any open subscriptions. This method
110
+ * bypasses Galena's internal task batching for a more immediate
111
+ * state update and propagation of state to consumers. It utilizes
112
+ * a micro-task that allows for the current call stack to clear
113
+ * ahead of propagating state updates to consumers
114
+ *
115
+ * ##### Synchronous updates
116
+ * ```typescript
117
+ * MyState.backgroundUpdate((state, initialState) => {
118
+ * state.listItems.push(5);
119
+ * });
120
+ * ```
121
+ * ##### Asynchronous updates
122
+ * ```typescript
123
+ * MyState.backgroundUpdate(async (state, initialState) => {
124
+ * const listItems = await fetch("/list-items");
125
+ * state.listItems = listItems;
126
+ * });
127
+ * ```
128
+ */
129
+ backgroundUpdate = this.mutation((func) => {
130
+ return func(this.state, this.initialState);
131
+ }, Priority.MICROTASK);
132
+ /**
133
+ * Priority Update
134
+ *
135
+ * Mutates state and notifies any open subscriptions. This method
136
+ * bypasses optimizations for task batching and scheduling. This means
137
+ * that state updates made with this method propagate to subscriptions
138
+ * as immediately as possible. Overusing this method can cause your
139
+ * state updates to perform slower in certain cases. The usage of this
140
+ * method should be conserved for state mutations that need to occur
141
+ * at a certain frame rate
142
+ *
143
+ * ##### Synchronous updates
144
+ * ```typescript
145
+ * MyState.priorityUpdate((state, initialState) => {
146
+ * state.listItems.push(5);
147
+ * });
148
+ * ```
149
+ * ##### Asynchronous updates
150
+ * ```typescript
151
+ * MyState.priorityUpdate(async (state, initialState) => {
152
+ * const listItems = await fetch("/list-items");
153
+ * state.listItems = listItems;
154
+ * });
155
+ * ```
156
+ */
157
+ priorityUpdate = this.mutation((func) => {
158
+ return func(this.state, this.initialState);
159
+ }, Priority.IMMEDIATE);
160
+ /**
161
+ * Reset
162
+ *
163
+ * Resets the current state to its initial state
164
+ */
165
+ reset = this.mutation(() => {
166
+ this.state = State.clone(this.initialState);
167
+ });
168
+ /**
169
+ * Mutation
170
+ *
171
+ * This method can be used to wrap arbitrary functions that when invoked
172
+ * will:
173
+ * 1. Notify your subscriptions with the latest state
174
+ * 2. Execute any registered middleware (such as loggers or profiling tools)
175
+ *
176
+ * Using this method, developers can compose and extend `Galena`'s internal
177
+ * infrastructure for state mutations to create proprietary models for your
178
+ * state
179
+ *
180
+ * ```typescript
181
+ * import { State } from "@figliolia/galena";
182
+ *
183
+ * // Extend of Galena State
184
+ * class MyState extends State {
185
+ * addListItem = mutation((newListItem) => {
186
+ * this.state.list.push(newListItem);
187
+ * });
188
+ * }
189
+ *
190
+ * // Create an instance
191
+ * const myState = new MyState("myState", { list: [] });
192
+ *
193
+ * // Invoke your custom mutation method
194
+ * myState.addListItem("new-item");
195
+ * ```
196
+ */
197
+ mutation(func, priority = Priority.BATCHED) {
198
+ return (...args) => {
199
+ this.lifeCycleEvent(MiddlewareEvents.onBeforeUpdate);
200
+ const returnValue = func(...args);
201
+ if (returnValue instanceof Promise) {
202
+ return returnValue.then((v) => {
203
+ this.scheduleUpdate(priority);
204
+ return v;
205
+ });
206
+ }
207
+ this.scheduleUpdate(priority);
208
+ return returnValue;
209
+ };
210
+ }
211
+ /**
212
+ * Schedule Update
213
+ *
214
+ * Schedules an update to State subscribers and emits the
215
+ * `onUpdate` lifecycle hook
216
+ */
217
+ scheduleUpdate(priority) {
218
+ this.lifeCycleEvent(MiddlewareEvents.onUpdate);
219
+ void this.scheduleTask(() => this.emitter.emit(this.name, this.state), priority);
220
+ }
221
+ /**
222
+ * Register Middleware
223
+ *
224
+ * Caches a `Middleware` instance and invokes its
225
+ * lifecycle subscriptions on all state transitions
226
+ */
227
+ registerMiddleware(...middleware) {
228
+ this.middleware.push(...middleware);
229
+ }
230
+ /**
231
+ * Subscribe
232
+ *
233
+ * Registers a subscription on the state instance. The
234
+ * callback you provide will execute each time state changes.
235
+ * Returns a unique identifier for your subscription
236
+ */
237
+ subscribe(callback) {
238
+ return this.emitter.on(this.name, callback);
239
+ }
240
+ /**
241
+ * Unsubscribe
242
+ *
243
+ * Given a subscription ID, removes a registered subscription
244
+ * from the `State` instance
245
+ */
246
+ unsubscribe(ID) {
247
+ return this.emitter.off(this.name, ID);
248
+ }
249
+ /**
250
+ * Life Cycle Event
251
+ *
252
+ * Triggers a life cycle event for each registered middleware
253
+ */
254
+ lifeCycleEvent(event) {
255
+ const maxIndex = this.middleware.length - 1;
256
+ for (let i = maxIndex; i > -1; i--) {
257
+ this.middleware[i][event](this);
258
+ }
259
+ }
260
+ /**
261
+ * Clone
262
+ *
263
+ * `State` instances accept any value as a form of reactive
264
+ * state. In order to maintain the initial state past any state
265
+ * transitions, this method clones the initial values provided
266
+ * to the `State` constructor and caches them to allow for
267
+ * developers to easily reset their current state back to its
268
+ * initial value
269
+ */
270
+ static clone(state) {
271
+ if (Array.isArray(state)) {
272
+ return [...state];
273
+ }
274
+ if (state instanceof Set) {
275
+ return new Set(state);
276
+ }
277
+ if (state instanceof Map) {
278
+ return new Map(state);
279
+ }
280
+ if (state && typeof state === "object") {
281
+ return { ...state };
282
+ }
283
+ return state;
284
+ }
285
+ }
@@ -0,0 +1,3 @@
1
+ export { Galena } from "./Galena.js";
2
+ export { State } from "./State.js";
3
+ export * from "./types.js";
@@ -0,0 +1,6 @@
1
+ export var Priority;
2
+ (function (Priority) {
3
+ Priority[Priority["IMMEDIATE"] = 1] = "IMMEDIATE";
4
+ Priority[Priority["MICROTASK"] = 2] = "MICROTASK";
5
+ Priority[Priority["BATCHED"] = 3] = "BATCHED";
6
+ })(Priority || (Priority = {}));
@@ -0,0 +1,42 @@
1
+ /**
2
+ * # Middleware
3
+ *
4
+ * A root interface for all `Galena` Middleware. When creating
5
+ * a middleware for your `Galena` state, simply extend this
6
+ * class any override any of its public lifecycle methods.
7
+ *
8
+ * ### Creating a Profiling Middleware
9
+ *
10
+ * ```typescript
11
+ * export class ProfilerMiddleware extends Middleware {
12
+ * updateState: number | null = null;
13
+ *
14
+ * override onBeforeUpdate(state: State) {
15
+ * this.updateStart = performance.now();
16
+ * }
17
+ *
18
+ * override onUpdate(state: State) {
19
+ * if(this.updateStart) {
20
+ * const timeToUpdate = performance.now() - this.updateStart;
21
+ * if(timeToUpdate > 16) {
22
+ * console.warn("A state transition took more than 16 milliseconds!", State);
23
+ * }
24
+ * }
25
+ * }
26
+ * }
27
+ * ```
28
+ */
29
+ export class Middleware {
30
+ /**
31
+ * On Before Update
32
+ *
33
+ * An event emitted each time a `State` mutation is enqueued
34
+ */
35
+ onBeforeUpdate(state) { }
36
+ /**
37
+ * On Update
38
+ *
39
+ * An event emitted each time a `State` instance is mutated
40
+ */
41
+ onUpdate(state) { }
42
+ }
@@ -0,0 +1,2 @@
1
+ export { Middleware } from "./Middleware.js";
2
+ export * from "./types.js";
@@ -0,0 +1,5 @@
1
+ export var MiddlewareEvents;
2
+ (function (MiddlewareEvents) {
3
+ MiddlewareEvents["onUpdate"] = "onUpdate";
4
+ MiddlewareEvents["onBeforeUpdate"] = "onBeforeUpdate";
5
+ })(MiddlewareEvents || (MiddlewareEvents = {}));
@@ -0,0 +1,44 @@
1
+ import { State } from "../Galena/State.js";
2
+ import { Middleware } from "../Middleware/Middleware.js";
3
+ /**
4
+ * Logger
5
+ *
6
+ * A middleware for Redux-style logging! Each state transition
7
+ * will log to the console the `State` instance that changed
8
+ * along with a before and after snapshot of the current state:
9
+ *
10
+ * ```typescript
11
+ * const State = new Galena([new Logger()]);
12
+ * // if using isolated state instances:
13
+ * const MyState = new State(...args);
14
+ * MyState.registerMiddleware(new Logger())
15
+ * ```
16
+ */
17
+ export class Logger extends Middleware {
18
+ previousState = null;
19
+ onBeforeUpdate(state) {
20
+ this.previousState = State.clone(state.state);
21
+ }
22
+ onUpdate(state) {
23
+ console.log("%cMutation:", "color: rgb(187, 186, 186); font-weight: bold", state.name, "@", this.time);
24
+ console.log(" %cPrevious State", "color: #26ad65; font-weight: bold", this.previousState);
25
+ console.log(" %cNext State ", "color: rgb(17, 118, 249); font-weight: bold", state.getState());
26
+ this.previousState = null;
27
+ }
28
+ /**
29
+ * Time
30
+ *
31
+ * Returns the time in which a given state transition completed
32
+ */
33
+ get time() {
34
+ const date = new Date();
35
+ const mHours = date.getHours();
36
+ const hours = mHours > 12 ? mHours - 12 : mHours;
37
+ const mins = date.getMinutes();
38
+ const minutes = mins.toString().length === 1 ? `0${mins}` : mins;
39
+ const secs = date.getSeconds();
40
+ const seconds = secs.toString().length === 1 ? `0${secs}` : secs;
41
+ const milliseconds = date.getMilliseconds();
42
+ return `${hours}:${minutes}:${seconds}:${milliseconds}`;
43
+ }
44
+ }
@@ -0,0 +1,35 @@
1
+ import { Middleware } from "../Middleware/Middleware.js";
2
+ /**
3
+ * Profiler
4
+ *
5
+ * A logger for state transitions exceeding a given threshold
6
+ * for duration:
7
+ *
8
+ * ```typescript
9
+ * const State = new Galena([new Profiler()]);
10
+ * // if using isolated state instances:
11
+ * const MyState = new State(...args);
12
+ * MyState.registerMiddleware(new Profiler())
13
+ * ```
14
+ */
15
+ export class Profiler extends Middleware {
16
+ threshold;
17
+ startTime = null;
18
+ constructor(threshold = 16) {
19
+ super();
20
+ this.threshold = threshold;
21
+ }
22
+ onBeforeUpdate(_) {
23
+ this.startTime = performance.now();
24
+ }
25
+ onUpdate(nextState) {
26
+ if (this.startTime) {
27
+ const endTime = performance.now();
28
+ const diff = endTime - this.startTime;
29
+ if (diff > this.threshold) {
30
+ console.warn("Slow state transition detected", nextState);
31
+ console.warn(`The last transition took ${diff}ms`);
32
+ }
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,2 @@
1
+ export { Logger } from "./Logger.js";
2
+ export { Profiler } from "./Profiler.js";
@@ -0,0 +1,3 @@
1
+ export * from "./Galena/index.js";
2
+ export * from "./Middleware/index.js";
3
+ export * from "./Middlewares/index.js";
@@ -0,0 +1,4 @@
1
+ {
2
+ "type": "module",
3
+ "exports": "./index.js"
4
+ }
package/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "@figliolia/galena",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
4
4
  "description": "A performant state management library supporting mutable state, batched updates, middleware and a rich development API",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
5
+ "main": "dist/cjs/index.js",
6
+ "module": "dist/mjs/index.js",
7
+ "types": "dist/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/mjs/index.js",
11
+ "require": "./dist/cjs/index.js",
12
+ "types": "./dist/types/index.d.ts"
13
+ }
14
+ },
7
15
  "files": [
8
16
  "dist",
9
17
  "src/*"
@@ -30,13 +38,14 @@
30
38
  "scripts": {
31
39
  "test": "jest",
32
40
  "coverage": "jest --env=jsdom --coverage --testResultsProcessor ./node_modules/jest-junit",
33
- "build": "rm -rf dist && tsc --project tsconfig.build.json && tsc-alias -p tsconfig.build.json",
41
+ "build": "npx ts-packager -e src",
34
42
  "lint": "tsc --noemit && eslint ./ --fix"
35
43
  },
36
44
  "dependencies": {
37
45
  "@figliolia/event-emitter": "^1.0.8"
38
46
  },
39
47
  "devDependencies": {
48
+ "@figliolia/ts-packager": "^1.0.3",
40
49
  "@types/node": "^16.7.13",
41
50
  "@typescript-eslint/eslint-plugin": "^5.59.1",
42
51
  "@typescript-eslint/parser": "^5.59.1",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes