@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.
Files changed (144) 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/GalenaProvider.cjs +50 -0
  10. package/dist/GalenaProvider.d.cts +35 -0
  11. package/dist/GalenaProvider.d.cts.map +1 -0
  12. package/dist/GalenaProvider.d.mts +35 -0
  13. package/dist/GalenaProvider.d.mts.map +1 -0
  14. package/dist/GalenaProvider.mjs +52 -0
  15. package/dist/GalenaProvider.mjs.map +1 -0
  16. package/dist/Logger.cjs +46 -0
  17. package/dist/Logger.d.cts +35 -0
  18. package/dist/Logger.d.cts.map +1 -0
  19. package/dist/Logger.d.mts +35 -0
  20. package/dist/Logger.d.mts.map +1 -0
  21. package/dist/Logger.mjs +48 -0
  22. package/dist/Logger.mjs.map +1 -0
  23. package/dist/Middleware.cjs +62 -0
  24. package/dist/Middleware.d.cts +65 -0
  25. package/dist/Middleware.d.cts.map +1 -0
  26. package/dist/Middleware.d.mts +65 -0
  27. package/dist/Middleware.d.mts.map +1 -0
  28. package/dist/Middleware.mjs +64 -0
  29. package/dist/Middleware.mjs.map +1 -0
  30. package/dist/Profiler.cjs +41 -0
  31. package/dist/Profiler.d.cts +31 -0
  32. package/dist/Profiler.d.cts.map +1 -0
  33. package/dist/Profiler.d.mts +31 -0
  34. package/dist/Profiler.d.mts.map +1 -0
  35. package/dist/Profiler.mjs +43 -0
  36. package/dist/Profiler.mjs.map +1 -0
  37. package/dist/State.cjs +147 -0
  38. package/dist/State.d.cts +114 -0
  39. package/dist/State.d.cts.map +1 -0
  40. package/dist/State.d.mts +114 -0
  41. package/dist/State.d.mts.map +1 -0
  42. package/dist/State.mjs +148 -0
  43. package/dist/State.mjs.map +1 -0
  44. package/dist/commonHooks.cjs +17 -0
  45. package/dist/commonHooks.mjs +18 -0
  46. package/dist/commonHooks.mjs.map +1 -0
  47. package/dist/connect-multi.cjs +42 -0
  48. package/dist/connect-multi.d.cts +10 -0
  49. package/dist/connect-multi.d.cts.map +1 -0
  50. package/dist/connect-multi.d.mts +10 -0
  51. package/dist/connect-multi.d.mts.map +1 -0
  52. package/dist/connect-multi.mjs +44 -0
  53. package/dist/connect-multi.mjs.map +1 -0
  54. package/dist/connect.cjs +41 -0
  55. package/dist/connect.d.cts +9 -0
  56. package/dist/connect.d.cts.map +1 -0
  57. package/dist/connect.d.mts +9 -0
  58. package/dist/connect.d.mts.map +1 -0
  59. package/dist/connect.mjs +43 -0
  60. package/dist/connect.mjs.map +1 -0
  61. package/dist/createUseState.cjs +47 -0
  62. package/dist/createUseState.d.cts +46 -0
  63. package/dist/createUseState.d.cts.map +1 -0
  64. package/dist/createUseState.d.mts +46 -0
  65. package/dist/createUseState.d.mts.map +1 -0
  66. package/dist/createUseState.mjs +49 -0
  67. package/dist/createUseState.mjs.map +1 -0
  68. package/dist/index.cjs +23 -0
  69. package/dist/index.d.cts +12 -0
  70. package/dist/index.d.mts +12 -0
  71. package/dist/index.mjs +11 -0
  72. package/dist/types.d.cts +23 -0
  73. package/dist/types.d.cts.map +1 -0
  74. package/dist/types.d.mts +23 -0
  75. package/dist/types.d.mts.map +1 -0
  76. package/dist/useState.cjs +45 -0
  77. package/dist/useState.d.cts +43 -0
  78. package/dist/useState.d.cts.map +1 -0
  79. package/dist/useState.d.mts +43 -0
  80. package/dist/useState.d.mts.map +1 -0
  81. package/dist/useState.mjs +47 -0
  82. package/dist/useState.mjs.map +1 -0
  83. package/media/Logging.png +0 -0
  84. package/media/Profiling.png +0 -0
  85. package/package.json +38 -59
  86. package/src/Galena.ts +120 -0
  87. package/src/{Middlewares/Logger.ts → Logger.ts} +15 -14
  88. package/src/Middleware.ts +62 -0
  89. package/src/Profiler.ts +53 -0
  90. package/src/State.ts +167 -0
  91. package/src/index.ts +6 -3
  92. package/src/types.ts +28 -0
  93. package/dist/cjs/Galena/Galena.js +0 -223
  94. package/dist/cjs/Galena/Guards.js +0 -40
  95. package/dist/cjs/Galena/Scheduler.js +0 -84
  96. package/dist/cjs/Galena/State.js +0 -314
  97. package/dist/cjs/Galena/index.js +0 -22
  98. package/dist/cjs/Galena/types.js +0 -9
  99. package/dist/cjs/Middleware/Middleware.js +0 -46
  100. package/dist/cjs/Middleware/index.js +0 -20
  101. package/dist/cjs/Middleware/types.js +0 -8
  102. package/dist/cjs/Middlewares/Logger.js +0 -51
  103. package/dist/cjs/Middlewares/Profiler.js +0 -38
  104. package/dist/cjs/Middlewares/index.js +0 -7
  105. package/dist/cjs/index.js +0 -19
  106. package/dist/cjs/package.json +0 -3
  107. package/dist/mjs/Galena/Galena.js +0 -218
  108. package/dist/mjs/Galena/Guards.js +0 -36
  109. package/dist/mjs/Galena/Scheduler.js +0 -79
  110. package/dist/mjs/Galena/State.js +0 -313
  111. package/dist/mjs/Galena/index.js +0 -3
  112. package/dist/mjs/Galena/types.js +0 -6
  113. package/dist/mjs/Middleware/Middleware.js +0 -42
  114. package/dist/mjs/Middleware/index.js +0 -2
  115. package/dist/mjs/Middleware/types.js +0 -5
  116. package/dist/mjs/Middlewares/Logger.js +0 -44
  117. package/dist/mjs/Middlewares/Profiler.js +0 -35
  118. package/dist/mjs/Middlewares/index.js +0 -2
  119. package/dist/mjs/index.js +0 -3
  120. package/dist/mjs/package.json +0 -4
  121. package/dist/types/Galena/Galena.d.ts +0 -160
  122. package/dist/types/Galena/Guards.d.ts +0 -29
  123. package/dist/types/Galena/Scheduler.d.ts +0 -51
  124. package/dist/types/Galena/State.d.ts +0 -235
  125. package/dist/types/Galena/index.d.ts +0 -3
  126. package/dist/types/Galena/types.d.ts +0 -13
  127. package/dist/types/Middleware/Middleware.d.ts +0 -43
  128. package/dist/types/Middleware/index.d.ts +0 -2
  129. package/dist/types/Middleware/types.d.ts +0 -4
  130. package/dist/types/Middlewares/Logger.d.ts +0 -27
  131. package/dist/types/Middlewares/Profiler.d.ts +0 -22
  132. package/dist/types/Middlewares/index.d.ts +0 -2
  133. package/dist/types/index.d.ts +0 -3
  134. package/src/Galena/Galena.ts +0 -252
  135. package/src/Galena/Guards.ts +0 -49
  136. package/src/Galena/Scheduler.ts +0 -85
  137. package/src/Galena/State.ts +0 -344
  138. package/src/Galena/index.ts +0 -3
  139. package/src/Galena/types.ts +0 -18
  140. package/src/Middleware/Middleware.ts +0 -45
  141. package/src/Middleware/index.ts +0 -2
  142. package/src/Middleware/types.ts +0 -4
  143. package/src/Middlewares/Profiler.ts +0 -41
  144. package/src/Middlewares/index.ts +0 -2
package/package.json CHANGED
@@ -1,73 +1,52 @@
1
1
  {
2
2
  "name": "@figliolia/galena",
3
- "version": "2.3.5",
4
- "description": "A performant state management library supporting mutable state, batched updates, middleware and a rich development API",
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
- },
15
- "files": [
16
- "dist",
17
- "src/*"
18
- ],
19
- "author": "Alex Figliolia",
20
- "license": "MIT",
3
+ "version": "3.0.0",
4
+ "description": "A performant state management library supporting middleware and a rich developer API",
21
5
  "homepage": "https://github.com/alexfigliolia/galena#readme",
6
+ "license": "MIT",
7
+ "author": "Alex Figliolia",
22
8
  "repository": {
23
9
  "type": "git",
24
10
  "url": "git+https://github.com/alexfigliolia/galena.git"
25
11
  },
26
- "keywords": [
27
- "state",
28
- "state management",
29
- "mutable",
30
- "extendable",
31
- "event",
32
- "emitter",
33
- "flux",
34
- "island",
35
- "batch",
36
- "performance"
12
+ "files": [
13
+ "dist",
14
+ "src",
15
+ "media"
37
16
  ],
38
- "scripts": {
39
- "build": "ts-packager -e src",
40
- "lint": "tsx ci/commands/Lint.ts",
41
- "test": "tsx ci/commands/Test.ts"
17
+ "main": "./dist/index.cjs",
18
+ "module": "./dist/index.mjs",
19
+ "types": "./dist/index.d.cts",
20
+ "exports": {
21
+ ".": {
22
+ "import": "./dist/index.mjs",
23
+ "require": "./dist/index.cjs"
24
+ },
25
+ "./package.json": "./package.json"
42
26
  },
43
- "dependencies": {
44
- "@figliolia/event-emitter": "^1.1.5"
27
+ "publishConfig": {
28
+ "access": "public"
45
29
  },
46
- "peerDependencies": {
47
- "@figliolia/event-emitter": "^1.1.5"
30
+ "scripts": {
31
+ "build": "tsdown",
32
+ "lint": "oxlint --type-aware --type-check --report-unused-disable-directives --fix && oxfmt"
48
33
  },
49
34
  "devDependencies": {
50
- "@figliolia/child-process": "^1.0.1",
51
- "@figliolia/ts-packager": "^1.1.0",
52
- "@types/node": "^20.11.19",
53
- "@typescript-eslint/eslint-plugin": "^5.59.1",
54
- "@typescript-eslint/parser": "^5.59.1",
55
- "eslint": "^8.39.0",
56
- "eslint-config-airbnb": "^19.0.4",
57
- "eslint-config-airbnb-typescript": "^17.0.0",
58
- "eslint-config-prettier": "^8.8.0",
59
- "eslint-import-resolver-typescript": "^3.6.1",
60
- "eslint-plugin-import": "^2.27.5",
61
- "eslint-plugin-json-format": "^2.0.1",
62
- "eslint-plugin-prettier": "^5.1.3",
63
- "eslint-plugin-simple-import-sort": "^10.0.0",
64
- "eslint-plugin-unused-imports": "3",
65
- "prettier": "^3.2.4",
66
- "tsc-alias": "^1.8.8",
67
- "tsx": "^4.7.1",
68
- "typescript": "^5.3.3"
35
+ "@figliolia/event-emitter": "^1.2.0",
36
+ "@typescript-eslint/eslint-plugin": "^8.57.0",
37
+ "@typescript-eslint/parser": "^8.57.0",
38
+ "eslint": "^10.0.3",
39
+ "eslint-import-resolver-typescript": "^4.4.4",
40
+ "eslint-plugin-import": "^2.32.0",
41
+ "eslint-plugin-simple-import-sort": "^12.1.1",
42
+ "eslint-plugin-unused-imports": "^4.4.1",
43
+ "oxfmt": "^0.42.0",
44
+ "oxlint": "^1.36.0",
45
+ "oxlint-tsgolint": "^0.17.0",
46
+ "tsdown": "^0.21.1",
47
+ "typescript": "^5.9.3"
69
48
  },
70
- "publishConfig": {
71
- "access": "public"
49
+ "peerDependencies": {
50
+ "@figliolia/event-emitter": ">=1.2.0"
72
51
  }
73
- }
52
+ }
package/src/Galena.ts ADDED
@@ -0,0 +1,120 @@
1
+ import { EventEmitter } from "@figliolia/event-emitter";
2
+ import type { Middleware } from "./Middleware";
3
+ import type { State } from "./State";
4
+ import type { AppSubscriber, GalenaSnapshot, StateTypes } from "./types";
5
+
6
+ /**
7
+ * Galena
8
+ *
9
+ * Galena is designed to house one or more units of `State`
10
+ * and exist as a pseudo global application state.
11
+ *
12
+ * By design, each of its `State` units have isolated reactivity
13
+ * that prevents entire state trees from updating when a single
14
+ * unit changes.
15
+ *
16
+ * This is dissimilar to redux-like models where downstream reconciliations
17
+ * will propagate everwhere a given store is read from. In galena, downstream
18
+ * reconciliations occur only for consumers of the slice of state that
19
+ * changed - making it safer to use with more frequent state changes.
20
+ *
21
+ * ```typescript
22
+ * import { Galena } from "@figliolia/galena";
23
+ *
24
+ * const AppState = new Galena({
25
+ * // your reactive instances
26
+ * }, ...middleware);
27
+ *
28
+ * // to retreive and work with an individual unit
29
+ * const myUnit = AppState.get("<key>"); // Returns State<T>
30
+ *
31
+ * // to run a callback anytime a unit of state changes
32
+ * const unsubscribe = AppState.subscribe(({ updated }) => {
33
+ * // do something with the `State` instance that updated
34
+ * });
35
+ * ```
36
+ */
37
+ export class Galena<T extends Record<string, State<any>>> {
38
+ private Emitter = new EventEmitter<{ change: GalenaSnapshot<T> }>();
39
+ constructor(
40
+ public readonly state: T,
41
+ ...middleware: Middleware<StateTypes<T>>[]
42
+ ) {
43
+ this.registerMiddleware(...middleware);
44
+ }
45
+
46
+ public get<K extends Extract<keyof T, string>>(key: K) {
47
+ return this.state[key];
48
+ }
49
+
50
+ public subscribe = (subscriber: AppSubscriber<T>) => {
51
+ const ID = this.Emitter.on("change", subscriber);
52
+ const unsubscribers: (() => void)[] = [];
53
+ for (const key in this.state) {
54
+ const instance = this.state[key];
55
+ if (!instance) {
56
+ continue;
57
+ }
58
+ unsubscribers.push(
59
+ instance.subscribe(() =>
60
+ this.emit({ state: this.state, updated: instance }),
61
+ ),
62
+ );
63
+ }
64
+ return () => {
65
+ this.Emitter.off("change", ID);
66
+ while (unsubscribers.length) {
67
+ unsubscribers.pop?.()?.();
68
+ }
69
+ };
70
+ };
71
+
72
+ public registerMiddleware(...middlewares: Middleware<StateTypes<T>>[]) {
73
+ for (const key in this.state) {
74
+ this.state[key]?.registerMiddleware?.(...middlewares);
75
+ }
76
+ }
77
+
78
+ private emit<K extends Extract<keyof T, string>>(
79
+ event: GalenaSnapshot<T, K>,
80
+ ) {
81
+ this.Emitter.emit("change", event);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Create Galena
87
+ *
88
+ * Galena is designed to house one or more units of `State`
89
+ * and exist as a pseudo global application state.
90
+ *
91
+ * By design, each of its `State` units have isolated reactivity
92
+ * that prevents entire state trees from updating when a single
93
+ * unit changes.
94
+ *
95
+ * This is dissimilar to redux-like models where downstream reconciliations
96
+ * will propagate everwhere a given store is read from. In galena, downstream
97
+ * reconciliations occur only for consumers of the slice of state that
98
+ * changed - making it safer to use with more frequent state changes.
99
+ *
100
+ * ```typescript
101
+ * import { createGalena } from "@figliolia/galena";
102
+ *
103
+ * const AppState = createGalena({
104
+ * // your reactive instances
105
+ * }, ...middleware);
106
+ *
107
+ * // to retreive and work with an individual unit
108
+ * const myUnit = AppState.get("<key>"); // Returns State<T>
109
+ *
110
+ * // to run a callback anytime a unit of state changes
111
+ * const unsubscribe = AppState.subscribe(({ updated }) => {
112
+ * // do something with the `State` instance that updated
113
+ * });
114
+ * ```
115
+ */
116
+ export const createGalena = <T extends Record<string, State<any>>>(
117
+ ...args: ConstructorParameters<typeof Galena<T>>
118
+ ) => {
119
+ return new Galena(...args);
120
+ };
@@ -1,5 +1,5 @@
1
- import { State } from "Galena/State";
2
- import { Middleware } from "Middleware/Middleware";
1
+ import { Middleware } from "./Middleware";
2
+ import type { State } from "./State";
3
3
 
4
4
  /**
5
5
  * Logger
@@ -9,24 +9,26 @@ import { Middleware } from "Middleware/Middleware";
9
9
  * along with a before and after snapshot of the current state:
10
10
  *
11
11
  * ```typescript
12
- * const State = new Galena([new Logger()]);
13
- * // if using isolated state instances:
14
- * const MyState = new State(...args);
15
- * MyState.registerMiddleware(new Logger())
12
+ * const AppState = new Galena({}, new Logger());
13
+ * // or
14
+ * AppState.registerMiddlerware(new Logger());
15
+ * // or
16
+ * const MyState = new State(4, new Logger());
17
+ * // or
18
+ * MyState.registerMiddleware(new Logger());
16
19
  * ```
17
20
  */
18
- export class Logger extends Middleware {
19
- private previousState: Record<string, any> | null = null;
21
+ export class Logger<T = any> extends Middleware {
22
+ private previousState: T | null = null;
20
23
 
21
- override onBeforeUpdate(state: State) {
22
- this.previousState = State.clone(state.state);
24
+ override onBeforeUpdate(state: State<T>) {
25
+ this.previousState = state.getSnapshot();
23
26
  }
24
27
 
25
- override onUpdate(state: State) {
28
+ override onUpdate(state: State<T>) {
26
29
  console.log(
27
30
  "%cMutation:",
28
31
  "color: rgb(187, 186, 186); font-weight: bold",
29
- state.name,
30
32
  "@",
31
33
  this.time,
32
34
  );
@@ -38,9 +40,8 @@ export class Logger extends Middleware {
38
40
  console.log(
39
41
  " %cNext State ",
40
42
  "color: rgb(17, 118, 249); font-weight: bold",
41
- state.getState(),
43
+ state.getSnapshot(),
42
44
  );
43
- this.previousState = null;
44
45
  }
45
46
 
46
47
  /**
@@ -0,0 +1,62 @@
1
+ import type { State } from "./State";
2
+
3
+ /**
4
+ * Middleware
5
+ *
6
+ * Galena's middleware API is designed to provide hooks
7
+ * for state changes that you can tap into to run your
8
+ * own logic.
9
+ *
10
+ * Middleware is great for logging, analytics, and profiling:
11
+ *
12
+ * ```typescript
13
+ * export class Profiler<T = any> extends Middleware<T> {
14
+ * private previousState: T | null = null;
15
+ * private startTime: null | number = null;
16
+ * constructor(public readonly threshold: number = 16) {
17
+ * super();
18
+ * }
19
+ *
20
+ * public override onBeforeUpdate(state: State<T>) {
21
+ * this.startTime = performance.now();
22
+ * this.previousState = state.getSnapshot();
23
+ * }
24
+ *
25
+ * public override onUpdate(state: T) {
26
+ * const diff = performance.now() - this.startTime;
27
+ * if(diff >= this.threshold) {
28
+ * console.warn(`A slow state transition was detected when transitioning the following piece of state`);
29
+ * console.log('Previous state', this.previousState);
30
+ * console.log('Current state', state.getSnapshot());
31
+ * }
32
+ * }
33
+ * }
34
+ * ```
35
+ *
36
+ * To register your middleware, simply add it when constructing
37
+ * a `State` or `Galena` instance.
38
+ *
39
+ * ```typescript
40
+ * import { State } from "@figliolia/galena";
41
+ * import { Profiler } from './myProfiler';
42
+ *
43
+ * const myState = new State(5, new Profiler());
44
+ * ```
45
+ */
46
+ export class Middleware<T = any> {
47
+ /**
48
+ * On Before Update
49
+ *
50
+ * Executes prior to a `State` instance being updated.
51
+ * Receives the state prior to its update as a parameter
52
+ */
53
+ public onBeforeUpdate(_state: State<T>) {}
54
+
55
+ /**
56
+ * On Update
57
+ *
58
+ * Executes after a `State` instance has been update.
59
+ * Receives the most recent state as a parameter
60
+ */
61
+ public onUpdate(_state: State<T>) {}
62
+ }
@@ -0,0 +1,53 @@
1
+ import { Middleware } from "./Middleware";
2
+ import type { State } from "./State";
3
+
4
+ /**
5
+ * Profiler
6
+ *
7
+ * A logger for state transitions exceeding a given
8
+ * millisecond threshold
9
+ *
10
+ * ```typescript
11
+ * const AppState = new Galena({}, new Profiler());
12
+ * // or
13
+ * AppState.registerMiddlerware(new Profiler());
14
+ * // or
15
+ * const MyState = new State(4, new Profiler());
16
+ * // or
17
+ * MyState.registerMiddleware(new Profiler());
18
+ * ```
19
+ */
20
+ export class Profiler<T = any> extends Middleware<T> {
21
+ private previousState: T | null = null;
22
+ private startTime: null | number = null;
23
+ constructor(public readonly threshold = 16) {
24
+ super();
25
+ }
26
+
27
+ public override onBeforeUpdate(state: State<T>) {
28
+ this.startTime = performance.now();
29
+ this.previousState = state.getSnapshot();
30
+ }
31
+
32
+ public override onUpdate(state: State<T>) {
33
+ if (this.startTime === null) {
34
+ return;
35
+ }
36
+ const diff = performance.now() - this.startTime;
37
+ if (diff >= this.threshold) {
38
+ console.warn(
39
+ `A slow state transition of ${diff.toFixed(1)}ms was detected when transitioning the following piece of state`,
40
+ );
41
+ console.log(
42
+ " %cPrevious State",
43
+ "color: #26ad65; font-weight: bold",
44
+ this.previousState,
45
+ );
46
+ console.log(
47
+ " %cCurrent State ",
48
+ "color: rgb(17, 118, 249); font-weight: bold",
49
+ state.getSnapshot(),
50
+ );
51
+ }
52
+ }
53
+ }
package/src/State.ts ADDED
@@ -0,0 +1,167 @@
1
+ import { EventEmitter } from "@figliolia/event-emitter";
2
+ import type { Middleware } from "./Middleware";
3
+ import type { NonFunction, Setter, Subscriber } from "./types";
4
+
5
+ /**
6
+ * State
7
+ *
8
+ * The unit of reactivity for Galena. `State`'s can act
9
+ * as isolated instances or be part of your global app
10
+ * state (via `Galena` instances).
11
+ *
12
+ * There are three ways to create state instances
13
+ *
14
+ * ```typescript
15
+ * import { State, createState, useState, Profiler } from "@figliolia/galena";
16
+ * // for island states that can be shared between react components
17
+ * const myState = new State("<any value>", ...middleware);
18
+ * // or
19
+ * const myState = createState("<any value>", ...middleware);
20
+ *
21
+ * myState.set("<new-value>");
22
+ * myState.update(previousValue => "<new-value>");
23
+ * myState.subscribe(nextValue => {});
24
+ * myState.registerMiddleware(new Profiler());
25
+ *
26
+ * // Similarly if you wish to use your state inside a react component
27
+ * const MyComponent = () => {
28
+ * const [state, setState] = useState(myState);
29
+ * // or
30
+ * const [state, setState] = useMyState("<any-value>", ...middlware);
31
+ *
32
+ * return (
33
+ * // your jsx
34
+ * );
35
+ * }
36
+ * ```
37
+ */
38
+ export class State<T> {
39
+ private state: NonFunction<T>;
40
+ public readonly middleware: Middleware<T>[] = [];
41
+ private readonly Emitter = new EventEmitter<{ change: NonFunction<T> }>();
42
+ constructor(
43
+ public readonly initialState: NonFunction<T>,
44
+ ...middleware: Middleware<T>[]
45
+ ) {
46
+ this.state = initialState;
47
+ this.registerMiddleware(...middleware);
48
+ }
49
+
50
+ /**
51
+ * Set
52
+ *
53
+ * Updates the current value of state notifying
54
+ * all interested parties
55
+ */
56
+ public readonly set = this.withEmission((state: NonFunction<T>) => state);
57
+
58
+ /**
59
+ * Update
60
+ *
61
+ * Updates the current value of state using a setter function
62
+ * receiving the previous state as a parameter. Notifies all
63
+ * interested parties
64
+ */
65
+ public readonly update = this.withEmission((setter: Setter<T>) => {
66
+ if (this.diffSetter(setter)) {
67
+ return setter;
68
+ }
69
+ return setter(this.state);
70
+ });
71
+
72
+ /**
73
+ * Reset
74
+ *
75
+ * Resets the current state back to the state which the instance
76
+ * was initialized with. Notifies all interested parties
77
+ */
78
+ public readonly reset = this.withEmission(() => this.initialState);
79
+
80
+ /**
81
+ * Get Snapshot
82
+ *
83
+ * Returns the current state. Designed for compatibility with
84
+ * `useSyncExternalStore`
85
+ */
86
+ public readonly getSnapshot = () => {
87
+ return this.state;
88
+ };
89
+
90
+ /**
91
+ * Subscribe
92
+ *
93
+ * Registers a callback to be executed each time state
94
+ * changes. Returns an `unsubscribe` function
95
+ */
96
+ public readonly subscribe = (fn: Subscriber<T>) => {
97
+ const ID = this.Emitter.on("change", fn);
98
+ return () => {
99
+ this.Emitter.off("change", ID);
100
+ };
101
+ };
102
+
103
+ /**
104
+ * Register Middleware
105
+ *
106
+ * Registers any number of `Middleware` instances on the
107
+ * current instance of `State`. Your middleware will begin
108
+ * executing at the next state transition
109
+ */
110
+ public registerMiddleware(...middleware: Middleware<T>[]) {
111
+ this.middleware.push(...middleware);
112
+ }
113
+
114
+ private withEmission<
115
+ F extends (...args: any[]) => NonFunction<T> | Promise<NonFunction<T>>,
116
+ >(fn: F) {
117
+ return (...args: Parameters<F>) => {
118
+ const result = fn(...args);
119
+ if (result instanceof Promise) {
120
+ void result.then(resolved => this.emit(resolved));
121
+ return;
122
+ }
123
+ return this.emit(result);
124
+ };
125
+ }
126
+
127
+ private emit(nextState: NonFunction<T>) {
128
+ this.invokeMiddleware("onBeforeUpdate");
129
+ this.state = nextState;
130
+ this.Emitter.emit("change", this.state);
131
+ this.invokeMiddleware("onUpdate");
132
+ }
133
+
134
+ protected diffSetter(setter: Setter<T>): setter is NonFunction<T> {
135
+ return typeof setter !== "function";
136
+ }
137
+
138
+ private invokeMiddleware<K extends keyof Middleware<T>>(fn: K) {
139
+ for (const middleware of this.middleware) {
140
+ middleware[fn](this);
141
+ }
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Create State
147
+ *
148
+ * Returns the unit of reactivity for Galena. `State`'s can act
149
+ * as isolated instances or be part of your global app
150
+ * state (via `Galena` instances);
151
+ *
152
+ * ```typescript
153
+ * import { createState, Profiler } from "@figliolia/galena";
154
+ *
155
+ * const myState = createState("<any value>", ...middleware);
156
+ *
157
+ * myState.set("<new-value>");
158
+ * myState.update(previousValue => "<new-value>");
159
+ * myState.subscribe(nextValue => {});
160
+ * myState.registerMiddleware(new Profiler());
161
+ * ```
162
+ */
163
+ export function createState<T>(
164
+ ...args: ConstructorParameters<typeof State<T>>
165
+ ) {
166
+ return new State<T>(...args);
167
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
- export * from "Galena";
2
- export * from "Middleware";
3
- export * from "Middlewares";
1
+ export * from "./Galena";
2
+ export * from "./Logger";
3
+ export * from "./Middleware";
4
+ export * from "./Profiler";
5
+ export * from "./State";
6
+ export * from "./types";
package/src/types.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { State } from "./State";
2
+
3
+ export type NonFunction<T> = T extends (...args: any[]) => any ? never : T;
4
+
5
+ export type PartialSupport<T> = T extends Record<string, any> ? Partial<T> : T;
6
+
7
+ export type Setter<T> =
8
+ | NonFunction<T>
9
+ | ((prevState: NonFunction<T>) => NonFunction<T> | Promise<NonFunction<T>>);
10
+
11
+ export type Subscriber<T> = ((state: NonFunction<T>) => void) | (() => void);
12
+
13
+ export interface GalenaSnapshot<
14
+ T extends Record<string, State<any>>,
15
+ K extends Extract<keyof T, string> = Extract<keyof T, string>,
16
+ > {
17
+ updated: T[K];
18
+ state: T;
19
+ }
20
+
21
+ export type AppSubscriber<
22
+ T extends Record<string, State<any>>,
23
+ K extends Extract<keyof T, string> = Extract<keyof T, string>,
24
+ > = ((payload: GalenaSnapshot<T, K>) => void) | (() => void);
25
+
26
+ export type StateTypes<T extends Record<string, State<any>>> = ReturnType<
27
+ T[keyof T]["getSnapshot"]
28
+ >;