@figliolia/galena 1.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.
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Galena = void 0;
4
+ const event_emitter_1 = require("@figliolia/event-emitter");
5
+ const State_1 = require("./State");
6
+ /**
7
+ * ## Galena
8
+ *
9
+ * A performant global state solution that scales
10
+ *
11
+ * ### Creating State
12
+ *
13
+ * ```typescript
14
+ * // AppState.ts
15
+ * import { Galena } from "../galena";
16
+ *
17
+ * const AppState = new Galena([...middleware]);
18
+ *
19
+ * const NavigationState = AppState.composeState("navigation", {
20
+ * currentRoute: "/",
21
+ * userID: "12345",
22
+ * permittedRoutes: ["/*"]
23
+ * });
24
+ * ```
25
+ *
26
+ * ### Subscribing to State Changes
27
+ * #### Using the Galena Instance
28
+ * ```typescript
29
+ * import { AppState } from "./AppState";
30
+ *
31
+ * AppState.subscribe(appState => {
32
+ * const navState = appState.get("navigation");
33
+ * const { currentRoute } = navState.state;
34
+ * // do something with state changes!
35
+ * });
36
+ * ```
37
+ * #### Using the State Instance
38
+ * ```typescript
39
+ * NavigationState.subscribe(navigation => {
40
+ * const { currentRoute } = navigation.state
41
+ * // do something with state changes!
42
+ * });
43
+ * ```
44
+ *
45
+ * #### Using Global Subscriptions
46
+ * ```typescript
47
+ * NavigationState.subscribeAll(galenaInstance => {
48
+ * const { currentRoute } = galenaInstance.get("navigation").state
49
+ * // do something with state changes!
50
+ * });
51
+ * ```
52
+ *
53
+ * ### Mutating State
54
+ * ```typescript
55
+ * NavigationState.update(state => {
56
+ * state.currentRoute = "/profile";
57
+ * // You can mutate state without creating new objects!
58
+ * });
59
+ * ```
60
+ */
61
+ class Galena {
62
+ constructor(middleware = []) {
63
+ this.state = {};
64
+ this.middleware = [];
65
+ this.IDs = new event_emitter_1.AutoIncrementingID();
66
+ this.subscriptions = new Map();
67
+ this.middleware = middleware;
68
+ }
69
+ /**
70
+ * Compose State
71
+ *
72
+ * Creates a new `State` instance and returns it. Your new state
73
+ * becomes immediately available on your `Galena` instance and
74
+ * is wired into your middleware. All existing subscriptions to
75
+ * state will automatically receive updates when your new unit of
76
+ * state updates
77
+ */
78
+ composeState(name, initialState,
79
+ // @ts-ignore
80
+ Model = (State_1.State)) {
81
+ const state = new Model(name, initialState);
82
+ state.registerMiddleware(...this.middleware);
83
+ this.mutable[name] = state;
84
+ this.reIndexSubscriptions(name);
85
+ return state;
86
+ }
87
+ /**
88
+ * Get
89
+ *
90
+ * Returns a unit of `State` by name
91
+ */
92
+ get(name) {
93
+ return this.state[name];
94
+ }
95
+ /**
96
+ * Mutable
97
+ *
98
+ * Returns a mutable state instance
99
+ */
100
+ get mutable() {
101
+ return this.state;
102
+ }
103
+ /**
104
+ * Update
105
+ *
106
+ * Runs a mutation on the specified unit of state
107
+ */
108
+ update(name, mutation) {
109
+ return this.get(name).update(mutation);
110
+ }
111
+ /**
112
+ * Background Update
113
+ *
114
+ * Runs a higher priority mutation on the specified unit of
115
+ * state
116
+ */
117
+ backgroundUpdate(name, mutation) {
118
+ return this.get(name).backgroundUpdate(mutation);
119
+ }
120
+ /**
121
+ * Priority Update
122
+ *
123
+ * Runs an immediate priority mutation on the specified unit
124
+ * of state
125
+ */
126
+ priorityUpdate(name, mutation) {
127
+ return this.get(name).priorityUpdate(mutation);
128
+ }
129
+ /**
130
+ * Subscribe
131
+ *
132
+ * Given the name of a unit of state, this method registers
133
+ * a subscription on the target state instance. The callback
134
+ * you provide will execute each time state changes. Returns
135
+ * a unique identifier for your subscription. To clean up your
136
+ * subscription, call `Galena.unsubscribe()` with the ID returned
137
+ * by this method
138
+ */
139
+ subscribe(name, mutation) {
140
+ return this.get(name).subscribe(mutation);
141
+ }
142
+ /**
143
+ * Unsubscribe
144
+ *
145
+ * Given a subscription ID returned from the `subscribe` method,
146
+ * this method removes and cleans up the corresponding subscription
147
+ */
148
+ unsubscribe(name, ID) {
149
+ return this.get(name).unsubscribe(ID);
150
+ }
151
+ /**
152
+ * Subscribe All
153
+ *
154
+ * Registers a callback on each registered `State` instance and
155
+ * is invoked each time your state changes. Using `Galena`'s
156
+ * `subscribeAll` method, although performant, can be less
157
+ * performant than subscribing directly to a target `State`
158
+ * instance using `Galena.subscribe()`. To clean up your
159
+ * subscription, call `Galena.unsubscribeAll()` with the ID
160
+ * returned
161
+ */
162
+ subscribeAll(callback) {
163
+ const subscriptionID = this.IDs.get();
164
+ const stateSubscriptions = [];
165
+ for (const key in this.state) {
166
+ stateSubscriptions.push([
167
+ key,
168
+ this.state[key].subscribe(() => {
169
+ callback(this);
170
+ }),
171
+ ]);
172
+ }
173
+ this.subscriptions.set(subscriptionID, stateSubscriptions);
174
+ return subscriptionID;
175
+ }
176
+ /**
177
+ * Unsubscribe
178
+ *
179
+ * Given a subscription ID returned from the `subscribeAll()` method,
180
+ * this method removes and cleans up the corresponding subscription
181
+ */
182
+ unsubscribeAll(ID) {
183
+ const IDs = this.subscriptions.get(ID);
184
+ if (IDs) {
185
+ for (const [state, ID] of IDs) {
186
+ this.state[state].unsubscribe(ID);
187
+ this.subscriptions.delete(ID);
188
+ }
189
+ }
190
+ }
191
+ /**
192
+ * ReIndex Subscriptions
193
+ *
194
+ * When units of state are created lazily, this method updates
195
+ * each existing subscription to receive mutations occurring on
196
+ * recently created `State` instances that post-date prior
197
+ * subscriptions
198
+ */
199
+ reIndexSubscriptions(name) {
200
+ var _a;
201
+ for (const [ID, unitSubscriptions] of this.subscriptions) {
202
+ for (const [state, subscriptionID] of unitSubscriptions) {
203
+ const callback = (_a = this.state[state]["emitter"]
204
+ .get(state)) === null || _a === void 0 ? void 0 : _a.get(subscriptionID);
205
+ if (callback) {
206
+ unitSubscriptions.push([
207
+ name,
208
+ this.state[name].subscribe(() => {
209
+ void callback(this.state);
210
+ }),
211
+ ]);
212
+ this.subscriptions.set(ID, unitSubscriptions);
213
+ break;
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+ exports.Galena = Galena;
@@ -0,0 +1,51 @@
1
+ import type { Task } from "./types";
2
+ import { Priority } from "./types";
3
+ /**
4
+ * Scheduler
5
+ *
6
+ * Scheduling dispatched events to state consumers is how Galena
7
+ * out-performs just about every state management library out there.
8
+ * The scheduler offers the ability to dispatch state updates on 3
9
+ * priorities:
10
+ *
11
+ * 1. Immediate - Immediate synchronous task execution and propagation of
12
+ * changes to consumers
13
+ * 2. Microtask - Immediate task execution and scheduled propagation of
14
+ * changes to consumers
15
+ * 3. Batched - Immediate task execution and batched propagation of
16
+ * changes to consumers
17
+ *
18
+ * This module manages the propagation of changes to State consumers
19
+ * by implementing the three priorities outlined above
20
+ */
21
+ export declare class Scheduler<T extends Task = Task> {
22
+ private task;
23
+ private schedule;
24
+ constructor();
25
+ /**
26
+ * Schedule Task
27
+ *
28
+ * Given a task (the emission of state changes to consumers)
29
+ * and a priority, this method executes the task on the priority
30
+ * level specified
31
+ */
32
+ protected scheduleTask(task: T, priority: Priority): void | Promise<void>;
33
+ /**
34
+ * Create Schedule
35
+ *
36
+ * Schedules the execution of the current task after 5 milliseconds
37
+ */
38
+ private createSchedule;
39
+ /**
40
+ * Clear Schedule
41
+ *
42
+ * Clears the schedule if it exists
43
+ */
44
+ private clearSchedule;
45
+ /**
46
+ * Execute Tasks
47
+ *
48
+ * Clears the schedule if it exists and executes the current task
49
+ */
50
+ private executeTasks;
51
+ }
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Scheduler = void 0;
4
+ const types_1 = require("./types");
5
+ /**
6
+ * Scheduler
7
+ *
8
+ * Scheduling dispatched events to state consumers is how Galena
9
+ * out-performs just about every state management library out there.
10
+ * The scheduler offers the ability to dispatch state updates on 3
11
+ * priorities:
12
+ *
13
+ * 1. Immediate - Immediate synchronous task execution and propagation of
14
+ * changes to consumers
15
+ * 2. Microtask - Immediate task execution and scheduled propagation of
16
+ * changes to consumers
17
+ * 3. Batched - Immediate task execution and batched propagation of
18
+ * changes to consumers
19
+ *
20
+ * This module manages the propagation of changes to State consumers
21
+ * by implementing the three priorities outlined above
22
+ */
23
+ class Scheduler {
24
+ constructor() {
25
+ this.task = null;
26
+ this.schedule = null;
27
+ this.executeTasks = this.executeTasks.bind(this);
28
+ }
29
+ /**
30
+ * Schedule Task
31
+ *
32
+ * Given a task (the emission of state changes to consumers)
33
+ * and a priority, this method executes the task on the priority
34
+ * level specified
35
+ */
36
+ scheduleTask(task, priority) {
37
+ this.task = task;
38
+ switch (priority) {
39
+ case types_1.Priority.IMMEDIATE:
40
+ return this.executeTasks();
41
+ case types_1.Priority.MICROTASK:
42
+ return Promise.resolve().then(() => {
43
+ return this.executeTasks();
44
+ });
45
+ case types_1.Priority.BATCHED:
46
+ default:
47
+ if (!this.schedule) {
48
+ this.createSchedule();
49
+ }
50
+ }
51
+ }
52
+ /**
53
+ * Create Schedule
54
+ *
55
+ * Schedules the execution of the current task after 5 milliseconds
56
+ */
57
+ createSchedule() {
58
+ this.clearSchedule();
59
+ this.schedule = setTimeout(this.executeTasks, 5);
60
+ }
61
+ /**
62
+ * Clear Schedule
63
+ *
64
+ * Clears the schedule if it exists
65
+ */
66
+ clearSchedule() {
67
+ if (this.schedule !== null) {
68
+ clearTimeout(this.schedule);
69
+ this.schedule = null;
70
+ }
71
+ }
72
+ /**
73
+ * Execute Tasks
74
+ *
75
+ * Clears the schedule if it exists and executes the current task
76
+ */
77
+ executeTasks() {
78
+ var _a;
79
+ this.clearSchedule();
80
+ (_a = this.task) === null || _a === void 0 ? void 0 : _a.call(this);
81
+ this.task = null;
82
+ }
83
+ }
84
+ exports.Scheduler = Scheduler;
@@ -0,0 +1,228 @@
1
+ import type { Middleware } from "../Middleware/Middleware";
2
+ import { Priority } from "./types";
3
+ import { Scheduler } from "./Scheduler";
4
+ /**
5
+ * ### State
6
+ *
7
+ * The root of all reactivity in Galena. State instances can
8
+ * operate in isolation by calling `new State(...args)` or as
9
+ * part of your application's larger global state by using
10
+ * `new Galena().composeState()`.
11
+ *
12
+ * `State` instances operate on the premise of pub-sub and mutability.
13
+ * This provides significant performance improvement over more traditional
14
+ * state management tools because
15
+ *
16
+ * 1. Mutations can occur in O(1) space
17
+ * 2. Mutations can be batched when dispatching updates to subscribers
18
+ *
19
+ * When deciding how many `State` instances are required for your
20
+ * applications needs, we suggest creating and organizing state in
21
+ * accordance with your application logic. Meaning, you might have a
22
+ * `State` instance for navigation/routing, another `State` instance
23
+ * for storing user information, and so on. Performance can improve
24
+ * significantly when state is dispersed amongst multiple instances
25
+ *
26
+ * #### Creating State Instances
27
+ *
28
+ * ```typescript
29
+ * const MyState = new State("MyState", {
30
+ * someData: true,
31
+ * listItems: [1, 2, 3, 4];
32
+ * // ...etc
33
+ * });
34
+ * ```
35
+ *
36
+ * #### Updating State
37
+ * ##### Synchronous updates
38
+ * ```typescript
39
+ * MyState.update((state) => {
40
+ * state.listItems.push(5);
41
+ * });
42
+ * ```
43
+ * ##### Asynchronous updates
44
+ * ```typescript
45
+ * MyState.update(async (state) => {
46
+ * const listItems = await fetch("/list-items");
47
+ * state.listItems = listItems;
48
+ * });
49
+ * ```
50
+ *
51
+ * #### Subscribing to State Changes
52
+ * ```typescript
53
+ * MyState.subscribe(({ state }) => {
54
+ * const { listItems } = state
55
+ * // Do something with your list items!
56
+ * });
57
+ * ```
58
+ */
59
+ export declare class State<T extends any = any> extends Scheduler {
60
+ state: T;
61
+ readonly name: string;
62
+ readonly initialState: T;
63
+ private readonly middleware;
64
+ private readonly emitter;
65
+ constructor(name: string, initialState: T);
66
+ /**
67
+ * Get State
68
+ *
69
+ * Returns a readonly snapshot of the current state
70
+ */
71
+ getState(): Readonly<T>;
72
+ /**
73
+ * Update
74
+ *
75
+ * Mutates state and notifies any open subscriptions. This method
76
+ * by default uses task batching for optimized performance. In almost
77
+ * every use-case, this method is the correct way to mutate state. If
78
+ * you need to bypass batching for higher-priority state updates, you
79
+ * can use `State.priorityUpdate()` or `State.backgroundUpdate()`
80
+ *
81
+ * ##### Synchronous updates
82
+ * ```typescript
83
+ * MyState.update((state, initialState) => {
84
+ * state.listItems.push(5);
85
+ * });
86
+ * ```
87
+ * ##### Asynchronous updates
88
+ * ```typescript
89
+ * MyState.update(async (state, initialState) => {
90
+ * const listItems = await fetch("/list-items");
91
+ * state.listItems = listItems;
92
+ * });
93
+ * ```
94
+ */
95
+ update: (func: (state: T, initialState: T) => void | Promise<void>) => any;
96
+ /**
97
+ * Background Update
98
+ *
99
+ * Mutates state and notifies any open subscriptions. This method
100
+ * bypasses Galena's internal task batching for a more immediate
101
+ * state update and propagation of state to consumers. It utilizes
102
+ * a micro-task that allows for the current call stack to clear
103
+ * ahead of propagating state updates to consumers
104
+ *
105
+ * ##### Synchronous updates
106
+ * ```typescript
107
+ * MyState.backgroundUpdate((state, initialState) => {
108
+ * state.listItems.push(5);
109
+ * });
110
+ * ```
111
+ * ##### Asynchronous updates
112
+ * ```typescript
113
+ * MyState.backgroundUpdate(async (state, initialState) => {
114
+ * const listItems = await fetch("/list-items");
115
+ * state.listItems = listItems;
116
+ * });
117
+ * ```
118
+ */
119
+ backgroundUpdate: (func: (state: T, initialState: T) => void | Promise<void>) => any;
120
+ /**
121
+ * Priority Update
122
+ *
123
+ * Mutates state and notifies any open subscriptions. This method
124
+ * bypasses optimizations for task batching and scheduling. This means
125
+ * that state updates made with this method propagate to subscriptions
126
+ * as immediately as possible. Overusing this method can cause your
127
+ * state updates to perform slower in certain cases. The usage of this
128
+ * method should be conserved for state mutations that need to occur
129
+ * at a certain frame rate
130
+ *
131
+ * ##### Synchronous updates
132
+ * ```typescript
133
+ * MyState.priorityUpdate((state, initialState) => {
134
+ * state.listItems.push(5);
135
+ * });
136
+ * ```
137
+ * ##### Asynchronous updates
138
+ * ```typescript
139
+ * MyState.priorityUpdate(async (state, initialState) => {
140
+ * const listItems = await fetch("/list-items");
141
+ * state.listItems = listItems;
142
+ * });
143
+ * ```
144
+ */
145
+ priorityUpdate: (func: (state: T, initialState: T) => void | Promise<void>) => any;
146
+ /**
147
+ * Reset
148
+ *
149
+ * Resets the current state to its initial state
150
+ */
151
+ reset: () => any;
152
+ /**
153
+ * Mutation
154
+ *
155
+ * This method can be used to wrap arbitrary functions that when invoked
156
+ * will:
157
+ * 1. Notify your subscriptions with the latest state
158
+ * 2. Execute any registered middleware (such as loggers or profiling tools)
159
+ *
160
+ * Using this method, developers can compose and extend `Galena`'s internal
161
+ * infrastructure for state mutations to create proprietary models for your
162
+ * state
163
+ *
164
+ * ```typescript
165
+ * import { State } from "../galena";
166
+ *
167
+ * // Extend of Galena State
168
+ * class MyState extends State {
169
+ * addListItem = mutation((newListItem) => {
170
+ * this.state.list.push(newListItem);
171
+ * });
172
+ * }
173
+ *
174
+ * // Create an instance
175
+ * const myState = new MyState("myState", { list: [] });
176
+ *
177
+ * // Invoke your custom mutation method
178
+ * myState.addListItem("new-item");
179
+ * ```
180
+ */
181
+ protected mutation<F extends (...args: any[]) => any>(func: F, priority?: Priority): (...args: Parameters<F>) => any;
182
+ /**
183
+ * Schedule Update
184
+ *
185
+ * Schedules an update to State subscribers and emits the
186
+ * `onUpdate` lifecycle hook
187
+ */
188
+ private scheduleUpdate;
189
+ /**
190
+ * Register Middleware
191
+ *
192
+ * Caches a `Middleware` instance and invokes its
193
+ * lifecycle subscriptions on all state transitions
194
+ */
195
+ registerMiddleware(...middleware: Middleware[]): void;
196
+ /**
197
+ * Subscribe
198
+ *
199
+ * Registers a subscription on the state instance. The
200
+ * callback you provide will execute each time state changes.
201
+ * Returns a unique identifier for your subscription
202
+ */
203
+ subscribe(callback: (nextState: State<T>) => void): string;
204
+ /**
205
+ * Unsubscribe
206
+ *
207
+ * Given a subscription ID, removes a registered subscription
208
+ * from the `State` instance
209
+ */
210
+ unsubscribe(ID: string): boolean | undefined;
211
+ /**
212
+ * Life Cycle Event
213
+ *
214
+ * Triggers a life cycle event for each registered middleware
215
+ */
216
+ private lifeCycleEvent;
217
+ /**
218
+ * Clone
219
+ *
220
+ * `State` instances accept any value as a form of reactive
221
+ * state. In order to maintain the initial state past any state
222
+ * transitions, this method clones the initial values provided
223
+ * to the `State` constructor and caches them to allow for
224
+ * developers to easily reset their current state back to its
225
+ * initial value
226
+ */
227
+ static clone<T>(state: T): T;
228
+ }